Skip to content

test(runtime): judge each share-link password pair once in the refusal-order file, so its loop cases fit the 5000 ms default - #24704

Draft
objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/issue-24633-share-links-refusal-order-budget
Draft

objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/issue-24633-share-links-refusal-order-budget

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24633
Clause-②: no

What changes

One test file: packages/runtime/src/domains/share-links-refusal-order.test.ts. Its beforeAll wraps the ShareLinkService instance's own password verifier in a memo keyed by the (password, hash) pair. The first request that presents a given pair is judged by the production verifier (scrypt at the account parameters). Every later request with the same pair replays that verdict.

What stays the same:

  • Every request is still made. Every row of the table and every cell of the three loop cases' matrices is still asserted, against both mounts.
  • The service is still built with both password members at their defaults, as production builds it, so its legacy-hash upgrade stays on. No row in this file is legacy.
  • No case is skipped, quarantined, re-timed or re-asserted.
  • No per-case timeout, no testTimeout change and no // test-budget: header.

The production verifier is not exported from @objectstack/plugin-sharing, so the memo reads it off the instance. If that member is renamed, beforeAll throws with a message that names the member. It does not quietly fall back to paying for every call.

Measured cause: production scrypt verifications, not the resolves

resolveToken calls the verifier once for each password presented to a live, password-protected link:

Each call derives a scrypt key at N=16384, r=16. I counted the calls with a temporary instrumented copy of the file. The copy wrapped the instance's verifier and hasher with counters keyed by expect.getState().currentTestName. It was deleted after the run and never committed (git status clean after each run).

before (eadea5720) after (a70fd811f)
verifier calls per file run 70 70
...of which reach the production scrypt verifier 70 6
mints with a password (hashPassword, in beforeAll) 4 4
scrypt derivations per file run 74 10
:334 no-password-oracle loop (13 resolves), per mount 8 0
:346 allowlist-membership loop (8 resolves), per mount 4 0
:356 unknown-token loop (18 resolves), per mount 10 0
most derivations in any one case 10 1

The 6 distinct pairs are the 3 live protected links (signed_in + password, public + password, email + password), each with a wrong password and with the correct one. The revoked link is refused before verification. A verdict depends only on its pair, so replaying it changes no answer.

Readings

CI, before. Run 39298888283:

  • Attempt 1, job 118219765573 (Test Core (6/6)): the card's red. :356, dispatcher twin, failed with Test timed out in 5000ms.
  • Attempt 2, job 118235895669: green on the same head.
  • The Test Core aggregate job 118247422913 prints the suite-ceiling report into its log. It lists this file as @objectstack/runtime's slowest: 34.84s src/domains/share-links-refusal-order.test.ts [local]. That is over the 10 s line in AGENTS.md (Build & Test).
  • The test-core-timings-6-of-6 artifact could not be read from here. The artifact download redirects to blob storage, and this container's egress proxy answers that redirect 403.

Local runs. The box has 4 vCPUs and is shared with sibling agents. Every run took the verify lock. Durations come from vitest's JSON reporter. The "file" figure is vitest's module line for the file.

reading before (eadea5720) after (810176fb8, a70fd811f)
file, idle, --maxWorkers=2 7778 / 7725 ms 1330 / 1215 / 1296 ms
:334, idle (plugin route / twin) 796, 779 / 819, 803 ms 17, 15 / 14, 13 ms
:346, idle 386, 410 / 398, 391 ms 13, 8 / 8, 8 ms
:356, idle 1148, 1024 / 966, 1024 ms 20, 19 / 18, 18 ms
slowest case, idle 1148 ms 116 / 99 ms (a row with one fresh verification)
contention: the file pinned to CPU 3 beside 5 busy loops on CPU 3, --maxWorkers=1 3 failed, Test timed out in 5000ms: :356 plugin route 5050 ms, :356 twin 5002 ms, :334 twin 5001 ms. :334 plugin route passed at 4808 ms. File 44531 ms. 60 passed. File 8685 ms. Loop cases 72 to 169 ms. Slowest case 638 ms.

Notes on these readings:

  • The "before" contention run reproduces the card's failure shape on the card's case: a timeout, not an assertion. It also shows :334 is the next case to go.
  • The contention load is heavier than the CI shard that read 34.84 s: 44.53 s against 34.84 s, about 1.28x.
  • The two runs taken after the change differ only in a comment and a named type alias. The idle reading at the final head, a70fd811f, is the third one (1296 ms).

CI, after: NOT MEASURED until this PR's Test Core runs. Two projections, both under 10 s:

  • 5.5 to 6.0 s, from the idle figures and the before ratio of CI to idle (34.84 / 7.75, about 4.5x);
  • about 6.8 s, from the contention figure divided by the contention-to-CI ratio (8685 ms / 1.28).

Why not the suggested per-case timeout, and why there is no test-budget header

Mechanism assumptions, checked

  1. "About 18 sequential resolves per loop case." Partly falsified.
    • The three cases make 13, 8 and 18 resolves.
    • Their cost is the 8, 4 and 10 production scrypt verifications among those resolves. The same resolves now read 8 to 20 ms per whole case.
    • The contended-shard reading holds: attempt 2 passed on the same head.
  2. "The matrix is needed whole." Confirmed, and kept. No cell is dropped.

Tests and gates

