Skip to content

Commit 34ab961

Browse files
dmealingclaude
andcommitted
test(cli)+docs(tph): #208 verify --db @Unmanaged gate + TPH lane-coverage note
Two small coverage/documentation gaps: - cli verify-db-drift: add a test that `verify --db` treats an @Unmanaged DB object (Flyway / a hand-migration owns its DDL) as EXTERNAL — an out-of-band legacy_accounts table present in the DB is excluded from the drift comparison (computeDrift threads it out via collectUnmanagedNames) → exit 0 + the "external (declared @Unmanaged …)" annotation. Without the exclusion it would surface as a spurious drop-table drift. The behavior existed (#208) but was untested at the verify --db surface. - api-contract tph/README: document that TPH runs BOTH lanes on TS + C# (full-stack) but the GENERATED lane only on the seam ports (Java/Kotlin/Python) as ACCEPTED design — a hand-rolled reference TPH server for a controller-only seam port would merely re-implement the generated routing, adding no independent contract signal. Prevents it being mistaken for a coverage gap. Test/docs-only; no library behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NGQ7oSuNcjhsMHWwZzhBwr
1 parent e005d27 commit 34ab961

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

fixtures/api-contract-conformance/tph/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,26 @@ The runtime persistence half (single-table insert/find/update semantics, decimal
8888
normalization, no-cross-subtype-update) is pinned by the `persistence-conformance`
8989
`tph-*` query scenarios. A port can land routes-only or runtime-only first, so the
9090
two corpora gate independently.
91+
92+
## Lane coverage — TPH is generated-lane-only on the seam ports (ACCEPTED design)
93+
94+
The base / m2m / jsonb api-contract corpora run **both** lanes (hand-rolled
95+
reference server + generated artifact) on all five ports. **TPH deliberately does
96+
NOT**: it runs both lanes on **TS and C#** (`api-contract-tph.test.ts`,
97+
`TphReferenceServer.cs`) — the full-stack ports whose generated output includes
98+
persistence, so a hand-rolled reference server is a genuinely independent
99+
implementation worth cross-checking — but the **generated lane ONLY** on the seam
100+
ports **Java / Kotlin / Python** (`TphGeneratedApiContractConformanceTest` /
101+
`test_api_contract_tph_generated.py`; no hand-rolled reference TPH server).
102+
103+
This asymmetry is intentional, not a coverage gap. Those three ports generate only
104+
the **controller/router** behind a consumer-supplied persistence seam, so the
105+
generated lane already boots the EMITTED polymorphic controller over HTTP — the
106+
artifact under test. A hand-rolled reference TPH server for a seam port would
107+
merely re-implement the discriminator routing + per-subtype filtering the
108+
generated controller already provides, yielding no independent contract signal
109+
beyond what the generated lane verifies. (For the base/m2m/jsonb corpora the seam
110+
ports DO keep a reference lane because its single-entity / traversal semantics are
111+
distinct enough from the generated code to be worth pinning independently.) The
112+
polymorphic-CRUD contract itself is still gated byte-identically across all five
113+
ports; only the redundant second lane is dropped where it would add nothing.

server/typescript/packages/cli/test/integration/verify-db-drift.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,4 +129,52 @@ describe("meta verify --db — schema-drift gate", () => {
129129
rmSync(repo, { recursive: true, force: true });
130130
}
131131
});
132+
133+
// #208 — an @unmanaged DB object (Flyway / a hand-migration owns its DDL) must be
134+
// INVISIBLE to `verify --db`: its out-of-band presence in the DB is NOT drift (it is
135+
// threaded out of computeDrift via collectUnmanagedNames), and verify annotates it as
136+
// external. Without the exclusion the extra table surfaces as a spurious drop-table
137+
// drift → exit 1. Uses an @unmanaged TABLE (the Flyway-owned-entity case).
138+
test("@unmanaged DB object present out-of-band → NOT drift; exit 0 + annotated external", async () => {
139+
const repo = mkdtempSync(join(tmpdir(), "metaobjects-verify-db-unmanaged-"));
140+
const dbUrl = `file:${join(repo, "local.db")}`;
141+
// Widget is managed; Legacy is an @unmanaged table (Flyway / a hand-migration owns it).
142+
const meta = JSON.stringify({
143+
"metadata.root": {
144+
package: "acme::drift",
145+
children: [
146+
{ "object.entity": { name: "Widget", children: [
147+
{ "field.long": { name: "id" } },
148+
{ "field.string": { name: "name", "@column": "name" } },
149+
{ "identity.primary": { name: "pk", "@fields": ["id"] } },
150+
] } },
151+
{ "object.entity": { name: "Legacy", children: [
152+
{ "source.rdb": { "@table": "legacy_accounts", "@unmanaged": true } },
153+
{ "field.long": { name: "id" } },
154+
{ "identity.primary": { name: "pk", "@fields": ["id"] } },
155+
] } },
156+
],
157+
},
158+
});
159+
try {
160+
mkdirSync(join(repo, "metaobjects"), { recursive: true });
161+
writeFileSync(join(repo, "metaobjects", "meta.drift.json"), meta, "utf8");
162+
// Materialize: creates `widgets`, and NEVER the @unmanaged legacy_accounts.
163+
await materialize(repo, dbUrl);
164+
// Flyway creates the table out-of-band — it now EXISTS in the DB but is unmodeled-as-managed.
165+
const client = createClient({ url: dbUrl });
166+
await client.execute(`CREATE TABLE legacy_accounts ("id" integer PRIMARY KEY)`);
167+
client.close();
168+
169+
const exit = await run(["verify", "--cwd", repo, "--db", dbUrl, "--dialect", "sqlite"]);
170+
// The @unmanaged table is excluded from the drift comparison → in sync → exit 0.
171+
expect(exit).toBe(0);
172+
// ...and it is annotated as external rather than vanishing silently.
173+
const all = [...out, ...err].join("\n");
174+
expect(all).toContain("legacy_accounts");
175+
expect(all).toContain("external");
176+
} finally {
177+
rmSync(repo, { recursive: true, force: true });
178+
}
179+
});
132180
});

0 commit comments

Comments
 (0)