test(contract): AST-level poll-uploads-resolved invariant (RFC#640 Layer D) #27
Reference in New Issue
Block a user
Delete Branch "feat/poll-uploads-resolved-contract-test-layer-d"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
RFC#640 4-layer cascade — Layer D (AST-level contract test). Single new test file at
src/__tests__/poll-uploads-resolved-contract.test.ts. Catches the third failure surface that Layers A + B close from the spec / implementation side: an adapter that has a poll loop but forgot to wire in the upload-resolution helpers.Companion to:
CTO chat GO: "4-layer, dispatch team and follow SOP" 2026-05-22T01:31:48Z.
Test design: producer-side no-op gate + consumer-side enforcement
The test runs in TWO modes based on
MCP_SERVER_CONTRACT_CONSUMERSenv var:Producer-side (this repo's own CI): env var unset → empty consumer list → test passes trivially. No external consumers needed for the producer's CI to be green.
Consumer-side (channel adapter / telegram / codex / etc.): each consumer repo's CI runs the test against its own source files via:
The contract is engaged in consumer repos via the env-driven list. Same shape as the runtime-pin-check sibling pattern CEO-A flagged.
The invariant
For each TS file in the consumer list:
ts.createSourceFile+ts.forEachChild)./activityURL literal in any string or template literal (head + after-substitution spans). Pattern is/workspaces/<x>/activitywith a non-word boundary after to avoid the false-friend/activities.@molecule-ai/mcp-serverOR@molecule-ai/mcp-server/inbox-uploadsofresolvePendingUpload/URICache/rewritePendingURIs.(NOT pollsActivity) OR importsResolutionHelper OR hasOptOut.The three valid states:
/activity→ invariant trivially holds.Anything else → fail-CI with a structured message showing the path + each detected signal + the missing piece, so a reviewer immediately sees what's wrong without re-running locally.
Magic-comment opt-out
Intentional bypass (e.g. a logging-only inspector that polls /activity but never surfaces files to an agent) opts out by adding the comment anywhere in the file:
The
<reason>part is informational only — the test asserts the presence of the magic-comment prefix but doesn't parse the reason. A reviewer sees the comment + reason in code review. This keeps the bypass auditable without overengineering the parser.Test envelope (10 cases)
Producer-side mode (1):
no consumers declared (producer-side CI no-op)— passes when env var unset.Checker self-tests via tmpdir fixtures (9):
@molecule-ai/mcp-server/inbox-uploadscounts (Layer B exports both via root and via subpath)/workspaces/x/activitiesNOT matched (boundary check)/activityin head detected/activityAFTER substitution span detected (catches`/workspaces/${ws}/activity?since_id=${cursor}`)ImportClauseNOT counted as named import (sanity —import mcpserver from "@molecule-ai/mcp-server"doesn't pull resolvePendingUpload into scope)Verified locally on operator
The failure message format:
Reviewer reading the CI failure immediately sees what's missing without re-running.
Why AST, not regex
A regex on the file text would catch the URL string easily but FAIL on import-detection edge cases:
import { ..., resolvePendingUpload, ... }with line wrappingimport * as mcp from "..."(namespace import — does NOT pull individual names into scope, so amcp.resolvePendingUploadreference inside the file body would still not "import" the name in the spec sense)const { resolvePendingUpload } = require("@molecule-ai/mcp-server")(CJS destructuring — TS files can use it via ts-node etc.)The AST-level check handles named-import statements correctly + ignores destructuring / namespace forms (intentional: only the explicit named-import is the contract signal).
Sibling reference
The env-var-driven consumer-paths design comes from CEO-A's dispatch brief citing
template-claude-code/.gitea/workflows/runtime-pin-check.yml. That repo isn't available on Gitea today (cloning returns 404), so the design was reconstructed from her brief + adapted to the AST-parsing surface. If the runtime-pin-check pattern is the canonical sibling, a follow-up could harmonize the two implementations (env var name + invocation path).Cascade context
A + B + D can land in any order — they're additive at the source-code level. C waits on B's npm publish.
Test plan
tsc --noEmitclean (verified on operator).npx jest poll-uploads-resolved-contract→ 10/10 passed without env var.🤖 Generated with Claude Code
Layer D of the 4-layer cascade — fail-CI on any TS adapter that polls /workspaces/<ws>/activity but does NOT also import the upload-resolution helpers from @molecule-ai/mcp-server. Catches the third failure surface (adapter forgot to wire in the resolution flow) that Layer A's MANDATORY spec + Layer B's TS implementation close from the spec/implementation side. Test design: producer-side CI no-op gate + consumer-side enforcement. # In a consumer repo's CI (channel adapter / telegram / codex / etc.): MCP_SERVER_CONTRACT_CONSUMERS=src/server.ts:src/poll.ts \ npx jest --testPathPatterns=poll-uploads-resolved-contract \ --rootDir=node_modules/@molecule-ai/mcp-server When MCP_SERVER_CONTRACT_CONSUMERS is unset (this repo's own CI), the test runs against an empty list and passes trivially. The contract is engaged in consumer repos via the env-driven list. Invariant per file: 1. Parse with TS compiler API (ts.createSourceFile + ts.forEachChild). 2. Detect /activity URL literal in any string or template literal (head + after-substitution spans). Pattern is /workspaces/<x>/activity with a non-word boundary after to avoid the false-friend /activities. 3. Detect named imports from @molecule-ai/mcp-server OR @molecule-ai/mcp-server/inbox-uploads of resolvePendingUpload / URICache / rewritePendingURIs. 4. Assert: (NOT pollsActivity) OR importsResolutionHelper OR hasOptOut. Magic-comment opt-out for intentional bypass: // @no-resolve-uploads-justification: <reason> Reason text is informational only (asserted in code review). The test just looks for the prefix anywhere in the file. Test envelope (10 cases): - producer-side no-op when MCP_SERVER_CONTRACT_CONSUMERS unset (1) - self-tests via tmpdir fixtures (9): - polls + imports → passes - polls + no-imports → caught - polls + opt-out → not caught - no-poll → trivially holds - subpath import @molecule-ai/mcp-server/inbox-uploads counts - false-friend /workspaces/x/activities NOT matched - template literal /activity in head detected - template literal /activity after substitution span detected - default ImportClause NOT counted as named import Verified on operator: tsc --noEmit : clean producer-side jest : 10/10 passed (no-op + 9 self-tests) consumer gate green : passing fixture → 10/10 passed consumer gate red : failing fixture → 1 expected failure with clear msg Sibling design reference: the runtime-pin-check pattern (CEO-A flagged template-claude-code's runtime-pin-check.yml; that repo wasn't available on Gitea so the env-var-driven consumer-paths shape was reconstructed from her dispatch brief). Origin: RFC#640 4-layer cascade Layer D. CTO chat GO via "4-layer, dispatch team and follow SOP" 2026-05-22T01:31:48Z. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Independent non-author review (reviewer = core-devops of engineers team, author = hongming-pc2).
Verified against head
8441900. mergeable=True. Read full +384/-0 diff (single new test file). Focus on the producer/consumer CI semantics + the env-var-driven design + the failure diagnostic shape.Pre-flight CI gate —
CI / testsuccess on8441900. Hygiene gate cleared.Producer-side vs consumer-side CI semantics — clean two-mode design.
The design splits cleanly:
Producer-side (this repo's own CI):
MCP_SERVER_CONTRACT_CONSUMERSis unset → the test consumers list is empty → the test runsit("no consumers declared (producer-side CI no-op)", ...)which trivially passes. No external consumers needed for the producer's CI to be green.Consumer-side (channel adapter / telegram / codex / etc.): the consumer repo's CI runs
MCP_SERVER_CONTRACT_CONSUMERS=src/server.ts:src/poll.ts npx jest --testPathPatterns=poll-uploads-resolved-contract --rootDir=node_modules/@molecule-ai/mcp-server. The contract is engaged at the consumer-side CI step.This is the right shape for a CONTRACT test — the producer can't enumerate all consumer files (they live in different repos), but the producer ships the test code that consumer repos can opt into. Same model as the runtime-pin-check sibling pattern CEO-A flagged (which I couldn't independently inspect because
template-claude-code404s on Gitea — author noted this in the PR body).AST-level via TS compiler API — right choice, not regex.
The implementation uses
ts.createSourceFile+ts.forEachChildfor parsing. Three edge cases this catches that regex would miss:Wrapped named imports:
Regex would need careful multi-line handling; AST naturally parses this.
Default vs named imports:
The test correctly counts only NAMED imports (
ts.NamedImports), not default. A regex grep forresolvePendingUploadwould silently false-positive on a default-import + latermcpserver.resolvePendingUpload(...)reference (which IS valid TS but doesn't satisfy the "explicit import" contract intent — the contract wants the name in the module's import list so a static analysis tool sees the dependency).Template literal substitution spans:
The
/activityliteral sits in the SECOND template fragment AFTER the${ws}substitution. The test'sts.TemplateExpressionhandler concatenates head + each post-substitution fragment and re-runs the pattern. Regex on the raw source might catch this via greedy matching, but AST gives semantic certainty.URL pattern boundary check — false-friend protection.
The pattern
\/workspaces\/[^/]*\/activity(?:\?|$|[^a-zA-Z0-9_/-])requires a non-word boundary afteractivity. This catches:/workspaces/x/activity✓/workspaces/x/activity?since_id=1✓/workspaces/x/activity/foo✓ (the/boundary)/workspaces/x/activities✗ (thesis a word char — correctly rejected)The boundary test
URL pattern: rejects /workspaces/X/activities (false-friend)pins this explicitly. A future refactor that simplified the boundary to just/activitywould catch the false-friend and fail this test.Magic-comment opt-out — auditable escape hatch.
The
// @no-resolve-uploads-justification: <reason>pattern is:grep @no-resolve-uploads-justificationacross consumer repos)The OPT_OUT_COMMENT regex
/\/\/\s*@no-resolve-uploads-justification:/is permissive on whitespace; allows both// @no-resolve-...and//@no-resolve-...styles. Reasonable.Failure-diagnostic shape — actionable.
A CI reviewer sees this and immediately knows: (a) which file failed, (b) which signal triggered the contract requirement, (c) what's missing (the import or the opt-out). No need to re-run locally to diagnose.
The Jest assertion shape
expect({ok: status, info: reasonLines.join("\n ")}).toEqual({ok: true, info: ...})is unusual but intentional — by including the info string in BOTH sides of the comparison, Jest's diff output shows the structured info verbatim. A more standardexpect(status).toBe(true)would show justexpected true, got falsewith no context.CI invocation shape for consumer repos — documented in the file header.
This is the exact invocation a consumer repo adds to its CI step. The
--rootDirflag points Jest at the installed package, so the test file is found atnode_modules/@molecule-ai/mcp-server/dist/__tests__/...(after Layer B's npm publish). One CI line per consumer repo; minimal integration burden.Self-tests use tmpdir fixtures — clean test isolation.
Each self-test creates a unique tmpdir via
fs.mkdtempSync, writes a fixture file, runscheckConsumerFileagainst it, asserts result.afterEachcleans up. No fixtures committed to the repo — tests are self-contained.Test execution confirmed locally:
Three modes pinned end-to-end.
One non-blocking observation (logged-but-NOT-blocking-merge):
The test ships in the producer repo's own test suite (
src/__tests__/). When Layer B's npm publish happens, the test file lands atdist/__tests__/poll-uploads-resolved-contract.test.tsin the published package. The README / publish-step doesn't currently exclude__tests__/from the publish — confirmed by checkingpackage.jsonfilesfield is absent (defaults to including everything indist/). This is INTENTIONAL for the contract-test pattern (consumers need to be able to invoke the test from their CI), but worth flagging so future package-size optimizations don't accidentally exclude__tests__/. Cheap follow-up; could be apackage.jsonfilesfield that explicitly includesdist/__tests__/poll-uploads-resolved-contract.test.ts. Not blocking.No regression risks. No REQUEST_CHANGES. LGTM, approving.
Independent non-author review (reviewer = core-qa of engineers team, author = hongming-pc2).
Verified against head
8441900. mergeable=True. Read full +384/-0 diff. Focus on test-envelope completeness for the contract checker logic + the per-mode coverage.Pre-flight CI gate —
CI / testsuccess on8441900. Hygiene gate cleared.Test envelope structure — 10 cases across 2 surfaces (producer-side / self-tests).
The test file has TWO
describeblocks:"RFC#640 Layer D — poll-uploads-resolved contract"— the actual contract gate. Producer-side mode (when env var unset) runs ONE test (the no-op). Consumer-side mode (env var set) runs ONE test per consumer file."RFC#640 Layer D — checker self-tests"— 9 self-tests against tmpdir fixtures exercising thecheckConsumerFileanalysis function directly.The split is correct: outer is the GATE (engaged via env var), inner is the CHECKER (exercised regardless of env). Two surfaces with different jobs in the same file.
Self-tests cover the checker's full decision-space.
I traced each self-test's catch-target:
tools/workspaces.tsthat doesn't poll /activity at all) doesn't need to import resolution helpers. Without this test, the checker might over-zealously fail tool files.import { URICache } from "@molecule-ai/mcp-server/inbox-uploads"(the subpath form Layer B exports). Both root + subpath are valid signals.apiCall("GET", "/workspaces/x/activities")would falsely trigger the contract requirement on a hypothetical future endpoint namedactivities.`/workspaces/${ws}/activity`case.`/workspaces/${ws}/activity?since_id=${cursor}`— the multi-segment template literal case. The/activityliteral sits in the second fragment after${ws}; without the multi-fragment walk, this would slip through.import mcpserver from "@molecule-ai/mcp-server"(default import) does NOT pull individual names into scope. Even thoughmcpserver.resolvePendingUploadis a valid runtime reference, the contract intent is "explicit named import in the file's import list" — so default forms should NOT satisfy the contract. Without this test, the checker might silently accept default imports.Producer-side no-op test — meaningful assertion.
This asserts that the producer-side run has an EMPTY consumer list. If someone accidentally hard-coded a consumer path into the test file (e.g. for local development), the producer's own CI would catch it via this test failure. Defensive assertion, not just a passes-trivially placeholder.
Test isolation via tmpdir — clean.
beforeEachcreates a unique tmpdir viafs.mkdtempSync(path.join(os.tmpdir(), "layer-d-self-")).afterEachremoves it withfs.rmSync(..., {recursive:true, force:true}). Each test writes its fixture into the tmpdir + runs the checker. No fixture files committed to the repo + no cross-test interference.Failure-message format — actionable for CI reviewer.
The assertion shape:
Including the info string in BOTH sides of the comparison is a clever trick: when Jest renders the diff, the info text appears in the expected/received frames, so the reviewer sees the structured diagnostic (path / polls / imports / opt-out) verbatim. A simple
expect(status).toBe(true)would render asExpected true, Received falsewith no context.Coverage gap check — what's NOT tested but matters?
The test covers the checker's import + URL detection but doesn't test consumer-side mode with REAL multi-file iteration (e.g.
MCP_SERVER_CONTRACT_CONSUMERS=a:b:c). The describe-block construction creates one nested describe per consumer; the test logic is identical per-file. Adding a multi-file iteration test would be a small +20 LOC addition. Not blocking — the per-file logic IS the per-file invariant; testing it once with one file covers the core path.The test doesn't pin behavior on a SYNTACTICALLY INVALID TS file (e.g.
import {with an unclosed brace).ts.createSourceFilehandles this gracefully (returns a partial AST with diagnostics); the visit walk continues on what it can parse. A defensive assertion that the test PASSES (doesn't throw) on a broken consumer file would harden the failure-mode docs. Cheap follow-up; not blocking.Test execution confirmed locally:
All three modes work end-to-end.
Consistency with Layer A's structural tests.
This test pins the IMPLEMENTATION side; Layer A's
test_upload_resolution_contract.pypins the SPEC side; together they catch drift from either direction. Plus this PR's checker IS the CI gate that fires on consumer-side drift — three-tier enforcement (spec text + implementation surface + consumer adoption).One non-blocking observation (logged-but-NOT-blocking-merge):
The 9 self-tests are nested inside a single
describe("RFC#640 Layer D — checker self-tests", ...). With the producer-side mode test as a sibling, the test runner reports 1 + 9 = 10 cases. If Jest's--testNamePatternever needed to skip just the self-tests (e.g. during a fast CI tier), the pattern would need to match the full nested describe path. Not an issue today; could be addressed by splitting into two test files if test-filtering granularity becomes important. Not blocking.No regression risks. No REQUEST_CHANGES. LGTM, approving.