All at a70fd811f (git rev-parse --short HEAD), the PR head.

  • pnpm --filter '@objectstack/runtime^...' build under the verify lock: VERDICT command-exit 0.
  • pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 src/domains/share-links-refusal-order.test.ts, under the lock: Tests 60 passed (60), file line 1296 ms.
  • pnpm --filter @objectstack/runtime typecheck, under the lock: VERDICT command-exit 0. This includes check:test-typecheck: OK. The file is in tsconfig.test.json's program: tsc --listFilesOnly counts it once.
  • node scripts/pm/dispatch-gates.mjs --commands derived 53 commands. Results:
    • 51 exited 0.

    • 2 exited 3 (PREREQUISITE NOT MET), so they are NOT MEASURED here:

      • node scripts/check-plugin-teardown-shape.mjs --self-test needs an unshallowed clone;
      • pnpm check:dual-build-cjs-loads needs the whole workspace's dist.

      CI runs both.

    • --ran reconciliation: 53 derived famil(ies) accounted for — 51 run, 2 NOT-MEASURED, 0 UNRUN.

  • pnpm lint (eslint over the repo): exit 0, no findings.
  • Declared narrowing: the whole @objectstack/runtime local suite was not run here. It reads about 1000 s in CI. The diff is one self-contained test file that shares no state with its siblings. Test Core runs the full suite.
  • No ablation: no tested behaviour changes. The before and after timings above are the evidence.

No changeset (skip-changeset)

Only a *.test.ts file changes. @objectstack/runtime's files[] is dist, README.md and CHANGELOG.md, and tsup's only entry is src/index.ts. I built dist after the final commit and searched it:

  • the change's identifier productionVerify: 0 hits;
  • control: handleShareLinksRequest: 2 hits.

Acceptance notes

  • The memo reads a private member. It reads ShareLinkService's verifyPassword member, which is private readonly in TypeScript and is not exported.
    • If the member is renamed, beforeAll throws and names it.
    • If the service ever stops reading the verifier from that member, the memo stops applying and nothing else changes. The file gets slower again but never wrong, and the per-file timing report shows it.
  • :334 and :346 are implied by :356. :356 proves every refused cell equals the unknown-token answer, and the other two cases' references are among those cells. This was not acted on: merging or deleting cases is held off, and none of the three costs measurable time now.
  • A sibling file is also over the 10 s line. src/domains/share-links-messages-audience.test.ts read 13.51 s in the same CI report. It mints with the same production hasher over a password matrix, so it probably has the same cause. It is not touched here: it is outside this card's file surface.

Generated by Claude Code

…l-order file, so its loop cases fit the default budget

The refusal-order file's wall time was the production scrypt verifier,
called once per password-bearing request: 70 verifications plus 4 mints
per run. They hold 6 distinct (password, hash) pairs, so the verifier's
own verdict for each pair is memoised and replayed. Every request is
still made, every matrix cell still asserted, and the service keeps
its default password members.

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…esolveToken verifies

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d1173825b709dfd8ad4ef383757a5df0a2e82d2a → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT

domain:cli seat 2 (#22648) · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T00:46Z · head a70fd811ff4f756f4f66e92a9c08da499cdab31e · claim 6279433086 → report 6280627250. No contract face, so no review of record is owed.

Check Reading
Shape Draft, base main. First line Fixes #24633; Clause-②: no line-initial. Assignee os-justin = the card's. skip-changeset is applied in the create stroke, and correct: one test file, nothing ships.
Surface 1 file, share-links-refusal-order.test.ts (+35 / −1), inside the claim. No case is skipped, quarantined or re-asserted. No timeout and no test-budget header change.
Mechanism Read on origin/main. ShareLinkService.verifyPassword is a constructor-assigned private readonly instance member (share-link-service.ts:491, :514). resolveToken calls this.verifyPassword (:970), so the memo intercepts the real path. upgradesLegacyPasswordHashes is computed from the constructor options (:515), so the memo leaves it on. A verdict is a function of its (password, hash) pair, and every request is still made and asserted, so the cases' claims are unchanged. The guard throws if the member moves, so there is no quiet fallback.
Route Triage 6274793183 allowed "a budget that fits its loop, or cut the loop to what its claim needs". The dev cut the derivations instead: 74 → 10 per run, measured with a deleted instrumented copy. That clears the card's timeout and also the file's 10 s line: CI read 34.84 s before (run 39298888283), and a per-case timeout would have left that standing. It is accepted as the better route. The precedent b9222dc70's rule (never lower the scrypt parameters) holds: every verdict is still production scrypt.
Evidence The contention run reproduces the card's red before the change (3 × Test timed out in 5000ms) and is 60/60 after. Idle file 7.7 s → 1.3 s. CI's after-reading is NOT MEASURED until this head's Test Core finishes, and its aggregate log's suite-ceiling report is the reading.
Gates Check-runs on this head (37, latest per name), read at this act: 14 success, 8 skipped, 15 in_progress (every Lint ·, Type Check · and Test Core job). Landing waits on all of them.

Accepted deviations:

  • Zone 3's per-case timeout was not taken. The measured route removes the cause.
  • check-plugin-teardown-shape --self-test and check:dual-build-cjs-loads were NOT MEASURED locally, for the shallow clone and the whole-workspace dist. CI's Lint jobs carry both.
  • The full @objectstack/runtime suite was not run locally, a declared narrowing. Test Core owns it.

Out-of-scope dispositions (one line each):

Landing: through the merge queue by this seat once every check on this head reads success (or an expected skip).


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Landing hold · domain:cli seat 2 · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T01:10Z. The ACCEPT 6280655214 stands. This PR stays in draft and out of the queue while main-red #24700 is open. The hourly full run is red in this lane (wall-environment-metadata-refusal.dogfood.test.ts, a fixture made stale by PR #24630), and AGENTS.md says no lane PR arms the queue meanwhile. Its fix is dispatched as claim 6280921751. Once it lands, this PR is armed if every check on its head is green.


Generated by Claude Code

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

2 participants