feat(slice-05): compatibility assembler capture + route + CLI plumbing (Item 2 part 2) (OPR.0.3.2.5)

Mirrors the Item 1 part 2 + 3 pattern (3b7432e4 + 6b81d3b6) condensed
into one commit for compatibility. Builds on 1fd11b76 (type layer):
assemblers populate manifest.compatibility from opts; route /create
accepts compatibility from request body; route /inspect surfaces it in
camelCase via the exported normalizer (both v1 path through the
normalizer and v2 path through explicit projection — avoiding the B1
trap from Item 1 by including a v2 round-trip test in the SAME commit
as the v2 projection); CLI bundle create exposes --min-daemon-version
and --min-cli-version flags.

Install-time version check + --skip-version-check operator override
land at Checkpoint 3.3 (closes Item 2).

Files touched:
- packages/daemon/src/domain/bundle-assembler.ts (v1): AssembleOptions
  extended with optional compatibility; assemble() populates
  manifest.compatibility when provided
- packages/daemon/src/domain/pod-bundle-assembler.ts (v2): same shape
- packages/daemon/src/routes/bundles.ts: new
  compatibilityFromRequestBody helper (sanitizes request body); /create
  passes compatibility through to both assemblers; /inspect v2
  projection now includes compatibility via normalizeCompatibilityBlock
  (camelCase contract consistent with v1 via the normalizer)
- packages/cli/src/commands/bundle.ts: --min-daemon-version and
  --min-cli-version flags on create; compatibility block built and
  included in request body only when at least one flag is set
- packages/daemon/test/bundle-assembler.test.ts: 2 v1 capture tests
- packages/daemon/test/pod-bundle-assembler.test.ts: 2 v2 capture tests
- packages/daemon/test/bundle-routes.test.ts: 2 round-trip tests
  (v1 create -> inspect camelCase + negative snake_case assertion;
  v2 create -> inspect camelCase + negative snake_case assertion)
- packages/cli/test/bundle.test.ts: 3 CLI tests (both flags wired;
  partial only --min-daemon-version; neither flag omits the field
  entirely from request body)

Diff: 8 files, +302 / -8 lines.

Verified:
- 99/99 daemon bundle suite green (was 93; +6 new)
- 15/15 CLI bundle suite green (was 12; +3 new)
- 114/114 total bundle tests across both packages
- scope-and-versioning sections 1 and 3 coherence re-audit clean
- audit-every-layer: zero new module-level constants read env/host/version

PRD bounce conditions verified:
- No schema bump (consumes optional manifest field added in 1fd11b76)
- No new SQLite migrations
- No event types
- No core-routing or bootstrap mutation
- No sidebar entries touched

Watched risks applied:
- Audit-every-layer: assembler reads compatibility from opts only
  (no env reads in either assembler). Route helper
  compatibilityFromRequestBody reads from typed request body only.
  CLI builds compatibility from explicit operator flags only. Zero
  module-level constants reading env, host, or version sources
  introduced in this commit (verified via grep — empty).
- Optional-field backward-compat both directions: missing
  opts.compatibility produces manifest with no compatibility section
  (older daemons consuming new manifests ignore unknown fields; newer
  daemons consuming older manifests treat missing as no-op for the
  install-time check at 3.3).
- B1 trap avoided: v2 route inspect projection ships TOGETHER with
  the v2 round-trip test in this same commit. Discriminator: removing
  either the v2 projection line in routes/bundles.ts or the v1 path's
  normalizer wiring (from 1fd11b76) makes the matching round-trip
  test fail.

Chain on this branch:
- 1fd11b76 (Item 2 type layer, CLEAR) <- prior tip
- THIS COMMIT — Item 2 part 2: assembler + route + CLI plumbing
- Next (3.3) — install-time version check + --skip-version-check

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:59:19 -07:00
1 parent 1fd11b762b
commit f10a19901a
8 files changed
+302 -8

No files matched your search

