fix(daemon): keep binary files byte-identical in rig bundles (#257)

* fix(daemon): keep binary files byte-identical in rig bundles

Bundle create read every vendored agent-package file, and the declared
culture, docs and startup files, as UTF-8 text and wrote it back as UTF-8.
Any byte sequence that is not valid UTF-8 became U+FFFD, and the integrity
manifest was hashed from the changed copy, so inspect and install verified
the corrupted bytes as intact.

Copy those files as bytes: the pod assembler reads them with readFileBuffer
and writes the same bytes, and the install-side skills and workflow-spec
routers copy with fs.copyFileSync. Rewriting rig.yaml and agent.yaml import
refs stays a text operation.

Refs #245

* test(daemon): assert async ps samplers stay pending through a microtask drain

The three anti-sync discriminators in list-processes-async.test.ts raced a
0ms timer against the sampler's result. Timer order is not a valid guarantee
of async execution: in a local probe, a 70ms synchronous stall after spawning
a single-PID `ps eww` let the result settle before the 0ms timer in most
trials. The CI failure of this test is consistent with that race; the CI
timing itself was not measured.

Assert instead that the returned promise is still pending after draining 20
microtask ticks: an async execFile cannot settle without an event-loop turn,
while a synchronous ps inside the call settles within a few ticks. The real
rows and HOME assertions are unchanged.

Refs #245

---------

Co-authored-by: OpenRig contributors <noreply@openrig.dev>
This commit is contained in:
Mike Schwarz
2026-09-30 19:28:55 -07:00
committed by GitHub
co-authored by OpenRig contributors
parent 0a22ea184c
commit 2174b11e5d
9 changed files with 283 additions and 50 deletions
@@ -22,8 +22,8 @@ import nodePath from "node:path";
/** Filesystem injection point — real impl wraps node:fs. Tests substitute in-memory. */
export interface SkillsRouterFsOps {
exists: (path: string) => boolean;
readFile: (path: string) => string;
writeFile: (path: string, content: string) => void;
/** Byte-for-byte copy; routed files are never decoded. */
copyFile: (src: string, dest: string) => void;
mkdirp: (path: string) => void;
}
@@ -125,8 +125,7 @@ export function routeSkills(input: RouteSkillsInput, fs: SkillsRouterFsOps): Rou
continue;
}
fs.mkdirp(nodePath.dirname(targetAbs));
const content = fs.readFile(sourceAbs);
fs.writeFile(targetAbs, content);
fs.copyFile(sourceAbs, targetAbs);
records.push({
declaredPath: declared,
status: "routed",
@@ -31,8 +31,8 @@ import nodePath from "node:path";
/** Filesystem injection point — real impl wraps node:fs. Tests substitute in-memory. */
export interface WorkflowSpecsRouterFsOps {
exists: (path: string) => boolean;
readFile: (path: string) => string;
writeFile: (path: string, content: string) => void;
/** Byte-for-byte copy; routed files are never decoded. */
copyFile: (src: string, dest: string) => void;
mkdirp: (path: string) => void;
}
@@ -195,8 +195,7 @@ export function routeWorkflowSpecs(
});
continue;
}
const content = fs.readFile(sourceAbs);
fs.writeFile(targetAbs, content);
fs.copyFile(sourceAbs, targetAbs);
routedBasenames.add(basename);
records.push({
declaredPath: declared,
@@ -7,8 +7,10 @@ import { serializePodBundleManifest, type PodBundleManifest, type PodBundleAgent
import type { RigSpec, StartupBlock } from "./types.js";
export interface PodAssemblerFsOps extends AgentResolverFsOps {
/** Raw bytes, for files the bundle copies verbatim (agent packages, culture, docs, startup files). */
readFileBuffer(path: string): Uint8Array;
mkdirp(path: string): void;
writeFile(path: string, content: string): void;
writeFile(path: string, content: string | Uint8Array): void;
copyDir(src: string, dest: string): void;
listFiles(dirPath: string): string[];
}
@@ -214,7 +216,7 @@ export class PodBundleAssembler {
throw new Error(`Path traversal detected: "${relPath}" escapes rig root`);
}
if (!this.fs.exists(absPath)) return; // optional files may not exist
const content = this.fs.readFile(absPath);
const content = this.fs.readFileBuffer(absPath);
assertShippableSubstance([{ path: relPath, bytes: content }]);
this.fs.mkdirp(nodePath.dirname(nodePath.join(outputDir, relPath)));
this.fs.writeFile(nodePath.join(outputDir, relPath), content);
@@ -232,7 +234,7 @@ export class PodBundleAssembler {
const files = this.fs.listFiles(srcDir);
const sources = files.map((file) => ({
file,
content: this.fs.readFile(nodePath.join(srcDir, file)),
content: this.fs.readFileBuffer(nodePath.join(srcDir, file)),
}));
assertShippableSubstance(sources.map(({ file, content }) => ({
path: nodePath.join(relPrefix, file),
+4 -4
View File
@@ -346,6 +346,8 @@ function podAssemblerFsOps(): PodAssemblerFsOps {
return {
...assemblerFsOps(),
readFile: (p) => fs.readFileSync(p, "utf-8"),
readFileBuffer: (p) => fs.readFileSync(p),
writeFile: (p, c) => fs.writeFileSync(p, c),
exists: (p) => fs.existsSync(p),
listFiles: (dir) => realFsOps().listFiles!(dir),
};
@@ -365,8 +367,7 @@ function pluginsRouterFsOps(): PluginsRouterFsOps {
function workflowSpecsRouterFsOps(): WorkflowSpecsRouterFsOps {
return {
exists: (p) => fs.existsSync(p),
readFile: (p) => fs.readFileSync(p, "utf-8"),
writeFile: (p, c) => fs.writeFileSync(p, c, "utf-8"),
copyFile: (s, d) => fs.copyFileSync(s, d),
mkdirp: (p) => fs.mkdirSync(p, { recursive: true }),
};
}
@@ -592,8 +593,7 @@ async function routePluginsAfterBootstrap(bundlePath: string): Promise<RoutePlug
function skillsRouterFsOps(): SkillsRouterFsOps {
return {
exists: (p) => fs.existsSync(p),
readFile: (p) => fs.readFileSync(p, "utf-8"),
writeFile: (p, c) => fs.writeFileSync(p, c, "utf-8"),
copyFile: (s, d) => fs.copyFileSync(s, d),
mkdirp: (p) => fs.mkdirSync(p, { recursive: true }),
};
}
@@ -0,0 +1,141 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { createHash } from "node:crypto";
import type Database from "better-sqlite3";
import { createDb } from "../src/db/connection.js";
import { migrate } from "../src/db/migrate.js";
import { ALL_MIGRATIONS } from "../src/db/all-migrations.js";
import { createTestApp } from "./helpers/test-app.js";
import { unpack } from "../src/domain/bundle-archive.js";
import { materializePodBundle } from "../src/domain/bundle-source-resolver.js";
// A pod bundle copies agent packages and declared rig files verbatim. Every byte must survive
// create -> archive -> install, whether or not the file is UTF-8 text.
const sha = (b: Uint8Array) => createHash("sha256").update(b).digest("hex");
function pseudoRandom(n: number, seed: number): Buffer {
const out = Buffer.alloc(n);
let x = seed >>> 0;
for (let i = 0; i < n; i++) {
x = (Math.imul(x, 1664525) + 1013904223) >>> 0;
out[i] = x >>> 24;
}
return out;
}
const PAYLOADS: Record<string, Buffer> = {
"agents/impl/assets/random.bin": pseudoRandom(4096, 7),
"agents/impl/assets/logo.png": Buffer.concat([Buffer.from("89504e470d0a1a0a", "hex"), Buffer.from(Array.from({ length: 128 }, (_, i) => 128 + i))]),
"agents/impl/assets/nul.dat": Buffer.from([0x61, 0x00, 0x62, 0x00, 0x00, 0x63]),
"agents/impl/assets/invalid-utf8.txt": Buffer.from([0x6f, 0x6b, 0x20, 0xc3, 0x28, 0x20, 0xa0, 0xa1, 0x20, 0xff]),
"agents/impl/assets/empty.txt": Buffer.alloc(0),
"agents/impl/skills/greet/SKILL.md": Buffer.from("Greet ü 漢 😀\r\nCRLF line\r\nno trailing newline", "utf8"),
"startup/blob.bin": Buffer.from([0xfe, 0xed, 0xfa, 0xce, 0x80, 0x00, 0xc0]),
};
const AGENT_YAML = [
'name: impl',
'version: "1.0.0"',
"resources:",
" skills:",
" - id: greet",
" path: skills/greet",
"profiles:",
" default:",
" uses:",
" skills: [greet]",
].join("\n");
const RIG_YAML = [
'version: "0.2"',
"name: bytes-rig",
"pods:",
" - id: dev",
" label: Dev",
" members:",
" - id: impl",
' agent_ref: "local:agents/impl"',
" profile: default",
" runtime: claude-code",
" cwd: .",
" startup:",
" files:",
" - path: startup/blob.bin",
" actions: []",
" edges: []",
"edges: []",
].join("\n");
describe("pod bundle byte preservation (create -> archive -> install)", () => {
let db: Database.Database;
let app: ReturnType<typeof createTestApp>["app"];
let tmpDir: string;
beforeEach(() => {
db = createDb();
migrate(db, ALL_MIGRATIONS);
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-bytes-"));
app = createTestApp(db).app;
});
afterEach(() => {
db.close();
fs.rmSync(tmpDir, { recursive: true, force: true });
});
it("keeps binary, non-UTF-8, NUL, empty and multibyte files byte-identical, with truthful integrity", async () => {
const src = path.join(tmpDir, "src");
for (const [rel, bytes] of Object.entries(PAYLOADS)) {
fs.mkdirSync(path.dirname(path.join(src, rel)), { recursive: true });
fs.writeFileSync(path.join(src, rel), bytes);
}
fs.writeFileSync(path.join(src, "agents/impl/agent.yaml"), AGENT_YAML);
fs.writeFileSync(path.join(src, "rig.yaml"), RIG_YAML);
const sourceBefore = Object.fromEntries(Object.keys(PAYLOADS).map((rel) => [rel, sha(fs.readFileSync(path.join(src, rel)))]));
const outputPath = path.join(tmpDir, "bytes.rigbundle");
const created = await app.request("/api/bundles/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ specPath: path.join(src, "rig.yaml"), rigRoot: src, bundleName: "bytes", bundleVersion: "0.1.0", outputPath }),
});
expect(created.status, await created.clone().text()).toBe(201);
expect((await created.json()).schemaVersion).toBe(2);
// Source assets stay untouched.
for (const rel of Object.keys(PAYLOADS)) {
expect(sha(fs.readFileSync(path.join(src, rel))), rel).toBe(sourceBefore[rel]);
}
// Archive: every copied file equals its source, and the manifest hashes the source bytes.
const extracted = path.join(tmpDir, "extracted");
fs.mkdirSync(extracted);
await unpack(outputPath, extracted);
const manifest = fs.readFileSync(path.join(extracted, "bundle.yaml"), "utf8");
for (const [rel, bytes] of Object.entries(PAYLOADS)) {
const archived = fs.readFileSync(path.join(extracted, rel));
expect(archived.equals(bytes), `${rel} in archive`).toBe(true);
expect(manifest, `${rel} integrity hash`).toContain(`${rel}: ${sha(bytes)}`);
}
const inspected = await app.request("/api/bundles/inspect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ bundlePath: outputPath }),
});
const view = await inspected.json();
expect(view.digestValid).toBe(true);
expect(view.integrityResult.passed).toBe(true);
// Install: the materialized target holds the source bytes.
const target = path.join(tmpDir, "target");
fs.mkdirSync(target);
expect(materializePodBundle(extracted, target)).toEqual({ ok: true });
for (const [rel, bytes] of Object.entries(PAYLOADS)) {
expect(fs.readFileSync(path.join(target, rel)).equals(bytes), `${rel} installed`).toBe(true);
}
});
});
@@ -1,5 +1,7 @@
import { describe, it, expect } from "vitest";
import nodePath from "node:path";
import realFs from "node:fs";
import os from "node:os";
import { routeSkills, type SkillsRouterFsOps, type RouteSkillsInput } from "../src/domain/bundle-skills-router.js";
// Item 6 / slice-05 Checkpoint 7.2: bundle-skills-router pure-function tests.
@@ -11,12 +13,11 @@ function mockFs(initialFiles: Record<string, string> = {}): SkillsRouterFsOps &
_written: written,
_mkdirpCalls: mkdirpCalls,
exists: (p: string) => written.has(p),
readFile: (p: string) => {
const v = written.get(p);
if (v === undefined) throw new Error(`File not found in mock: ${p}`);
return v;
copyFile: (src: string, dest: string) => {
const v = written.get(src);
if (v === undefined) throw new Error(`File not found in mock: ${src}`);
written.set(dest, v);
},
writeFile: (p: string, c: string) => { written.set(p, c); },
mkdirp: (p: string) => { mkdirpCalls.push(p); },
};
}
@@ -173,3 +174,25 @@ describe("routeSkills", () => {
expect(fs._written.get(`${packageRoot}/test-pkg/skills/DUAL.md`)).toBe("dual");
});
});
describe("routeSkills copies bytes through the real filesystem", () => {
it("a non-UTF-8 file arrives byte-identical", () => {
const root = realFs.mkdtempSync(nodePath.join(os.tmpdir(), "bundle-skills-router-bytes-"));
try {
const bytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x80, 0x00, 0xc3, 0x28, 0xff, 0xfe]);
const bundleRoot = nodePath.join(root, "bundle");
realFs.mkdirSync(nodePath.join(bundleRoot, "skills/blob"), { recursive: true });
realFs.writeFileSync(nodePath.join(bundleRoot, "skills/blob", "logo.png"), bytes);
const target = nodePath.join(root, "target");
const result = routeSkills(
{ ...makeInput(), bundleRoot, declaredSkills: ["skills/blob/logo.png"], targetSkillsDir: target },
{ exists: realFs.existsSync, mkdirp: (p) => realFs.mkdirSync(p, { recursive: true }), copyFile: realFs.copyFileSync },
);
expect(result.routedCount).toBe(1);
const installedAt = result.records[0]!.installedAt!;
expect(realFs.readFileSync(installedAt).equals(bytes)).toBe(true);
} finally {
realFs.rmSync(root, { recursive: true, force: true });
}
});
});
@@ -1,5 +1,7 @@
import { describe, it, expect } from "vitest";
import nodePath from "node:path";
import realFs from "node:fs";
import os from "node:os";
import { routeWorkflowSpecs, type WorkflowSpecsRouterFsOps, type RouteWorkflowSpecsInput } from "../src/domain/bundle-workflow-specs-router.js";
// Item 6 / slice-05 Checkpoint 7.3e step 2: bundle-workflow-specs-router
@@ -13,12 +15,11 @@ function mockFs(initialFiles: Record<string, string> = {}): WorkflowSpecsRouterF
_written: written,
_mkdirpCalls: mkdirpCalls,
exists: (p: string) => written.has(p),
readFile: (p: string) => {
const v = written.get(p);
if (v === undefined) throw new Error(`File not found in mock: ${p}`);
return v;
copyFile: (src: string, dest: string) => {
const v = written.get(src);
if (v === undefined) throw new Error(`File not found in mock: ${src}`);
written.set(dest, v);
},
writeFile: (p: string, c: string) => { written.set(p, c); },
mkdirp: (p: string) => { mkdirpCalls.push(p); },
};
}
@@ -230,3 +231,25 @@ describe("routeWorkflowSpecs", () => {
expect(r2.records[0]!.status).toBe("routed");
});
});
describe("routeWorkflowSpecs copies bytes through the real filesystem", () => {
it("a non-UTF-8 file arrives byte-identical", () => {
const root = realFs.mkdtempSync(nodePath.join(os.tmpdir(), "bundle-workflow-specs-router-bytes-"));
try {
const bytes = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x80, 0x00, 0xc3, 0x28, 0xff, 0xfe]);
const bundleRoot = nodePath.join(root, "bundle");
realFs.mkdirSync(nodePath.join(bundleRoot, "workflows"), { recursive: true });
realFs.writeFileSync(nodePath.join(bundleRoot, "workflows", "latin1.yaml"), bytes);
const target = nodePath.join(root, "target");
const result = routeWorkflowSpecs(
{ ...makeInput(), bundleRoot, declaredWorkflowSpecs: ["workflows/latin1.yaml"], targetWorkflowSpecsDir: target },
{ exists: realFs.existsSync, mkdirp: (p) => realFs.mkdirSync(p, { recursive: true }), copyFile: realFs.copyFileSync },
);
expect(result.routedCount).toBe(1);
const installedAt = result.records[0]!.installedAt!;
expect(realFs.readFileSync(installedAt).equals(bytes)).toBe(true);
} finally {
realFs.rmSync(root, { recursive: true, force: true });
}
});
});
@@ -6,7 +6,7 @@
// pre-B12 implementation violated: invoking the sampler must hand control back to the event loop
// immediately instead of blocking for the whole spawn. The old code ran ps via execSync inside the
// async body, so the CALL ITSELF stalled for the full ps duration (measured ~100-220ms on this
// class of box) and every HTTP request queued behind it. The discriminator is an ORDERING
// class of box) and every HTTP request queued behind it. The discriminator is a SETTLEMENT
// property, not a wall-clock bound — the inline note records why a bound was tried and dropped.
// Door test: revert the async wrap locally and this fails; on the candidate it passes.
@@ -14,6 +14,23 @@ import { describe, it, expect } from "vitest";
import { defaultListProcesses as refresherListProcesses } from "../src/domain/resume-metadata-refresher.js";
import { defaultListProcesses as codexListProcesses } from "../src/adapters/codex-runtime-adapter.js";
// The discriminator. Drain microtasks only — no timers, no I/O. An async ps sampler cannot settle
// here, because its child-process I/O needs an event-loop turn that no microtask provides. A sampler
// that ran ps synchronously inside the call settles within a few ticks (the pre-B12/F1 shape needs
// about 3). The depth is a bounded assumption, not a proof that nothing can block: a synchronous
// spawn hidden behind more than MICROTASK_DRAIN_TICKS awaits would also read as pending. An earlier
// version raced a 0ms timer against the result instead. Timer order can race: in a local probe, a
// 70ms synchronous stall after spawning a single-PID ps let the result settle before a 0ms timer in
// most trials. A CI failure of that version is consistent with this race (inferred; the CI timing
// was not measured), so timer order is not asserted.
const MICROTASK_DRAIN_TICKS = 20;
async function settlesWithinMicrotasks(pending: Promise<unknown>): Promise<boolean> {
let settled = false;
void pending.then(() => { settled = true; }, () => { settled = true; });
for (let i = 0; i < MICROTASK_DRAIN_TICKS; i++) await Promise.resolve();
return settled;
}
const SITES = [
["resume-metadata-refresher", refresherListProcesses],
["codex-runtime-adapter", codexListProcesses],
@@ -31,19 +48,14 @@ describe.each(SITES)("B12-T real async list_processes — %s", (_site, listProce
it("hands control back to the event loop instead of blocking for the spawn (RED on the pre-B12 sync implementation)", async () => {
// Note on what is NOT asserted: the invocation's synchronous-return time. Measured here, even
// the async implementation spends 60-80ms in the call under load (child-process spawn setup),
// so a wall-clock bound is environment-hostage. The deterministic discriminator is ORDER: the
// pre-B12 execSync implementation finished ps inside the call, so its promise settles on the
// first microtask — ahead of any timer — and `turnedBeforeResolve` reads false there, always.
// so a wall-clock bound is environment-hostage. The pre-B12 execSync implementation finished ps
// inside the call, so its promise settles within the microtask drain; the async one cannot.
const pending = listProcesses();
// While ps runs, the loop must turn: a 0ms timer armed AFTER the call must fire BEFORE the
// (much slower) spawn resolves.
let loopTurnedFirst = false;
setTimeout(() => { loopTurnedFirst = true; }, 0);
const { rows, turnedBeforeResolve } = await pending.then((r) => ({ rows: r, turnedBeforeResolve: loopTurnedFirst }));
const settledEarly = await settlesWithinMicrotasks(pending);
const rows = await pending;
expect(rows.length).toBeGreaterThan(0); // the non-blocking return was not an empty-result shortcut
expect(turnedBeforeResolve).toBe(true);
expect(settledEarly).toBe(false);
});
});
@@ -77,13 +89,12 @@ describe("F1 real async resolve_home — codex-thread-id", () => {
it("hands control back to the event loop instead of blocking for the spawn (RED on the pre-F1 sync implementation)", async () => {
const { defaultResolveHomeDirByPid } = await import("../src/domain/codex-thread-id.js");
const { home, turnedBeforeResolve } = await withChild(async (pid) => {
const pending = defaultResolveHomeDirByPid(pid);
let loopTurnedFirst = false;
setTimeout(() => { loopTurnedFirst = true; }, 0);
return Promise.resolve(pending).then((h) => ({ home: h, turnedBeforeResolve: loopTurnedFirst }));
const { home, settledEarly } = await withChild(async (pid) => {
const pending = Promise.resolve(defaultResolveHomeDirByPid(pid));
const settledEarly = await settlesWithinMicrotasks(pending);
return { home: await pending, settledEarly };
});
expect(home).toBe("/tmp/f1-probe-home"); // the fast return was not an empty shortcut
expect(turnedBeforeResolve).toBe(true);
expect(settledEarly).toBe(false);
});
});
@@ -7,19 +7,27 @@ import type { RigSpec } from "../src/domain/types.js";
// -- Mock filesystem --
function mockFs(files: Record<string, string>): PodAssemblerFsOps {
const written: Record<string, string> = {};
function mockFs(files: Record<string, string | Uint8Array>): PodAssemblerFsOps {
const written: Record<string, string | Uint8Array> = {};
const dirs = new Set<string>();
const read = (p: string): string | Uint8Array => {
if (p in files) return files[p]!;
if (p in written) return written[p]!;
throw new Error(`File not found: ${p}`);
};
return {
readFile: (p: string) => {
if (p in files) return files[p]!;
if (p in written) return written[p]!;
throw new Error(`File not found: ${p}`);
const v = read(p);
return typeof v === "string" ? v : Buffer.from(v).toString("utf8");
},
readFileBuffer: (p: string) => {
const v = read(p);
return typeof v === "string" ? Buffer.from(v, "utf8") : v;
},
exists: (p: string) => p in files || p in written,
mkdirp: (p: string) => { dirs.add(p); },
writeFile: (p: string, content: string) => { written[p] = content; },
writeFile: (p: string, content: string | Uint8Array) => { written[p] = content; },
copyDir: () => {},
listFiles: (dirPath: string) => {
const result: string[] = [];
@@ -31,7 +39,7 @@ function mockFs(files: Record<string, string>): PodAssemblerFsOps {
return result;
},
_written: written, // for test inspection
} as PodAssemblerFsOps & { _written: Record<string, string> };
} as PodAssemblerFsOps & { _written: Record<string, string | Uint8Array> };
}
// -- Helpers --
@@ -463,6 +471,33 @@ describe("PodBundleAssembler", () => {
});
// T11: integration: assemble -> verify manifest + file contents
it("copies agent-package and rig files as bytes, never decoding them", () => {
const spec = makeRigSpec({
cultureFile: "culture.md",
startup: { files: [{ path: "startup/blob.bin", deliveryHint: "auto", required: true, appliesOn: ["fresh_start", "restore"] }], actions: [] },
});
const binary = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x80, 0x00, 0xc3, 0x28, 0xff]);
const culture = Uint8Array.from([0x23, 0x20, 0xa0, 0xa1, 0x0a]);
const startup = Uint8Array.from([0xfe, 0xed, 0x00, 0xc0]);
const files: Record<string, string | Uint8Array> = {
[`${RIG_ROOT}/rig.yaml`]: rigSpecYaml(spec),
[`${RIG_ROOT}/culture.md`]: culture,
[`${RIG_ROOT}/startup/blob.bin`]: startup,
[`${RIG_ROOT}/agents/impl/agent.yaml`]: validAgentYaml("impl"),
[`${RIG_ROOT}/agents/impl/assets/logo.png`]: binary,
};
const fs = mockFs(files);
new PodBundleAssembler({ fsOps: fs }).assemble({
rigRoot: RIG_ROOT, rigSpecPath: `${RIG_ROOT}/rig.yaml`,
outputDir: "/tmp/staging", bundleName: "bytes", bundleVersion: "1.0.0",
});
const written = (fs as unknown as { _written: Record<string, string | Uint8Array> })._written;
expect(Buffer.from(written["/tmp/staging/agents/impl/assets/logo.png"]!).equals(Buffer.from(binary))).toBe(true);
expect(Buffer.from(written["/tmp/staging/culture.md"]!).equals(Buffer.from(culture))).toBe(true);
expect(Buffer.from(written["/tmp/staging/startup/blob.bin"]!).equals(Buffer.from(startup))).toBe(true);
});
it("integration: assembled bundle has correct manifest and files", () => {
const spec = makeRigSpec({
cultureFile: "culture.md",
@@ -492,7 +527,7 @@ describe("PodBundleAssembler", () => {
// Verify written files exist
const written = (fs as unknown as { _written: Record<string, string> })._written;
expect(written["/tmp/staging/rig.yaml"]).toBeDefined();
expect(written["/tmp/staging/culture.md"]).toBe("# Culture doc");
expect(Buffer.from(written["/tmp/staging/culture.md"]!).toString("utf8")).toBe("# Culture doc");
expect(written["/tmp/staging/bundle.yaml"]).toBeDefined();
});