mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 22:58:42 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d7970fd50 |
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user