Skip to content

Commit fe97d85

Browse files
test(runtime): hold clock-derived header values fixed in the /meta action-entry wire comparison (#31195)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiQbT4FcQAg8Z3TPX85xEdZz)_ Fixes #31171 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:只改一个测试文件。`/meta` 动作条目对比测试在比较两次响应时,把 `Last-Modified`(以及 `Date`)头的日期值视为固定占位,其余字节照旧逐字比较。 - 为什么改:这个头的值就是"读取那一刻的时间",精度到秒;两次连续请求恰好跨过一秒边界时,测试就会误报失败,今天已经把 #31142 和 #31149 踢出合并队列两次。 - 风险与回滚:仅测试代码,不发布任何包;状态码、正文、ETag 和其它所有头仍逐字比较,头的有无也仍然比较。回滚就是还原这一个文件。 - 先看:`packages/runtime/src/domains/mcp-object-schema-mask.test.ts` 里新的 `CLOCK_HEADERS` 和 `wireText`。 - 两分钟验证:`pnpm --filter @objectstack/runtime exec vitest run src/domains/mcp-object-schema-mask.test.ts`,预期 45 个用例全部通过。 - 需要你做的:无,席位落地。 ## What changes `wireText` in the ADR-0106 D1 action-entry describe of `packages/runtime/src/domains/mcp-object-schema-mask.test.ts` serialised the whole `/meta` wire (status, body, headers) and every comparison in that describe is an exact string equality over it. The REST cached arm sends `Last-Modified`, and the protocol stamps it with the time of the read (`packages/metadata-protocol/src/protocol-search-and-cache.ts`, `lastModified: new Date().toISOString()`), rendered at one-second precision. Two sequential reads that straddle a second boundary therefore differ in that header only. `wireText` now reads the value of a `Last-Modified` or `Date` header (case-insensitive) as the fixed token `CLOCK` when the value parses as a date. Everything else is compared exactly as before: - status, body, `ETag`, `Cache-Control`, `Vary` and every other header, byte for byte; - the clock header's presence and its position among the headers (an answer that gains or drops `Last-Modified` still differs); - a clock header whose value is not a date (it is kept verbatim). Every `wireText` caller in the describe (the absent-name equivalences, the `layers` / `legacyLayers` / `published` reads, the member-vs-staff CONTROL, the exempt-vs-unsecured CONTROL and the conditional-read 404) still proves the same thing: none of them is about the time of the read. Both queue ejections were this header, read from the Test Core logs: - #31142, build 42383103865, job 128435166865: line 801 (exempt vs unsecured, `usr_system item st_ping`), `Last-Modified` `14:49:31 GMT` vs `14:49:30 GMT`, every other byte equal. - #31149, build 42385456389: line 785 (member vs staff), `15:19:24 GMT` vs `15:19:23 GMT` (diagnosis on the card). No nondeterminism in the projection itself was found: in both logs the body and the `ETag` are equal and the header keys come in the same order. ## Tests <details><summary>Ablation (one-time, the test file mutated through scripts/ablation-replace.mjs, restored to HEAD after each leg)</summary> The member-vs-staff CONTROL assertion was replaced so that the staff read happens with the clock moved forward one second (`vi.useFakeTimers({ toFake: ['Date'] })` + `vi.setSystemTime(Date.now() + 1000)`), with a precondition on the REST cached arm that the two `Last-Modified` values really differ. Run with `-t "CONTROL — a field-bound action"` (3 tests: REST cached, REST uncached, dispatcher). - Leg A, new `wireText` + one-second skew: 3 passed. - Leg B, old helper (`JSON.stringify`) + one-second skew: REST cached arm FAILS on `st_rename`; the diff is `"Last-Modified":"Sat, 07 Nov 2026 16:14:07 GMT"` vs `16:14:06 GMT`. This reproduces the queue failure deterministically. - Leg C, new `wireText`, no skew, `Last-Modified` deleted from the staff answer: REST cached arm FAILS (`"Last-Modified":"CLOCK"` against an answer without the header), so presence is still compared. Each leg: `ablation-replace: ok mutation landed` before the run and `ok restored: blob == HEAD (784ab70) and git diff HEAD is empty` after it. The test file runs from source, so no `dist/` is involved. </details> <details><summary>Repeat runs</summary> - `pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/mcp-object-schema-mask.test.ts`, 15 runs in a loop on dbd57b6: 15 × `Tests 45 passed (45)`, every run exit 0. </details> ## Acceptance notes - The `Last-Modified` the `/meta` item door sends on its cached arm is the time of the read, not the time the item last changed (`protocol-search-and-cache.ts` stamps `new Date()`), so it carries no information a client can use; freshness is gated by the `ETag`. Noted, not filed: this PR does not change it and no test here depends on it. - The uncached REST arm and the dispatcher door send no `Last-Modified`, so on those doors the comparison was already stable. ## Verification Merged `origin/main` (eb68b98) once before opening; the derived gate list was taken at fc6ee74. <details><summary>Derived gates (node scripts/pm/dispatch-gates.mjs --commands, 50 commands)</summary> All on fc6ee74 (after the merge of origin/main): - `node scripts/check-closing-keyword-parity.mjs :: exit 0` - `node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0` - `node scripts/check-comment-mask-adoption.mjs :: exit 0` - `node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0` - `node scripts/check-comment-mask-corpus.mjs :: exit 0` - `node scripts/check-dts-emitted.mjs --self-test :: exit 0` - `node scripts/check-issue-citations.mjs :: exit 0` - `node scripts/check-plugin-teardown-shape.mjs :: exit 0` - `node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0` - `node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0` - `node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0` - `node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0` - `node scripts/docs-audit/check-affected-docs.mjs :: exit 0` - `node scripts/docs-audit/check-drift-comment.mjs :: exit 0` - `pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0` - `pnpm check:ci-filter-parity :: exit 0` - `pnpm check:closed-ceremony-text :: exit 0` - `pnpm check:cross-package-test-inputs :: exit 0` - `pnpm check:doc-authoring :: exit 0` - `pnpm check:driver-memory-census :: exit 0` - `pnpm check:dts-closure :: exit 0` - `pnpm check:dual-build-cjs-loads :: exit 0` - `pnpm check:engine-double-contract :: exit 0` - `pnpm check:gitlink-declared :: exit 0` - `pnpm check:issue-citations :: exit 0` - `pnpm check:keyed-text-bounds :: exit 0` - `pnpm check:lean-entry-closure :: exit 0` - `pnpm check:logger-receiver-detach :: exit 0` - `pnpm check:nul-bytes :: exit 0` - `pnpm check:objectql-double-limit :: exit 0` - `pnpm check:org-identifier :: exit 0` - `pnpm check:page-declaration-shape :: exit 0` - `pnpm check:platform-object-tenancy-census :: exit 0` - `pnpm check:published-files :: exit 0` - `pnpm check:query-options-erasure :: exit 0` - `pnpm check:refd-timer-probe :: exit 0` - `pnpm check:registry-log-declared :: exit 0` - `pnpm check:rest-log-spy-declared :: exit 0` - `pnpm check:route-envelope :: exit 0` - `pnpm check:slot-lookup :: exit 0` - `pnpm check:sourcemap-no-sources-content :: exit 0` - `pnpm check:system-context-census :: exit 0` - `pnpm check:test-source-alias :: exit 0` - `pnpm check:tier-file-adoption :: exit 0` - `pnpm check:type-check-coverage :: exit 0` - `pnpm check:type-check-debt :: exit 0` - `pnpm check:undeclared-dep-imports :: exit 0` - `pnpm check:vouch-for-reference-callers :: exit 0` - `pnpm check:watch-hint-literal :: exit 0` - `pnpm check:where-matcher :: exit 0` Two first ran exit 3 and were rerun after building their prerequisite: `node scripts/check-plugin-teardown-shape.mjs --self-test` (positive-control commit 621a487 not in the shallow clone; fetched it), `pnpm check:dual-build-cjs-loads` (no dist; full `turbo run build`, 119/119 tasks). Reconciliation: `node scripts/pm/dispatch-gates.mjs --ran` → 50 derived, 50 run, 0 NOT-MEASURED, 0 UNRUN. Package: `pnpm --filter @objectstack/runtime typecheck` exit 0 (tsc + check:test-typecheck); `pnpm --filter @objectstack/runtime test` exit 0, Test Files 554 passed (554), Tests 8918 passed / 19 skipped; eslint over packages/runtime (`--no-inline-config`) exit 0. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (Test-only; `@objectstack/runtime` ships `dist` only, so nothing publishes: `skip-changeset`.) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_017ihL2gH1g3Qk8UMKLnGKoq)_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1c66988 commit fe97d85

1 file changed

Lines changed: 21 additions & 1 deletion

File tree

‎packages/runtime/src/domains/mcp-object-schema-mask.test.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,27 @@ describe('[ADR-0106 D1] a standalone or object-less action entry is served as th
652652

653653
/** One `/meta` answer, every byte a caller can compare: status, body and headers. */
654654
interface Wire { status: number; body: any; headers: Record<string, string> }
655-
const wireText = (w: Wire) => JSON.stringify(w);
655+
/**
656+
* Headers whose value is the wall clock at the moment of the read, not
657+
* anything the projection decided: the protocol stamps `lastModified` with
658+
* the time of the read, which the door renders at one-second precision, so
659+
* two sequential reads of one answer differ there whenever they straddle a
660+
* second boundary.
661+
*/
662+
const CLOCK_HEADERS = new Set(['last-modified', 'date']);
663+
/**
664+
* The wire as compared: every byte, except that a clock header's VALUE
665+
* reads as a fixed token when it is a date. Its presence, its position and
666+
* a value that is not a date are still compared, so an answer that gains
667+
* or drops the header still differs.
668+
*/
669+
const wireText = (w: Wire) => JSON.stringify({
670+
...w,
671+
headers: w.headers && Object.fromEntries(Object.entries(w.headers).map(([k, v]) => [
672+
k,
673+
CLOCK_HEADERS.has(k.toLowerCase()) && !Number.isNaN(Date.parse(v)) ? 'CLOCK' : v,
674+
])),
675+
});
656676

657677
/** A code/package store answering `/published` with each standalone fixture as declared, and nothing for any other name. */
658678
const publishedStore = {

0 commit comments

Comments
 (0)