Repository navigation
Commit 836bd90
test(plugin-sharing): size the three scrypt cases near vitest's 5000 ms default to their measured work (#22426)
Fixes #22418
Clause-②: no
## What changes
Three cases in `packages/plugins/plugin-sharing` get an explicit
per-case timeout, sized to the work each one measures. Every other
scrypt-reaching case in the package keeps vitest's 5000 ms default; the
enumeration below gives each one's reading and the reason.
| Case | Work (scrypt derivations) | Slowest `Test Core` reading |
Budget |
|:--|:--|:--|:--|
| `share-link-password-webcontainer.test.ts:95`, "a password whose NFKC
form differs from its input is one password on both paths" (the card's
case, `:72` on `ca135dcc`) | 4 pure-JS + 4 native | timed out at 5030
and 5039 ms; 5515 ms projected | `17_000` |
| `share-link-password-webcontainer.test.ts:74`, "a hash minted on
WebContainer verifies natively, and the reverse" | 3 pure-JS + 3 native
| 4136 ms | `13_000` |
| `share-link-password.test.ts:481`, "no log line carries the presented
password, on any outcome" | 9 native | 3805 ms | `12_000` |
Spelled the way this repo spells a per-case budget, as the third
argument of `it`: two named constants in the webcontainer file and a
literal `12_000` in the other. A comment beside each records the rule,
the derivation count and the readings, so the next author can re-size
from a fresh reading.
Not touched: the scrypt parameters (N, r, p), any production file, the
vitest config, `testTimeout` anywhere, and the case bodies. No skip, no
quarantine, no retry. No changeset: only two `*.test.ts` files change,
and the package's `files[]` is `dist`, `README.md` and `CHANGELOG.md`.
## The rule and the margin
- **Which cases get a budget:** a case whose slowest CI reading is over
half of the 5000 ms default. The default leaves every other case that
has a CI reading at least 2x headroom over its slowest reading, and 2x
is above the largest spread one case showed between the two CI runs:
1.88x (`/resolve still accepts the ?password= query parameter`, 588 ms
in one run and 1104 ms in the other).
- **How big:** 3x the slowest CI reading, rounded up to the next second.
3x covers that measured 1.88x spread with room for a heavier shard. The
suspected trigger for these reds, per the card, is a shard re-pack
(`040184752c`, #22368), which can change a shard's load again without
touching this package.
- **The NFKC case's reading is projected, because CI cut it off at the
timeout.** Its derivations are 4/3 of the cross-verify case's (4 + 4
against 3 + 3), and the cross-verify case read 4136 ms in the same run,
so the projection is 5515 ms. The 4/3 ratio checks out locally: the two
cases read 926 and 675 ms idle (1.37x), and 5557 and 4682 ms under
contention (1.19x). Under contention the projection overestimates, so it
errs toward a larger budget.
## Readings
**CI** (per-case durations from vitest's default reporter in the two red
`Test Core (5/6)` job logs on the card, read through the job-log API):
job `113665992253` (push, `e75dceddd7`) and job `113668129564` (PR
#22215 merge of `e75dceddd7`). The log API returns only the last 5000
lines of a 19k to 48k line log. In both red jobs, plugin-sharing's last
24 files fall in that window, and the two
`share-link-eligibility.test.ts` cases do not. Three hourly scheduled
`Test Core (5/6)` jobs (`113678118904` on `ca135dcc`, `113663806311`,
`113649948997`) and the two green jobs on the card had no plugin-sharing
lines in their windows. The direct REST log download is refused by this
container's egress proxy, so no other log was reachable.
**Local** (a 4-vCPU container; every timing run was taken under the
shared verify lock, before the gate battery started, with no other run
alongside; the load average was 0.2 when the work began). The census and
idle readings use `--maxWorkers=2`. The contention runs pin the four
scrypt-reaching files to one core beside 5 busy loops on that core, with
`--maxWorkers=1`. That load was picked because the NFKC case's
CI-to-local ratio is at least 5.4x (at least 5030 ms against 926 ms).
Case by case, it reads 0.95x to 1.48x the slowest CI reading, across the
22 cases that read over 1 s in CI and passed under it. So it is a load
as heavy as the red shard, or heavier.
| Case | Contention, before (`ca135dcc`) | Contention, after
(`616794ce1`) | Idle, before | Idle, after |
|:--|:--|:--|--:|--:|
| NFKC (webcontainer `:95`) | **`Test timed out in 5000ms`** (5024 ms) |
passed, 5557 ms | 958 ms | 833 ms |
| cross-verify (webcontainer `:74`) | passed, 3935 ms | passed, 4682 ms
| 675 ms | 681 ms |
| no-log (`share-link-password.test.ts:481`) | **`Test timed out in
5000ms`** (5067 ms) | passed, 5016 ms | 789 ms | 789 ms |
| whole contention run, 4 files | 2 failed, 94 passed (96) | 96 passed
(96) | | |
| idle run, the two named files | | | 48 passed (48) | 48 passed (48) |
The before contention run reproduces the card's failure shape: the same
case, as a timeout and not an assertion. It also shows the no-log case
is the next one to go.
## The enumeration pin: every case in the package that runs a
scrypt-class derivation
The only scrypt-class key derivation in the package is
`share-link-password.ts`: `deriveKeyNode` (`node:crypto` scrypt) and
`deriveKeyPureJs` (`@noble/hashes` scrypt). A test reaches them through
`hashShareLinkPassword` / `verifyShareLinkPassword`, through
`ShareLinkService.createLink` with a password, through `resolveToken`
against a stored `scrypt$` hash, or through the legacy upgrade.
The list is measured, not grepped: a temporary vitest setup file, never
committed, wrapped `node:crypto`'s `scrypt` and `scryptSync`, synced
them into the builtin ESM namespace, and counted the calls per case. It
ran over the whole package at `ca135dcc`: 40 files, 1002 cases, 1002
census rows. **52 cases in 4 files reach a derivation**:
- `share-link-password-webcontainer.test.ts`: 4 cases;
- `share-link-password.test.ts`: 42 of its 44 cases. The two others are
the injected-hasher case and the authenticated list-route case;
- `share-link-internal-columns.test.ts`: 4 cases;
- `share-link-eligibility.test.ts`: 2 cases.
Pure-JS derivations cannot be counted that way, because `@noble/hashes`
is a native ESM namespace. They are counted by reading the four
webcontainer cases, which are the only cases in the package that set a
WebContainer signal (a grep for `STACKBLITZ`, `/bin/jsh` and
`webcontainer` across the package's tests finds no other).
"Local, idle" is the census run. "CI" is the push / PR red run.
| File:line at HEAD | Case | scrypt work | Local, idle (ms) | CI (ms),
push / PR | Budget |
|:--|:--|:--|--:|:--|:--|
| `share-link-password-webcontainer.test.ts:95` | a password whose NFKC
form differs from its input is one password on both paths | 4 pure-JS +
4 native | 926 | **timed out** at 5030 / 5039 | **17_000 ms** = 3 × 5515
(projected), rounded up |
| `share-link-password-webcontainer.test.ts:74` | a hash minted on
WebContainer verifies natively, and the reverse | 3 pure-JS + 3 native |
675 | 4136 / 3688 | **13_000 ms** = 3 × 4136, rounded up |
| `share-link-password-webcontainer.test.ts:121` | the pure-JS key is
byte-identical to node:crypto for the same password and salt | 1 pure-JS
+ 1 native | 216 | 1435 / 1137 | default: 29% of 5000 ms |
| `share-link-password-webcontainer.test.ts:64` | the WebContainer path
does not touch node:crypto.scrypt; the native path does | 1 pure-JS + 1
native | 270 | 1305 / 1393 | default: 28% of 5000 ms |
| `share-link-password.test.ts:481` | no log line carries the presented
password, on any outcome | 9 native | 793 | 3805 / 3417 | **12_000 ms**
= 3 × 3805, rounded up |
| `share-link-password.test.ts:352` | legacy plaintext row › still
verifies, refuses a wrong password without touching the row, and is
upgraded by the right one | 5 native | 584 | 2163 / 1342 | default: 43%
of 5000 ms |
| `share-link-password.test.ts:352` | legacy salted SHA-256 row › still
verifies, refuses a wrong password without touching the row, and is
upgraded by the right one | 5 native | 731 | 2132 / 1815 | default: 43%
of 5000 ms |
| `share-link-password.test.ts:617` | /resolve sends no-store + Vary on
success and on every refusal | 3 native | 280 | 1573 / 938 | default:
31% of 5000 ms |
| `share-link-password.test.ts:338` | the verifier accepts the right
password and refuses a wrong one, an empty one and an unknown form | 4
native | 332 | 1407 / 1356 | default: 28% of 5000 ms |
| `share-link-password.test.ts:284` | the list and the redemption
result, through the service and through the route, carry no hash | 3
native | 291 | 1387 / 951 | default: 28% of 5000 ms |
| `share-link-password.test.ts:617` | /messages sends no-store + Vary on
success and on every refusal | 3 native | 289 | 1337 / 942 | default:
27% of 5000 ms |
| `share-link-password.test.ts:582` | /resolve never falls back to a raw
compare: an undecodable value that IS the raw password is still refused
| 2 native | 212 | 1244 / 992 | default: 25% of 5000 ms |
| `share-link-password.test.ts:391` | a refused upgrade write does not
block the read, leaves the legacy form verifying, and is reported once
without the password | 3 native | 260 | 900 / 1138 | default: 23% of
5000 ms |
| `share-link-password.test.ts:552` | /messages still serves a raw with
a %-escape password sent raw, with no encoding header | 2 native | 174 |
1110 / 896 | default: 22% of 5000 ms |
| `share-link-password.test.ts:445` | /resolve still accepts the
?password= query parameter | 2 native | 197 | 588 / 1104 | default: 22%
of 5000 ms |
| `share-link-password.test.ts:535` | /messages does not guess: the same
encoded value with no encoding header is compared raw | 2 native | 195 |
1092 / 955 | default: 22% of 5000 ms |
| `share-link-password.test.ts:600` | /messages: the ?password= form
still wins, and the header pair is then not read | 2 native | 212 | 1075
/ 886 | default: 22% of 5000 ms |
| `share-link-password.test.ts:552` | /messages still serves a Latin-1
password sent raw, with no encoding header | 2 native | 187 | 1072 / 822
| default: 21% of 5000 ms |
| `share-link-password.test.ts:523` | /messages serves a emoji password
sent percent-encoded under utf-8 | 2 native | 196 | 994 / 1046 |
default: 21% of 5000 ms |
| `share-link-password.test.ts:600` | /resolve: the ?password= form
still wins, and the header pair is then not read | 2 native | 188 | 1044
/ 828 | default: 21% of 5000 ms |
| `share-link-password.test.ts:552` | /messages still serves a raw with
a stray % password sent raw, with no encoding header | 2 native | 174 |
1038 / 951 | default: 21% of 5000 ms |
| `share-link-password.test.ts:552` | /resolve still serves a raw with a
%-escape password sent raw, with no encoding header | 2 native | 183 |
938 / 1033 | default: 21% of 5000 ms |
| `share-link-password.test.ts:535` | /resolve does not guess: the same
encoded value with no encoding header is compared raw | 2 native | 187 |
988 / 1000 | default: 20% of 5000 ms |
| `share-link-password.test.ts:326` | a new link stores scrypt, with no
plaintext, and two hashes of one password differ | 2 native | 172 | 710
/ 951 | default: 19% of 5000 ms |
| `share-link-password.test.ts:523` | /messages serves a CJK password
sent percent-encoded under utf-8 | 2 native | 170 | 878 / 950 | default:
19% of 5000 ms |
| `share-link-password.test.ts:433` | /resolve accepts the
x-share-password header | 2 native | 179 | 712 / 945 | default: 19% of
5000 ms |
| `share-link-password.test.ts:445` | /messages still accepts the
?password= query parameter | 2 native | 223 | 943 / 700 | default: 19%
of 5000 ms |
| `share-link-password.test.ts:552` | /resolve still serves a raw with a
stray % password sent raw, with no encoding header | 2 native | 202 |
937 / 738 | default: 19% of 5000 ms |
| `share-link-password.test.ts:460` | /resolve refuses a wrong password
sent as a header | 2 native | 196 | 841 / 913 | default: 18% of 5000 ms
|
| `share-link-password.test.ts:552` | /resolve still serves a Latin-1
password sent raw, with no encoding header | 2 native | 210 | 912 / 865
| default: 18% of 5000 ms |
| `share-link-password.test.ts:267` | the mint response, through the
service and through the route, carries no hash | 2 native | 225 | 799 /
900 | default: 18% of 5000 ms |
| `share-link-password.test.ts:433` | /messages accepts the
x-share-password header | 2 native | 206 | 824 / 897 | default: 18% of
5000 ms |
| `share-link-password.test.ts:305` | the list and the redemption result
carry no hash even from an engine that does not strip it | 2 native |
191 | 884 / 895 | default: 18% of 5000 ms |
| `share-link-password.test.ts:460` | /resolve refuses a wrong password
sent as a query | 2 native | 217 | 894 / 874 | default: 18% of 5000 ms |
| `share-link-password.test.ts:523` | /resolve serves a emoji password
sent percent-encoded under utf-8 | 2 native | 178 | 843 / 819 | default:
17% of 5000 ms |
| `share-link-password.test.ts:582` | /messages never falls back to a
raw compare: an undecodable value that IS the raw password is still
refused | 2 native | 221 | 795 / 834 | default: 17% of 5000 ms |
| `share-link-password.test.ts:460` | /messages refuses a wrong password
sent as a header | 2 native | 173 | 773 / 770 | default: 15% of 5000 ms
|
| `share-link-password.test.ts:460` | /messages refuses a wrong password
sent as a query | 2 native | 208 | 652 / 741 | default: 15% of 5000 ms |
| `share-link-password.test.ts:523` | /resolve serves a CJK password
sent percent-encoded under utf-8 | 2 native | 200 | 616 / 691 | default:
14% of 5000 ms |
| `share-link-password.test.ts:568` | /messages refuses an octet that is
not UTF-8 with 400 VALIDATION_FAILED, before the token is looked up | 1
native | 87 | 574 / 513 | default: 11% of 5000 ms |
| `share-link-password.test.ts:568` | /resolve refuses an encoding the
server does not read with 400 VALIDATION_FAILED, before the token is
looked up | 1 native | 88 | 550 / 365 | default: 11% of 5000 ms |
| `share-link-password.test.ts:568` | /resolve refuses an octet that is
not UTF-8 with 400 VALIDATION_FAILED, before the token is looked up | 1
native | 110 | 538 / 484 | default: 11% of 5000 ms |
| `share-link-password.test.ts:568` | /messages refuses an encoding the
server does not read with 400 VALIDATION_FAILED, before the token is
looked up | 1 native | 104 | 537 / 450 | default: 11% of 5000 ms |
| `share-link-password.test.ts:568` | /resolve refuses a truncated UTF-8
sequence with 400 VALIDATION_FAILED, before the token is looked up | 1
native | 86 | 446 / 497 | default: 10% of 5000 ms |
| `share-link-password.test.ts:568` | /messages refuses a truncated
UTF-8 sequence with 400 VALIDATION_FAILED, before the token is looked up
| 1 native | 92 | 496 / 455 | default: 10% of 5000 ms |
| `share-link-password.test.ts:375` | is not attempted for a
switched-off object, whose redemption is refused | 1 native | 96 | 303 /
439 | default: 9% of 5000 ms |
| `share-link-eligibility.test.ts:1191` | the `password_hash` shape —
NEEDS_PASSWORD and WRONG_PASSWORD both become the unknown-token answer |
5 native | 662 | NOT MEASURED (outside both logs' tail windows) |
default (eligibility note below) |
| `share-link-eligibility.test.ts:1291` | fail-closed: an object whose
schema the engine cannot answer for is refused, not probed | 2 native |
336 | NOT MEASURED (outside both logs' tail windows) | default
(eligibility note below) |
| `share-link-internal-columns.test.ts:143` | a password-protected link
refuses without the password and with a wrong one, and opens with it | 3
native | 318 | 1361 / 1528 | default: 31% of 5000 ms |
| `share-link-internal-columns.test.ts:155` | the redemption route
answers password-required, refuses a wrong password, and opens with the
right one | 3 native | 295 | 1489 / 1289 | default: 30% of 5000 ms |
| `share-link-internal-columns.test.ts:130` | the generic read omits
both columns; storage holds both | 1 native | 150 | 774 / 566 | default:
15% of 5000 ms |
| `share-link-internal-columns.test.ts:182` | the creator's link list
carries each link's token, and no hash | 1 native | 95 | 391 / 416 |
default: 8% of 5000 ms |
**The eligibility note.** Neither `share-link-eligibility.test.ts` case
has a CI reading, because both finished before either red log's window
opened. They keep the default, on two measured comparisons:
- **The same work as the legacy rows.** The `password_hash`-shape case
does 5 native derivations, exactly as many as each legacy row, and its
idle reading is between theirs: 662 ms against 731 and 584 ms. The
legacy rows' slowest CI reading is 2163 ms, 43% of the default.
- **Under the contention load it passed,** at 2832 ms (before) and 2902
ms (after), beside the legacy rows at 2714 / 2638 ms and 2800 / 2988 ms
in the same runs. That load reads up to 1.48x the CI shard, and the case
was still at 58% of the default.
Its CI time is NOT MEASURED, and is stated as such rather than inferred
into the table. The fail-closed case does 2 derivations and read 336 ms
idle and 1113 ms under contention.
## Mechanism assumptions, checked
1. *"Two test files run scrypt-class derivations."* **Four do.**
`share-link-internal-columns.test.ts` (4 cases) and
`share-link-eligibility.test.ts` (2 cases) reach the derivation through
`ShareLinkService`. Neither is anywhere near the budget, so neither is
edited, and the file surface stays the two claimed files.
2. *"5030 / 5039 ms for the NFKC case and 1.1 to 4.1 s for its
siblings."* Confirmed from the same two job logs: siblings 1305 / 1393,
4136 / 3688 and 1435 / 1137 ms. The logs also show a third case near the
budget in the other file, the no-log case at 3805 / 3417 ms. The
contention run reproduces it as the next timeout.
3. *"Follow #21920's timeout spelling and margin comment."* **The
precedent has no per-case timeout.** #21920 landed as PR #21925
(`7b6c65290`), which states "No global `testTimeout` change, no per-case
timeout". It moved a module load out of the clocked window into
collection, because that cost was loading, not the case's own work. The
shape does not transfer here. The cost here is the case's own work, the
triage ruled that work stays, and the derivation's only module load
(`@noble/hashes`, on the first pure-JS call) is small: the two
one-derivation-per-path cases read 1305 / 1393 and 1435 / 1137 ms in CI.
So the spelling follows the repo's own per-case convention instead: the
third argument of `it`, with a comment stating why and how much.
`packages/drivers/driver-sql`'s "Why the it() below carries an explicit
60_000 budget" blocks are an example.
## Tests and gates
All at `616794ce1` (`git rev-parse --short HEAD`), the PR head.
- `pnpm --filter '@objectstack/plugin-sharing^...' build` (the
dependency closure), under the verify lock: `VERDICT command-exit 0`.
- `pnpm --filter @objectstack/plugin-sharing test`, under the lock:
`Test Files 40 passed (40)`, `Tests 1002 passed (1002)`, `VERDICT
command-exit 0`.
- `pnpm --filter @objectstack/plugin-sharing typecheck`, under the lock:
`VERDICT command-exit 0`, with `check:test-typecheck: OK`, 3
pre-existing ledgered errors held. `tsc -p tsconfig.test.json
--listFiles` counts 40 `src/**/*.test.ts` in the program, both edited
files among them.
- The two named files, `pnpm --filter @objectstack/plugin-sharing exec
vitest run --maxWorkers=2 FILES`: 48 passed (48), before and after.
Per-case readings are in the table above.
- The contention reproduction, before and after: shown in the readings
table above.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 57 commands for this diff, and all
57 exit 0. Three needed a second run, which is declared here:
- `check:i18n` and `check:dual-build-cjs-loads` first answered
`PREREQUISITE NOT MET` (exit 3, no `dist/`). Each was run again after
building its prerequisite (the i18n closure the gate names, then the
workspace build), and both exit 0.
- `check:type-check-debt` first answered exit 3 because its own build
collided with my concurrent closure build: `plugin-webhooks`' DTS step
read a half-written `service-messaging` `dist/`. Run again alone, it
exits 0.
- `--ran` reconciliation: "57 derived famil(ies) accounted for — 57 run,
0 NOT-MEASURED (a DERIVED zero — all 57 recorded an exit code and none
of them is 3)".
- Lint, narrowed to the two changed files: `eslint --no-inline-config
--format json` on both files gives 0 errors and 0 warnings. The
narrowing loses nothing, for three reasons:
- both files are inside `eslint.config.mjs`'s population, since an
ignored file would come back with an ignore warning;
- the JSON output counts 2 files;
- the config never enables type-aware linting (its own comment says so:
no `parserOptions.project`, no typed rules) and loads no
import-resolving plugin, so this diff cannot move the verdict on any
file it does not touch.
The full `pnpm lint` is CI's run.
- No ablation leg is needed beyond the before/after pair. The "before"
leg is the committed `ca135dcc` tree, which is exactly this change
removed, and it was run under the same load as the "after" leg.
## Acceptance notes
- `share-link-password.test.ts`'s header lists, as pinned, "the pure-JS
scrypt the WebContainer path uses and `node:crypto`'s produce
interchangeable hashes". No case in that file pins it: the pin lives in
`share-link-password-webcontainer.test.ts`. This is a stale doc bullet,
an observation only, and not filed. Carrier: none.
- The job-log API returns only a log's last 5000 lines, and this
container's egress proxy refuses the REST log download. So per-case CI
readings were reachable only for files that finish late in their shard.
That is why the eligibility cases have no CI reading. It is an
observation about the measuring instrument, not about the code.
- Re-sizing: when a budgeted case's derivation count changes, its budget
changes by the same ratio, from a fresh CI reading. Lowering N or r to
make a case fit is ruled out by the triage on the card.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01WYYhVJ78u7PhwFViWo1EmQ)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent b0a4aed commit 836bd90
2 files changed
Lines changed: 33 additions & 3 deletions
File tree
- packages/plugins/plugin-sharing/src
Lines changed: 25 additions & 2 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
27 | 27 | | |
28 | 28 | | |
29 | 29 | | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
30 | 53 | | |
31 | 54 | | |
32 | 55 | | |
| |||
67 | 90 | | |
68 | 91 | | |
69 | 92 | | |
70 | | - | |
| 93 | + | |
71 | 94 | | |
72 | 95 | | |
73 | 96 | | |
| |||
93 | 116 | | |
94 | 117 | | |
95 | 118 | | |
96 | | - | |
| 119 | + | |
97 | 120 | | |
98 | 121 | | |
99 | 122 | | |
| |||
Lines changed: 8 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
471 | 471 | | |
472 | 472 | | |
473 | 473 | | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
474 | 481 | | |
475 | 482 | | |
476 | 483 | | |
| |||
481 | 488 | | |
482 | 489 | | |
483 | 490 | | |
484 | | - | |
| 491 | + | |
485 | 492 | | |
486 | 493 | | |
487 | 494 | | |
| |||
0 commit comments