Skip to content

test(http-conformance): a real createHonoApp host holds PUT /meta If-Match, If-None-Match and ?mode=draft - #22341

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22221-hono-meta-write-conformance
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-22221-hono-meta-write-conformance

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22221
Clause-②: no

What this adds

One test file, no production code: packages/qa/http-conformance/src/hono-meta-save-preconditions.conformance.test.ts. It composes the real createHonoApp over a LiteKernel and sends every request through app.request(...). So the @objectstack/hono catch-all's hand-off to dispatch() (packages/adapters/hono/src/index.ts:739) is in the path, which no test held before. Four rows. Each asserts the status, the envelope, and the stored rows, read back from sys_metadata after the request:

row request through the catch-all asserted
control PUT /api/v1/meta/view/case_grid twice, no header 200, success: true, receipts state: 'active'; active row v2, no draft row
stale If-Match PUT with If-Match set to the first receipt's version 409, success: false, error.code METADATA_CONFLICT; active row still v2; the current token then saves v3
If-None-Match: * a first write with it (200), then the same over the existing row 409, success: false, METADATA_CONFLICT; active row still first
?mode=draft PUT ?mode=draft over an active row 200, receipt state: 'draft'; active row unchanged, draft row staged; GET ...?state=draft answers staged, and the plain GET still answers the active row

