fix(slice-05): v2 inspect provenance test + camelCase contract consistency (guard B1 repair on 6b81d3b6) (OPR.0.3.2.5)

Repairs guard BLOCKING-CONCERN B1 on qitem-20260518194024-2fbe1454 /
6b81d3b6: the v2 inspect provenance projection line at
routes/bundles.ts was claimed but not regression-covered — the
existing v2 inspect test sent no provenance in its create request,
so removing the projection still passed 16/16. Discriminator
verified as dead w.r.t. tests.

Also resolves the API-contract inconsistency guard flagged: v1
inspect surfaces camelCase provenance via the normalizer (from
82d39715), but v2 inspect was returning raw snake_case. One
/api/bundles/inspect endpoint should return one shape regardless of
schema version.

Fixes:
- Export normalizeProvenanceBlock from domain/bundle-types.ts so it
  can be shared between the v1 normalizer pipeline and the v2 inspect
  projection.
- Route v2 inspect projection now wraps rawParsed.provenance through
  normalizeProvenanceBlock, returning normalized camelCase (matches
  v1 contract). Field stays optional; undefined when absent.
- New regression test in test/bundle-routes.test.ts: pod-aware (v2)
  create with provenance -> inspect, asserts response.manifest.provenance
  contains camelCase keys (sourceHost/authorSession/cliVersion/notes/
  daemonVersion) and explicitly asserts snake_case keys are NOT present
  (negative discriminator on the contract).

Verified:
- 81/81 daemon bundle suite green post-fix (was 80; +1 new v2 round-trip)
- Discriminator confirmed: removing the v2 projection line makes the
  new test fail (1/17 fail in bundle-routes); restoring it returns
  17/17 PASS
- 12/12 CLI bundle suite unchanged (no CLI files touched in this fix)
- scope-and-versioning sections 1 and 3 coherence re-audit clean
- audit-every-layer unchanged: helpers still function-level reads only

Diff: 3 files, +86 / -6 lines. No CLI files modified.

Branch chain since main: 82d39715 (CLEAR) -> 3b7432e4 (CLEAR) ->
6b81d3b6 (BLOCKING-CONCERN B1; this commit repairs) -> new tip.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-18 12:46:07 -07:00
1 parent 6b81d3b6d5
commit 77bed48ba6
3 files changed
+86 -6

No files matched your search