+11 -1
View File
@@ -63,17 +63,27 @@ export function bundleCommand(depsOverride?: StatusDeps): Command {
.option("--include-packages <refs...>", "Package refs to include (default: all from spec)")
.option("--rig-root <root>", "Root directory for pod-aware resolution")
.option("--notes <text>", "Operator notes captured in bundle provenance metadata")
.option("--min-daemon-version <ver>", "Minimum daemon version required to install this bundle (Item 2 compatibility)")
.option("--min-cli-version <ver>", "Minimum CLI version required to install this bundle (Item 2 compatibility)")
.option("--json", "JSON output")
.action(async (spec: string, opts: { output: string; name: string; bundleVersion: string; includePackages?: string[]; rigRoot?: string; notes?: string; json?: boolean }) => {
.action(async (spec: string, opts: { output: string; name: string; bundleVersion: string; includePackages?: string[]; rigRoot?: string; notes?: string; minDaemonVersion?: string; minCliVersion?: string; json?: boolean }) => {
const deps = getDepsF();
const client = await getClient(deps);
if (!client) { process.exitCode = 1; return; }
// Item 2 / slice-05: build compatibility from operator flags. Only included
// in the request body when at least one of the two flags is set.
const compatibility: Record<string, string> = {};
if (opts.minDaemonVersion) compatibility.minDaemonVersion = opts.minDaemonVersion;
if (opts.minCliVersion) compatibility.minCliVersion = opts.minCliVersion;
const hasCompatibility = Object.keys(compatibility).length > 0;
const res = await client.post<Record<string, unknown>>("/api/bundles/create", {
specPath: spec, bundleName: opts.name, bundleVersion: opts.bundleVersion, outputPath: opts.output,
includePackages: opts.includePackages,
rigRoot: opts.rigRoot ? nodePath.resolve(opts.rigRoot) : undefined,
provenance: buildClientProvenance(opts.notes),
...(hasCompatibility ? { compatibility } : {}),
});
if (opts.json) {
+51
View File
@@ -251,6 +251,57 @@ describe("Bundle CLI", () => {
expect(provenance!["notes"]).toBeUndefined();
});
// Item 2 / slice-05: --min-daemon-version + --min-cli-version flags wire into request body compatibility
it("bundle create --min-daemon-version and --min-cli-version wire into request body compatibility", async () => {
capturedCreateBodies = [];
await captureLogs(async () => {
await makeCmd().parseAsync([
"node", "rig", "bundle", "create", "/tmp/rig.yaml",
"-o", "/tmp/test.rigbundle",
"--min-daemon-version", "0.3.2",
"--min-cli-version", "0.3.2",
]);
});
const createBody = capturedCreateBodies[capturedCreateBodies.length - 1];
expect(createBody).toBeTruthy();
const compatibility = createBody!["compatibility"] as Record<string, unknown> | undefined;
expect(compatibility).toBeTruthy();
expect(compatibility!["minDaemonVersion"]).toBe("0.3.2");
expect(compatibility!["minCliVersion"]).toBe("0.3.2");
});
// Item 2 / slice-05: --min-daemon-version alone (partial) still wires
it("bundle create --min-daemon-version alone wires partial compatibility into request body", async () => {
capturedCreateBodies = [];
await captureLogs(async () => {
await makeCmd().parseAsync([
"node", "rig", "bundle", "create", "/tmp/rig.yaml",
"-o", "/tmp/test.rigbundle",
"--min-daemon-version", "0.3.2",
]);
});
const createBody = capturedCreateBodies[capturedCreateBodies.length - 1];
expect(createBody).toBeTruthy();
const compatibility = createBody!["compatibility"] as Record<string, unknown> | undefined;
expect(compatibility).toBeTruthy();
expect(compatibility!["minDaemonVersion"]).toBe("0.3.2");
expect(compatibility!["minCliVersion"]).toBeUndefined();
});
// Item 2 / slice-05: no flags → compatibility omitted (no empty object sent)
it("bundle create with neither min-version flag omits compatibility from request body", async () => {
capturedCreateBodies = [];
await captureLogs(async () => {
await makeCmd().parseAsync([
"node", "rig", "bundle", "create", "/tmp/rig.yaml",
"-o", "/tmp/test.rigbundle",
]);
});
const createBody = capturedCreateBodies[capturedCreateBodies.length - 1];
expect(createBody).toBeTruthy();
expect(createBody!["compatibility"]).toBeUndefined();
});
// Item 1 / slice-05: authorSession populates when OPENRIG_SESSION_NAME env is set
it("bundle create includes authorSession when OPENRIG_SESSION_NAME env is set", async () => {
capturedCreateBodies = [];
+12 -1
View File
@@ -2,7 +2,7 @@ import nodePath from "node:path";
import { LegacyRigSpecCodec as RigSpecCodec } from "./rigspec-codec.js"; // TODO: AS-T08b — migrate to pod-aware RigSpec
import { LegacyRigSpecSchema as RigSpecSchema } from "./rigspec-schema.js"; // TODO: AS-T08b — migrate to pod-aware RigSpec
// TODO: AS-T12 — migrate to pod-aware bundle types
import { serializeLegacyBundleManifest as serializeBundleManifest, type LegacyBundleManifest as BundleManifest, type BundleProvenance } from "./bundle-types.js";
import { serializeLegacyBundleManifest as serializeBundleManifest, type LegacyBundleManifest as BundleManifest, type BundleProvenance, type BundleCompatibility } from "./bundle-types.js";
export interface AssemblerFsOps {
readFile: (path: string) => string;
@@ -35,6 +35,14 @@ export interface AssembleOptions {
* (backward compat).
*/
provenance?: BundleProvenance;
/**
* Optional Item-2 compatibility input. Caller declares minimum
* daemon + CLI versions and optional schema_version reaffirmation.
* Missing block = no compatibility recorded (backward compat); the
* install-time version check at /api/bundles/install (Checkpoint 3.3)
* is a no-op for bundles without compatibility.
*/
compatibility?: BundleCompatibility;
}
/**
@@ -128,6 +136,9 @@ export class LegacyBundleAssembler {
createdAt: opts.provenance.createdAt ?? createdAt,
};
}
if (opts.compatibility) {
manifest.compatibility = { ...opts.compatibility };
}
// Write bundle.yaml
this.fs.writeFile(
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
import { RigSpecCodec } from "./rigspec-codec.js";
import { RigSpecSchema } from "./rigspec-schema.js";
import { resolveAgentRef, type AgentResolverFsOps } from "./agent-resolver.js";
import { serializePodBundleManifest, type PodBundleManifest, type PodBundleAgentEntry, type PodBundleAgentImportEntry, type BundleProvenance } from "./bundle-types.js";
import { serializePodBundleManifest, type PodBundleManifest, type PodBundleAgentEntry, type PodBundleAgentImportEntry, type BundleProvenance, type BundleCompatibility } from "./bundle-types.js";
import type { RigSpec, StartupBlock } from "./types.js";
export interface PodAssemblerFsOps extends AgentResolverFsOps {
@@ -28,6 +28,12 @@ export interface PodAssembleOptions {
* (backward compat).
*/
provenance?: BundleProvenance;
/**
* Optional Item-2 compatibility input. Caller declares minimum
* daemon + CLI versions and optional schema_version reaffirmation.
* Missing block = no compatibility recorded (backward compat).
*/
compatibility?: BundleCompatibility;
}
export interface PodAssembleResult {
@@ -188,6 +194,9 @@ export class PodBundleAssembler {
createdAt: opts.provenance.createdAt ?? createdAt,
};
}
if (opts.compatibility) {
manifest.compatibility = { ...opts.compatibility };
}
// Write manifest
this.fs.writeFile(
+26 -4
View File
@@ -14,8 +14,8 @@ 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, normalizeProvenanceBlock } from "../domain/bundle-types.js";
import type { PodBundleManifest, BundleProvenance } from "../domain/bundle-types.js";
import { parseLegacyBundleManifest as parseBundleManifest, normalizeLegacyBundleManifest as normalizeBundleManifest, serializePodBundleManifest, parsePodBundleManifest, validatePodBundleManifest, normalizeProvenanceBlock, normalizeCompatibilityBlock } from "../domain/bundle-types.js";
import type { PodBundleManifest, BundleProvenance, BundleCompatibility } from "../domain/bundle-types.js";
import { fileURLToPath } from "node:url";
/**
@@ -35,6 +35,21 @@ function getDaemonVersion(): string {
}
}
/**
* Sanitize raw compatibility from request body into a BundleCompatibility
* object. Only known typed fields accepted; unknown fields dropped silently.
* Returns undefined if input is missing or has no usable fields.
*/
function compatibilityFromRequestBody(raw: unknown): BundleCompatibility | undefined {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return undefined;
const c = raw as Record<string, unknown>;
const result: BundleCompatibility = {};
if (typeof c["minDaemonVersion"] === "string") result.minDaemonVersion = c["minDaemonVersion"];
if (typeof c["minCliVersion"] === "string") result.minCliVersion = c["minCliVersion"];
if (typeof c["schemaVersion"] === "number") result.schemaVersion = c["schemaVersion"];
return Object.keys(result).length > 0 ? result : undefined;
}
/**
* Normalize raw provenance from request body into a BundleProvenance object.
* Only string fields are accepted; unknown/non-string fields are dropped
@@ -129,6 +144,9 @@ bundleRoutes.post("/create", async (c) => {
? { ...clientProvenance, daemonVersion: getDaemonVersion() }
: undefined;
// Item 2 / slice-05: build compatibility from request body (no server-side fields)
const compatibility = compatibilityFromRequestBody(body["compatibility"]);
if (!specPath || !bundleName || !bundleVersion || !outputPath) {
return c.json({ error: "specPath, bundleName, bundleVersion, and outputPath are required" }, 400);
}
@@ -148,7 +166,7 @@ bundleRoutes.post("/create", async (c) => {
const tmpStaging = fs.mkdtempSync(nodePath.join(os.tmpdir(), "pod-bundle-create-"));
try {
const assembler = new PodBundleAssembler({ fsOps: podAssemblerFsOps() });
const result = assembler.assemble({ rigRoot: effectiveRigRoot, rigSpecPath: nodePath.resolve(specPath), outputDir: tmpStaging, bundleName, bundleVersion, provenance });
const result = assembler.assemble({ rigRoot: effectiveRigRoot, rigSpecPath: nodePath.resolve(specPath), outputDir: tmpStaging, bundleName, bundleVersion, provenance, compatibility });
const integrity = computeIntegrity(tmpStaging, integrityFsOps());
result.manifest.integrity = integrity;
@@ -205,7 +223,7 @@ bundleRoutes.post("/create", async (c) => {
try {
const assembler = new BundleAssembler({ fsOps: assemblerFsOps() });
const manifest = assembler.assemble({
specPath: nodePath.resolve(specPath), packages, outputDir: tmpStaging, bundleName, bundleVersion, provenance,
specPath: nodePath.resolve(specPath), packages, outputDir: tmpStaging, bundleName, bundleVersion, provenance, compatibility,
});
const integrity = computeIntegrity(tmpStaging, integrityFsOps());
@@ -291,6 +309,10 @@ bundleRoutes.post("/inspect", async (c) => {
// (v1 path normalizes through normalizeLegacyBundleManifest below).
// Field is optional; undefined when bundle has no provenance.
provenance: normalizeProvenanceBlock(rawParsed["provenance"]),
// Item 2 / slice-05: surface compatibility normalized to camelCase
// (same single-contract reason as provenance above). v1 already
// surfaces via the normalizer at the end of this handler.
compatibility: normalizeCompatibilityBlock(rawParsed["compatibility"]),
};
const integrityCompat = integritySection ? {
schemaVersion: 2,
+50 -1
View File
@@ -5,7 +5,7 @@ import os from "node:os";
// TODO: AS-T12 — migrate to pod-aware bundle assembler
import { LegacyBundleAssembler as BundleAssembler, type AssemblerFsOps } from "../src/domain/bundle-assembler.js";
// TODO: AS-T12 — migrate to pod-aware bundle types
import { parseLegacyBundleManifest as parseBundleManifest, validateLegacyBundleManifest as validateBundleManifest, normalizeLegacyBundleManifest as normalizeBundleManifest, type BundleProvenance } from "../src/domain/bundle-types.js";
import { parseLegacyBundleManifest as parseBundleManifest, validateLegacyBundleManifest as validateBundleManifest, normalizeLegacyBundleManifest as normalizeBundleManifest, type BundleProvenance, type BundleCompatibility } from "../src/domain/bundle-types.js";
const VALID_SPEC = `
schema_version: 1
@@ -317,4 +317,53 @@ describe("BundleAssembler", () => {
const diskYaml = fs.readFileSync(path.join(outputDir, "bundle.yaml"), "utf-8");
expect(diskYaml).not.toContain("provenance:");
});
// Item 2 — compatibility capture (slice-05 Checkpoint 3.2)
it("captures full compatibility from opts into manifest", () => {
const specPath = writeSpec();
const pkgDir = writePkg("pkg", { "package.yaml": "name: pkg" });
const outputDir = path.join(tmpDir, "staging");
const assembler = new BundleAssembler({ fsOps: realFsOps() });
const compatibility: BundleCompatibility = {
minDaemonVersion: "0.3.2",
minCliVersion: "0.3.2",
schemaVersion: 1,
};
const manifest = assembler.assemble({
specPath, outputDir, bundleName: "test", bundleVersion: "1.0",
packages: [{ name: "pkg", version: "1.0", sourcePath: pkgDir, originalSource: "local:./pkg", manifestHash: "h1" }],
compatibility,
});
expect(manifest.compatibility).toBeDefined();
expect(manifest.compatibility?.minDaemonVersion).toBe("0.3.2");
expect(manifest.compatibility?.minCliVersion).toBe("0.3.2");
expect(manifest.compatibility?.schemaVersion).toBe(1);
// Round-trip via the on-disk bundle.yaml
const diskYaml = fs.readFileSync(path.join(outputDir, "bundle.yaml"), "utf-8");
const parsed = parseBundleManifest(diskYaml);
const validation = validateBundleManifest(parsed, { requireIntegrity: false });
expect(validation.valid).toBe(true);
const normalized = normalizeBundleManifest(parsed);
expect(normalized.compatibility?.minDaemonVersion).toBe("0.3.2");
expect(normalized.compatibility?.minCliVersion).toBe("0.3.2");
});
it("omits compatibility when opts.compatibility not provided (backward compat)", () => {
const specPath = writeSpec();
const pkgDir = writePkg("pkg", { "package.yaml": "name: pkg" });
const outputDir = path.join(tmpDir, "staging");
const assembler = new BundleAssembler({ fsOps: realFsOps() });
const manifest = assembler.assemble({
specPath, outputDir, bundleName: "test", bundleVersion: "1.0",
packages: [{ name: "pkg", version: "1.0", sourcePath: pkgDir, originalSource: "local:./pkg", manifestHash: "h1" }],
});
expect(manifest.compatibility).toBeUndefined();
const diskYaml = fs.readFileSync(path.join(outputDir, "bundle.yaml"), "utf-8");
expect(diskYaml).not.toContain("compatibility:");
});
});
@@ -487,6 +487,100 @@ describe("Bundle API routes", () => {
expect(body.digestValid).toBe(true);
});
// Item 2 / slice-05 / Checkpoint 3.2: v1 create -> inspect compatibility round-trip.
// Discriminator: removing the v1 inspect normalizer's compatibility surfacing
// OR the route /create compatibility-extraction must make this test fail.
it("POST /api/bundles/create accepts compatibility + /inspect surfaces it (v1 round-trip)", async () => {
const { specPath } = seedPackage();
const bundlePath = path.join(tmpDir, "compat-test.rigbundle");
const createRes = await app.request("/api/bundles/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
specPath, bundleName: "compat-test", bundleVersion: "0.1.0", outputPath: bundlePath,
compatibility: { minDaemonVersion: "0.3.2", minCliVersion: "0.3.2", schemaVersion: 1 },
}),
});
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.compatibility).toBeDefined();
expect(inspectBody.manifest.compatibility.minDaemonVersion).toBe("0.3.2");
expect(inspectBody.manifest.compatibility.minCliVersion).toBe("0.3.2");
expect(inspectBody.manifest.compatibility.schemaVersion).toBe(1);
// Negative — snake_case keys must NOT be present (camelCase contract)
expect(inspectBody.manifest.compatibility.min_daemon_version).toBeUndefined();
});
// Item 2 / slice-05 / Checkpoint 3.2: v2 create -> inspect compatibility round-trip.
// Avoids the B1 trap from Item 1: this test ships in the SAME commit as the v2 inspect
// compatibility projection in routes/bundles.ts. Discriminator: removing the v2
// compatibility-projection line must make this test fail.
it("POST /api/bundles/inspect with v2 bundle surfaces compatibility 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-compat-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-compat.rigbundle");
const createRes = await app.request("/api/bundles/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
specPath, bundleName: "v2-compat", bundleVersion: "0.1.0", outputPath: bundlePath,
compatibility: { minDaemonVersion: "0.3.2", minCliVersion: "0.3.2" },
}),
});
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);
expect(inspectBody.manifest.compatibility).toBeDefined();
expect(inspectBody.manifest.compatibility.minDaemonVersion).toBe("0.3.2");
expect(inspectBody.manifest.compatibility.minCliVersion).toBe("0.3.2");
// Negative — snake_case keys must NOT be present (camelCase contract)
expect(inspectBody.manifest.compatibility.min_daemon_version).toBeUndefined();
expect(inspectBody.manifest.compatibility.min_cli_version).toBeUndefined();
});
// 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
@@ -580,6 +580,54 @@ describe("PodBundleAssembler", () => {
expect(result.manifest.provenance).toBeUndefined();
});
// Item 2 — compatibility capture (slice-05 Checkpoint 3.2)
it("v2: captures compatibility from opts into manifest", () => {
const spec = makeRigSpec();
const files: Record<string, string> = {
[`${RIG_ROOT}/rig.yaml`]: rigSpecYaml(spec),
[`${RIG_ROOT}/agents/impl/agent.yaml`]: validAgentYaml("impl"),
};
const fs = mockFs(files);
const assembler = new PodBundleAssembler({ fsOps: fs });
const result = assembler.assemble({
rigRoot: RIG_ROOT,
rigSpecPath: `${RIG_ROOT}/rig.yaml`,
outputDir: "/tmp/bundle-staging-compat",
bundleName: "test-bundle",
bundleVersion: "1.0.0",
compatibility: {
minDaemonVersion: "0.3.2",
minCliVersion: "0.3.2",
schemaVersion: 2,
},
});
expect(result.manifest.compatibility).toBeDefined();
expect(result.manifest.compatibility?.minDaemonVersion).toBe("0.3.2");
expect(result.manifest.compatibility?.minCliVersion).toBe("0.3.2");
expect(result.manifest.compatibility?.schemaVersion).toBe(2);
});
it("v2: omits compatibility when opts.compatibility not provided (backward compat)", () => {
const spec = makeRigSpec();
const files: Record<string, string> = {
[`${RIG_ROOT}/rig.yaml`]: rigSpecYaml(spec),
[`${RIG_ROOT}/agents/impl/agent.yaml`]: validAgentYaml("impl"),
};
const fs = mockFs(files);
const assembler = new PodBundleAssembler({ fsOps: fs });
const result = assembler.assemble({
rigRoot: RIG_ROOT,
rigSpecPath: `${RIG_ROOT}/rig.yaml`,
outputDir: "/tmp/bundle-staging-no-compat",
bundleName: "test", bundleVersion: "1.0",
});
expect(result.manifest.compatibility).toBeUndefined();
});
});
describe("PodBundleManifest validation", () => {