File choice (the seat's cut): a new file beside the read-side twin, not rows added to it. hono-meta-item-read-gate.conformance.test.ts is about the read door, and it answers from a stub protocol (a vi.fn over a plain object) that keeps no version and no draft. The write rows need a real store. So they live in their own file, and each file stays about one door.

There is no package.json, turbo.json or scripts/cross-package-test-inputs.mjs change: every import is a dependency this package already declares (H5 below). There is no changeset: @objectstack/http-conformance is private: true, and the diff is one test file.

Readings (the PM's H1 to H5, measured on origin/main 28bff18d)

  • H1, the line under test: holds. The read path:

    • the catch-all calls dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix) (packages/adapters/hono/src/index.ts:739);
    • that reaches handleMetadataRequest's PUT branch, which calls metaSaveRequestOptions({ headers: _context.request?.headers, query }) (packages/runtime/src/domains/meta.ts:1366; the function is in packages/rest/src/meta-save-request.ts);
    • the answer is spread into protocol.saveMetaItem.

    The function that reads each one:

    • If-Match and If-None-Match: metaSaveRequestOptions, through its requestHeader() helper. That helper calls Headers.get on the raw Request the catch-all hands on as context.request.
    • ?mode=draft: also metaSaveRequestOptions, but from http.query. That is the catch-all's queryParams argument (flattened from the URL), not the Request. See H4.
  • H2, the harness: the twin's shape is reused, its store is not. Same shape: LiteKernel, then createHonoApp({ kernel, prefix: '/api/v1', cors: false }), then app.request. Same single stubbed seam: HttpDispatcher.prototype.timedResolveExecutionContext. Two differences, both deliberate:

    • Store. The twin's stub protocol cannot refuse or stage a save. What I added: ObjectQLPlugin's built-in assembly (registerProtocol at its default). It registers a real ObjectStackProtocolImplementation and the sys_metadata* objects. It runs over SqliteWasmDriver({ filename: ':memory:' }), handed in as a driver.memory service. The boot logs Schema sync complete {"synced":5}. Both packages were already in this package's devDependencies, because the integration suite boots them.
    • Identity. The twin keys the principal on a request header. Here the stub answers one author (manage_metadata) for every request, so no row depends on the request to know who the caller is. Otherwise, ablating the { request } hand-off would also strip the caller. Every row, the control included, would then go red for the wrong reason.
  • H3, the four rows through the real app: hold. See the table above. 4 passed.

  • H4, the ablation: partly falsified. Passing {} turns only the two header rows red. The ?mode=draft row stays green under it, because mode rides the queryParams argument of the same call, not context.request. So the draft row gets its own ablation of the hand-off: the query argument. The control stayed green in every leg. Table below.

  • H5, cross-package test inputs: nothing to add. pnpm check:cross-package-test-inputs exits 0: OK: 30 package(s) read outside themselves, all declared ... 2854 test file(s) import a workspace sibling by bare specifier over 378 package pair(s), every one declared. The new file imports only by bare specifier, over pairs that are already declared.

Ablations (one per row, never committed)

The subject is packages/adapters/hono/src/index.ts, HEAD blob 8c32e89e8fc2. vitest.config.ts's anchored alias points @objectstack/hono at that SOURCE, so no dist/ leg applies. Each leg went red while packages/adapters/hono/dist was untouched, which proves the source is what runs.

How each leg ran:

  • through node scripts/ablation-replace.mjs in WRAP mode, under os-verify-lock.sh;
  • the anchor hit x1 before and x0 after, and the mutation was counted on disk;
  • the file was then restored, proven by its blob (8c32e89e8fc2, equal to HEAD) and an empty git diff HEAD.

I wrote down the expected direction of each leg before running it. Each leg landed as predicted.

leg mutation at :739 control stale If-Match If-None-Match: * ?mode=draft
head none green green green green
A { request: c.req.raw } → {} (blob 3900c478d42e) green red: 200, success: true where 409 was expected red: 200 where 409 was expected green
A' the same argument → { request: new Request(c.req.raw.url, { method: c.req.method }) }, a Request with no headers (blob effd7d0f0b20) green red red green
B queryParams → {} in the same call (blob b64245cd76d5) green green green red: receipt state: 'active' where 'draft' was expected

Each red reproduces the defect #22141 described, on the wire: a guarded save was answered 200 and written, or a draft went live.

Tier

Test Core, on every PR. ci.yml's Test Core (N/6) shards run pnpm turbo run test over the affected packages. This package's test script is a plain vitest run (one config, no project split), so the new file runs whenever @objectstack/http-conformance is affected. Test Core is one of the seven required contexts.

Verification (head e66efff7, base 28bff18d)

  • Build. pnpm --workspace-concurrency=2 --filter '@objectstack/http-conformance^...' build (the dependency closure): exit 0. A grep confirms that @objectstack/runtime's dist/ carries metaSaveRequestOptions({ headers: _context.request?.headers, query }).
  • Tests. pnpm --filter @objectstack/http-conformance test: Test Files 9 passed (9), Tests 106 passed (106).
  • Typecheck. pnpm --filter @objectstack/http-conformance typecheck: exit 0, check:test-typecheck: OK ... 3 file(s) / 27 error(s) / 10 pinned signature(s) held, ledger unchanged. The new file is in that program: tsc --listFiles -p tsconfig.test.json lists 9 of the package's 9 test files, the new one included. A file the ledger does not list may carry no error.
  • Gates. I ran the order's 71 commands and recorded each exit code before any pipe: 70 exited 0.
    • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, re-derived at head, gives 53 commands (1 path, 206 changed lines), a subset of the 71.
    • The --ran reconciliation: ✓ dispatch-gates --ran: 53 derived famil(ies) accounted for — 52 run, 1 NOT-MEASURED, 0 unrun.
    • NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: exit 3, PREREQUISITE NOT MET. 34 packages are unbuilt in this worktree. This diff adds one test file to a private package that has no build script, so it emits nothing that gate reads. CI runs the gate in full.
  • Lint. pnpm lint (eslint . --no-inline-config, the whole repo) at e66efff7: exit 0, no findings.
  • Upstream. origin/main is 1 commit past the base (e36ee535). It touches 10 files under service-storage, plugins/organizations and qa/dogfood, none of them in this suite's dependency closure, so I did not merge main.

Acceptance notes

  • H4's reading was half right, and the missing half matters to anyone who reads the catch-all. The { request: c.req.raw } argument carries the two precondition headers. The queryParams argument carries the lifecycle. A row now goes red if either one is lost.
  • Boot noise, not a finding. Each boot prints two [sql-driver] DATABASE_ERROR lines (no such table: sys_setting, no such table: _objectstack_sequences) at kernel:ready. They come from platform-migration probes against tables this minimal composition does not provision. They print before the first request, and no row depends on them.
  • The catch-all's 409 still carries no currentVersion as data (PR fix(runtime): the @objectstack/hono catch-all's PUT /meta honours If-Match, If-None-Match and ?mode=draft #22206's open question H6). These rows assert only status, success and error.code, so they neither pin nor block any answer to that question.

Written by session session_01RWZbGvPFcRKvUqASZtunCU.


Generated by Claude Code

…f-Match, If-None-Match and ?mode=draft

Four rows through app.request over a real ObjectStackProtocolImplementation
(ObjectQLPlugin's built-in assembly on in-memory SQLite WASM): a stale
If-Match, If-None-Match: * over an existing row, ?mode=draft, and the
unguarded control. Each reads the store after the request.

Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 8, 2026

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 e36ee5351b998ec74eab6d2c27d34e4af09f466a → packageMentionDocs.

@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 8, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 23:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 23:01
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 6a53564 Oct 8, 2026
39 of 40 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22221-hono-meta-write-conformance branch October 8, 2026 23:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

2 participants