+8 -2
View File
@@ -67,8 +67,14 @@ function provenanceToYamlRecord(p: BundleProvenance): Record<string, unknown> {
return out;
}
/** Normalize raw provenance to typed BundleProvenance. Returns undefined when absent or empty. */
function normalizeProvenanceBlock(raw: unknown): BundleProvenance | undefined {
/**
* Normalize raw snake_case provenance (as parsed from YAML or received from
* a request body) to typed camelCase BundleProvenance. Returns undefined when
* absent or empty. Exported so both the v1 normalizer pipeline and the v2
* inspect-route projection produce identical camelCase shapes — the
* /api/bundles/inspect contract is one shape regardless of schema version.
*/
export function normalizeProvenanceBlock(raw: unknown): BundleProvenance | undefined {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
const p = raw as Record<string, unknown>;
const result: BundleProvenance = {};
+6 -4
View File
@@ -14,7 +14,7 @@ import { LegacyRigSpecCodec } from "../domain/rigspec-codec.js";
import { LegacyRigSpecSchema } from "../domain/rigspec-schema.js";
import { RigSpecCodec } from "../domain/rigspec-codec.js";
import { RigSpecSchema } from "../domain/rigspec-schema.js";
import { parseLegacyBundleManifest as parseBundleManifest, normalizeLegacyBundleManifest as normalizeBundleManifest, serializePodBundleManifest, parsePodBundleManifest, validatePodBundleManifest } from "../domain/bundle-types.js";
import { parseLegacyBundleManifest as parseBundleManifest, normalizeLegacyBundleManifest as normalizeBundleManifest, serializePodBundleManifest, parsePodBundleManifest, validatePodBundleManifest, normalizeProvenanceBlock } from "../domain/bundle-types.js";
import type { PodBundleManifest, BundleProvenance } from "../domain/bundle-types.js";
import { fileURLToPath } from "node:url";
@@ -286,9 +286,11 @@ bundleRoutes.post("/inspect", async (c) => {
algorithm: integritySection.algorithm ?? "sha256",
files: integritySection.files ?? {},
} : undefined,
// Item 1 / slice-05: surface provenance raw from manifest so JSON-path
// consumers (UI inspector, jq pipelines) see it. Field is optional.
provenance: rawParsed["provenance"] as Record<string, unknown> | undefined,
// Item 1 / slice-05: surface provenance in normalized camelCase so the
// /api/bundles/inspect contract is one shape regardless of v1 vs v2
// (v1 path normalizes through normalizeLegacyBundleManifest below).
// Field is optional; undefined when bundle has no provenance.
provenance: normalizeProvenanceBlock(rawParsed["provenance"]),
};
const integrityCompat = integritySection ? {
schemaVersion: 2,
@@ -487,6 +487,78 @@ describe("Bundle API routes", () => {
expect(body.digestValid).toBe(true);
});
// Item 1 / slice-05 / guard B1 repair: pod-aware (v2) create -> inspect provenance round-trip.
// Asserts the inspect response surfaces provenance in normalized camelCase,
// matching the v1 contract. Discriminator: removing the v2 inspect projection
// line in routes/bundles.ts must make this test fail.
it("POST /api/bundles/inspect with v2 bundle surfaces provenance in camelCase (create -> inspect round-trip)", async () => {
const agentsDir = path.join(tmpDir, "agents", "impl");
fs.mkdirSync(agentsDir, { recursive: true });
fs.writeFileSync(path.join(agentsDir, "agent.yaml"), [
'name: impl-agent',
'version: "1.0.0"',
'resources:',
' skills: []',
'profiles:',
' default:',
' uses:',
' skills: []',
].join("\n"));
const specPath = path.join(tmpDir, "rig.yaml");
fs.writeFileSync(specPath, [
'version: "0.2"',
'name: v2-prov-test',
'pods:',
' - id: dev',
' label: Dev',
' members:',
' - id: impl',
' agent_ref: "local:agents/impl"',
' profile: default',
' runtime: claude-code',
' cwd: .',
' edges: []',
'edges: []',
].join("\n"));
const bundlePath = path.join(tmpDir, "v2-prov.rigbundle");
const createRes = await app.request("/api/bundles/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
specPath, bundleName: "v2-prov", bundleVersion: "0.1.0", outputPath: bundlePath,
provenance: {
sourceHost: "v2-route-test-host",
authorSession: "velocity-driver@openrig-velocity",
cliVersion: "0.3.2",
notes: "v2 route round-trip fixture",
},
}),
});
expect(createRes.status).toBe(201);
const inspectRes = await app.request("/api/bundles/inspect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ bundlePath }),
});
expect(inspectRes.status).toBe(200);
const inspectBody = await inspectRes.json();
expect(inspectBody.manifest.schemaVersion).toBe(2);
// The contract: provenance returned in normalized camelCase (matches v1)
expect(inspectBody.manifest.provenance).toBeDefined();
expect(inspectBody.manifest.provenance.sourceHost).toBe("v2-route-test-host");
expect(inspectBody.manifest.provenance.authorSession).toBe("velocity-driver@openrig-velocity");
expect(inspectBody.manifest.provenance.cliVersion).toBe("0.3.2");
expect(inspectBody.manifest.provenance.notes).toBe("v2 route round-trip fixture");
expect(typeof inspectBody.manifest.provenance.daemonVersion).toBe("string");
expect(inspectBody.manifest.provenance.daemonVersion.length).toBeGreaterThan(0);
// Negative — snake_case keys must NOT be present (camelCase contract)
expect(inspectBody.manifest.provenance.source_host).toBeUndefined();
expect(inspectBody.manifest.provenance.author_session).toBeUndefined();
});
// T11: Install concurrency lock
it("concurrent bundle install returns 409", async () => {
// Acquire lock manually