mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 03:39:51 -10:00
feat(slice-05): bundle-agent-images-router module — Item 6 cross-primitive bundling part 6b (Checkpoint 7.3g step 2) (OPR.0.3.2.5)
Pure-function router module for the agent_images cross-primitive kind. Per PRD §Item 6 line 197 +e7a0b253PRD-coherent contract: manifest declares agent_images entries as paths to agent-image DIRECTORIES (not manifest paths — distinct shape from context_packs). Router copies the declared directory to <targetAgentImagesDir>/<basename(sourceAbs)>/ (operator-host canonical layout per agent-image-types.ts:9-10). Consumer contract (agent-image-library-service.ts:77-95) is structurally IDENTICAL to context-pack-library-service.scan but the ROUTER'S code path differs in WHICH check it does WHERE (per banked tenth-catch discipline: PRD field-shape ≠ consumer target-shape even when consumer scan is the same). ALL FOUR banked router-level catches from the context_packs cycle built in PROACTIVELY from the first commit (no B1 cycles expected): - Source path containment (mirrors all sibling routers) - sourceAbs exists + isDirectory (declared path IS the image dir — PRD contract) - manifest.yaml-INSIDE-image-dir exists AS A FILE (file-presence and not-isDirectory checks; bankedd491eca9+3cd581e3lessons) - basename(sourceAbs) collision detection (routedDirNames Set; banked16ebb8aflesson) bundle-agent-images-router.ts (+205): - AgentImagesRouterFsOps interface (exists, isDirectory, mkdirp, copyDir) - 6-value status enum: routed / missing / unsafe / not_directory / not_manifest / conflict (not_manifest covers both manifest-absent and manifest-is-directory cases per the banked file-vs-dir discrimination) - routeAgentImages pure function with the full discrimination matrix bundle-agent-images-router.test.ts (+225): 10 tests A1-A10 — mock supports files+dirs sets per thed491eca9lesson; imageFixture(dir) helper builds a complete fixture (image dir + manifest.yaml file inside) for clarity. C9 mixed-list aggregate exercises ALL 6 statuses. Discriminator-flips BOTH PROACTIVELY: Flip A — remove the manifest-file existence + manifest-is-directory checks (both banked from context_packs). Result: 3/10 fail — exactly A7 (manifest absent) + A8 (manifest-is-directory) + A10 mixed-list aggregate. Proves both banked checks load-bearing. Restored. Flip B — remove the routedDirNames collision detection. Result: 2/10 fail — exactly A9 (basename collision) + A10 mixed-list aggregate. Proves collision detection load-bearing. Restored. Final: 46/46 across all 5 router modules: - bundle-skills-router 8 - bundle-plugins-router 9 - bundle-workflow-specs-router 11 - bundle-context-packs-router 11 - bundle-agent-images-router 10 (NEW) Per banked tenth-catch discipline: did NOT copy the context_packs manifest-path routing shape into agent_images. Followed the PRD field shape (dir-path) and the consumer contract (manifest.yaml inside the dir) separately. Step 3 will mirror context_packs /install integration shape with the agent_images path-contract adaptation. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
e7a0b253b0
commit
13d2adf992
2 files changed
+445
No files matched your search
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Bundle agent_images router (Item 6 / slice-05 Checkpoint 7.3g step 2).
|
||||
*
|
||||
* Pure function. Copies agent-image directories declared in a bundle
|
||||
* manifest's agent_images[] block from the bundle's extracted tree to the
|
||||
* operator agent-images library. No daemon dependencies — fully
|
||||
* unit-testable via FsOps injection.
|
||||
*
|
||||
* Per PRD §Item 6 line 197: agent_images entries are paths to the
|
||||
* agent-image DIRECTORY (not manifest paths — distinct shape from
|
||||
* context_packs per the e7a0b253 PRD-coherent repair). The router copies
|
||||
* the declared directory to <targetAgentImagesDir>/<basename(sourceAbs)>/
|
||||
* — the operator-host canonical layout per agent-image-types.ts:9-10.
|
||||
*
|
||||
* Consumer contract (agent-image-library-service.ts:77-95): the consumer
|
||||
* walks roots whose immediate children are image DIRS, requires each
|
||||
* image dir to be a directory + contain a manifest.yaml FILE. The router
|
||||
* enforces these consumer-visibility invariants at copy time so
|
||||
* routedCount stays truthful at the consumer-visible boundary.
|
||||
*
|
||||
* All four banked router-level catches from the context_packs cycle
|
||||
* (d491eca9 + 3cd581e3 + 16ebb8af + a0e7e0e1) apply identically and are
|
||||
* built in from the first commit:
|
||||
* 1. source path containment (mirrors all sibling routers)
|
||||
* 2. sourceAbs exists + isDirectory (declared path IS the image dir)
|
||||
* 3. manifest.yaml inside exists AS A FILE (consumer requires
|
||||
* readable manifest.yaml; rejects manifest-missing AND manifest-
|
||||
* is-a-directory cases — file-vs-dir discrimination axis)
|
||||
* 4. basename(sourceAbs) collision detection (first wins, second
|
||||
* flagged status=conflict)
|
||||
*
|
||||
* /install handler integration lands at Checkpoint 7.3g step 3 with a
|
||||
* real consumer-scan() proof against the routed dir (mirror of the
|
||||
* cb0bf7b9 context_packs ContextPackLibraryService.scan reachability
|
||||
* proof).
|
||||
*/
|
||||
|
||||
import nodePath from "node:path";
|
||||
|
||||
/** Filesystem injection point — real impl wraps node:fs. Tests substitute in-memory. */
|
||||
export interface AgentImagesRouterFsOps {
|
||||
exists: (path: string) => boolean;
|
||||
isDirectory: (path: string) => boolean;
|
||||
mkdirp: (path: string) => void;
|
||||
copyDir: (src: string, dest: string) => void;
|
||||
}
|
||||
|
||||
/** Inputs to routeAgentImages. */
|
||||
export interface RouteAgentImagesInput {
|
||||
/** Absolute path to the bundle's extracted root (tmp dir from unpack). */
|
||||
bundleRoot: string;
|
||||
/** Relative paths to agent-image DIRECTORIES declared in the bundle
|
||||
* manifest's agent_images[] block (per PRD §Item 6 line 197). */
|
||||
declaredAgentImages: string[];
|
||||
/** Absolute path to the operator agent-images library (per
|
||||
* agent-image-types.ts:9-10: typically `<openrigHome>/agent-images/`
|
||||
* or workspace-local `.openrig/agent-images/`). Caller resolves. */
|
||||
targetAgentImagesDir: string;
|
||||
}
|
||||
|
||||
/** One routed agent_image (or one rejection). */
|
||||
export interface RoutedAgentImageRecord {
|
||||
/** Declared path from manifest.agent_images[]. */
|
||||
declaredPath: string;
|
||||
/** "routed" = image dir copied successfully and consumer-visible.
|
||||
* "missing" = declared image dir not in bundle (honest skip).
|
||||
* "unsafe" = source escapes bundleRoot OR target escapes
|
||||
* targetAgentImagesDir (structurally impossible given basename, kept
|
||||
* defensive).
|
||||
* "not_directory" = declared path exists but is not a directory.
|
||||
* "not_manifest" = image dir exists but manifest.yaml inside is
|
||||
* absent OR is itself a directory — consumer requires a readable
|
||||
* manifest.yaml file at the image dir root.
|
||||
* "conflict" = basename collides with an earlier routed image; first
|
||||
* wins, second flagged so routedCount stays truthful at the consumer-
|
||||
* visible boundary (banked 16ebb8af lesson). */
|
||||
status: "routed" | "missing" | "unsafe" | "not_directory" | "not_manifest" | "conflict";
|
||||
/** Where the image landed in the target library (absolute image dir
|
||||
* path), if routed. */
|
||||
installedAt?: string;
|
||||
/** Human-readable detail (3-part error shape input for caller). */
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
/** Aggregate routing result. */
|
||||
export interface RouteAgentImagesResult {
|
||||
records: RoutedAgentImageRecord[];
|
||||
routedCount: number;
|
||||
rejectedCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Route each declared agent_image from the bundle tree to the operator
|
||||
* agent-images library. Per-entry safety: SOURCE containment under
|
||||
* bundleRoot + TARGET containment under targetAgentImagesDir. Consumer-
|
||||
* visibility invariants (declared dir IS a directory + manifest.yaml
|
||||
* inside exists AS A FILE) enforced BEFORE write so routedCount stays
|
||||
* truthful at the consumer-visible boundary. Caller writes the install
|
||||
* audit record using the records returned here.
|
||||
*/
|
||||
export function routeAgentImages(
|
||||
input: RouteAgentImagesInput,
|
||||
fs: AgentImagesRouterFsOps,
|
||||
): RouteAgentImagesResult {
|
||||
const records: RoutedAgentImageRecord[] = [];
|
||||
const bundleRootResolved = nodePath.resolve(input.bundleRoot);
|
||||
const targetRootResolved = nodePath.resolve(input.targetAgentImagesDir);
|
||||
fs.mkdirp(input.targetAgentImagesDir);
|
||||
// Track basenames already routed so collisions surface as conflict
|
||||
// records, not silent overwrites (banked workflow_specs +
|
||||
// context_packs lessons).
|
||||
const routedDirNames = new Set<string>();
|
||||
|
||||
for (const declared of input.declaredAgentImages) {
|
||||
const sourceAbs = nodePath.resolve(input.bundleRoot, declared);
|
||||
// Defense-in-depth path-containment on SOURCE (mirrors sibling routers).
|
||||
if (sourceAbs !== bundleRootResolved && !sourceAbs.startsWith(bundleRootResolved + nodePath.sep)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "unsafe",
|
||||
detail: `agent_image path '${declared}' escapes bundle workspace; rejected`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!fs.exists(sourceAbs)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "missing",
|
||||
detail: `agent_image dir '${declared}' not present in bundle; skipped`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!fs.isDirectory(sourceAbs)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "not_directory",
|
||||
detail: `agent_image '${declared}' exists but is not a directory; consumer requires the declared path to be an image dir`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// Manifest-file existence check (banked d491eca9 lesson on
|
||||
// context_packs): the consumer (agent-image-library-service.ts:93)
|
||||
// skips any image dir whose manifest.yaml is absent. Reject pre-write.
|
||||
const manifestPath = nodePath.join(sourceAbs, "manifest.yaml");
|
||||
if (!fs.exists(manifestPath)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "not_manifest",
|
||||
detail: `agent_image '${declared}' is missing manifest.yaml; consumer-invisible (agent-image-library scan requires it)`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
// Manifest-type check (banked 3cd581e3 lesson on context_packs):
|
||||
// exists() returns true for both files and directories. If manifest.yaml
|
||||
// is a directory, the consumer's readFileSync would throw and scan()
|
||||
// records an error diagnostic instead of indexing the image. Reject.
|
||||
if (fs.isDirectory(manifestPath)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "not_manifest",
|
||||
detail: `agent_image '${declared}' has manifest.yaml as a directory, not a file; consumer requires a readable file`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const dirName = nodePath.basename(sourceAbs);
|
||||
const targetAbs = nodePath.resolve(input.targetAgentImagesDir, dirName);
|
||||
// Sanity check (basename is structurally safe but resolve could
|
||||
// theoretically be a no-op for "" → keep the check, expected always-
|
||||
// true).
|
||||
if (targetAbs !== targetRootResolved && !targetAbs.startsWith(targetRootResolved + nodePath.sep)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "unsafe",
|
||||
detail: `agent_image target path for '${declared}' escapes target agent-images library; rejected`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (routedDirNames.has(dirName)) {
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "conflict",
|
||||
detail: `agent_image basename '${dirName}' collides with an earlier declared path; only the first is routed (banked collision-detection lesson)`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
fs.copyDir(sourceAbs, targetAbs);
|
||||
routedDirNames.add(dirName);
|
||||
records.push({
|
||||
declaredPath: declared,
|
||||
status: "routed",
|
||||
installedAt: targetAbs,
|
||||
});
|
||||
}
|
||||
|
||||
const routedCount = records.filter((r) => r.status === "routed").length;
|
||||
const rejectedCount = records.length - routedCount;
|
||||
return { records, routedCount, rejectedCount };
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { routeAgentImages, type AgentImagesRouterFsOps, type RouteAgentImagesInput } from "../src/domain/bundle-agent-images-router.js";
|
||||
|
||||
// Item 6 / slice-05 Checkpoint 7.3g step 2: bundle-agent-images-router
|
||||
// pure-function tests. PRD line 197: declared paths are agent-image
|
||||
// DIRECTORIES (not manifest paths — distinct from context_packs).
|
||||
// All 4 banked router-level catches (file-existence, isDirectory check
|
||||
// on manifest, declared-path-must-be-dir, basename collision) built in
|
||||
// from the first commit per banked discipline.
|
||||
|
||||
function mockFs(initial: { dirs?: string[]; files?: string[] } = {}): AgentImagesRouterFsOps & {
|
||||
_copyCalls: Array<{ src: string; dest: string }>;
|
||||
_mkdirpCalls: string[];
|
||||
_dirs: Set<string>;
|
||||
_files: Set<string>;
|
||||
} {
|
||||
const dirs = new Set<string>(initial.dirs ?? []);
|
||||
const files = new Set<string>(initial.files ?? []);
|
||||
const copyCalls: Array<{ src: string; dest: string }> = [];
|
||||
const mkdirpCalls: string[] = [];
|
||||
return {
|
||||
_copyCalls: copyCalls,
|
||||
_mkdirpCalls: mkdirpCalls,
|
||||
_dirs: dirs,
|
||||
_files: files,
|
||||
exists: (p: string) => dirs.has(p) || files.has(p),
|
||||
isDirectory: (p: string) => dirs.has(p),
|
||||
mkdirp: (p: string) => { mkdirpCalls.push(p); dirs.add(p); },
|
||||
copyDir: (src: string, dest: string) => { copyCalls.push({ src, dest }); dirs.add(dest); },
|
||||
};
|
||||
}
|
||||
|
||||
/** Convenience: build a complete agent-image fixture (image dir + manifest.yaml file inside). */
|
||||
function imageFixture(imageDir: string): { dirs: string[]; files: string[] } {
|
||||
return { dirs: [imageDir], files: [`${imageDir}/manifest.yaml`] };
|
||||
}
|
||||
|
||||
const BUNDLE_ROOT = "/bundle/root";
|
||||
const TARGET = "/operator/.openrig/agent-images";
|
||||
|
||||
function makeInput(overrides?: Partial<RouteAgentImagesInput>): RouteAgentImagesInput {
|
||||
return {
|
||||
bundleRoot: BUNDLE_ROOT,
|
||||
declaredAgentImages: [],
|
||||
targetAgentImagesDir: TARGET,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("routeAgentImages", () => {
|
||||
// A1: empty list → empty records + target dir mkdirp'd
|
||||
it("empty declaredAgentImages produces empty records but still mkdirp's target", () => {
|
||||
const fs = mockFs();
|
||||
const result = routeAgentImages(makeInput(), fs);
|
||||
expect(result.records).toEqual([]);
|
||||
expect(result.routedCount).toBe(0);
|
||||
expect(result.rejectedCount).toBe(0);
|
||||
expect(fs._mkdirpCalls).toContain(TARGET);
|
||||
});
|
||||
|
||||
// A2: routes one image: copyDir from sourceAbs (declared dir) to target/<basename>
|
||||
it("routes one agent_image: declared dir copied to target/<basename>", () => {
|
||||
const fs = mockFs(imageFixture(`${BUNDLE_ROOT}/agent-images/seat-a`));
|
||||
const result = routeAgentImages(
|
||||
makeInput({ declaredAgentImages: ["agent-images/seat-a"] }),
|
||||
fs,
|
||||
);
|
||||
expect(result.routedCount).toBe(1);
|
||||
expect(result.records[0]!.status).toBe("routed");
|
||||
expect(result.records[0]!.installedAt).toBe(`${TARGET}/seat-a`);
|
||||
expect(fs._copyCalls).toHaveLength(1);
|
||||
expect(fs._copyCalls[0]).toEqual({
|
||||
src: `${BUNDLE_ROOT}/agent-images/seat-a`,
|
||||
dest: `${TARGET}/seat-a`,
|
||||
});
|
||||
});
|
||||
|
||||
// A3: multiple distinct images route correctly
|
||||
it("routes multiple distinct agent_images each to target/<basename>", () => {
|
||||
const fs = mockFs({
|
||||
dirs: [`${BUNDLE_ROOT}/agent-images/seat-a`, `${BUNDLE_ROOT}/agent-images/seat-b`],
|
||||
files: [
|
||||
`${BUNDLE_ROOT}/agent-images/seat-a/manifest.yaml`,
|
||||
`${BUNDLE_ROOT}/agent-images/seat-b/manifest.yaml`,
|
||||
],
|
||||
});
|
||||
const result = routeAgentImages(
|
||||
makeInput({
|
||||
declaredAgentImages: [
|
||||
"agent-images/seat-a",
|
||||
"agent-images/seat-b",
|
||||
],
|
||||
}),
|
||||
fs,
|
||||
);
|
||||
expect(result.routedCount).toBe(2);
|
||||
expect(fs._copyCalls).toHaveLength(2);
|
||||
});
|
||||
|
||||
// A4: declared source dir absent → status=missing
|
||||
it("missing declared image dir → status=missing (honest-scoping)", () => {
|
||||
const fs = mockFs();
|
||||
const result = routeAgentImages(
|
||||
makeInput({ declaredAgentImages: ["agent-images/absent"] }),
|
||||
fs,
|
||||
);
|
||||
expect(result.routedCount).toBe(0);
|
||||
expect(result.records[0]!.status).toBe("missing");
|
||||
expect(result.records[0]!.detail).toContain("not present");
|
||||
expect(fs._copyCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
// A5: unsafe source path escaping bundle workspace rejected
|
||||
it("unsafe source path escaping bundle workspace → status=unsafe", () => {
|
||||
const fs = mockFs();
|
||||
const result = routeAgentImages(
|
||||
makeInput({ declaredAgentImages: ["../escape"] }),
|
||||
fs,
|
||||
);
|
||||
expect(result.records[0]!.status).toBe("unsafe");
|
||||
expect(result.records[0]!.detail).toContain("escapes bundle workspace");
|
||||
expect(fs._copyCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
// A6 (PRD-coherent discrimination): declared path exists but is a FILE
|
||||
// not a directory. PRD line 197 says declared path is an agent-image
|
||||
// DIRECTORY; if operator-declared path is a file, reject.
|
||||
it("declared path exists but is a file (not directory) → status=not_directory", () => {
|
||||
const fs = mockFs({ files: [`${BUNDLE_ROOT}/agent-images/seat-as-file`] });
|
||||
const result = routeAgentImages(
|
||||
makeInput({ declaredAgentImages: ["agent-images/seat-as-file"] }),
|
||||
fs,
|
||||
);
|
||||
expect(result.records[0]!.status).toBe("not_directory");
|
||||
expect(result.records[0]!.detail).toContain("not a directory");
|
||||
expect(fs._copyCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
// A7 (banked d491eca9 lesson): image dir exists but manifest.yaml inside
|
||||
// is absent. Consumer skips the image — false-positive routedCount class.
|
||||
it("image dir exists but manifest.yaml inside is absent → status=not_manifest", () => {
|
||||
const fs = mockFs({
|
||||
dirs: [`${BUNDLE_ROOT}/agent-images/halfimage`],
|
||||
// NO manifest.yaml file
|
||||
});
|
||||
const result = routeAgentImages(
|
||||
makeInput({ declaredAgentImages: ["agent-images/halfimage"] }),
|
||||
fs,
|
||||
);
|
||||
expect(result.records[0]!.status).toBe("not_manifest");
|
||||
expect(result.records[0]!.detail).toContain("missing manifest.yaml");
|
||||
expect(fs._copyCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
// A8 (banked 3cd581e3 lesson): image dir exists, manifest.yaml inside is
|
||||
// itself a DIRECTORY (not file). Consumer's readFileSync throws — false-
|
||||
// positive class. Reject.
|
||||
it("image dir exists but manifest.yaml inside is itself a directory → status=not_manifest", () => {
|
||||
const fs = mockFs({
|
||||
dirs: [
|
||||
`${BUNDLE_ROOT}/agent-images/dirmanifest`,
|
||||
`${BUNDLE_ROOT}/agent-images/dirmanifest/manifest.yaml`,
|
||||
],
|
||||
});
|
||||
const result = routeAgentImages(
|
||||
makeInput({ declaredAgentImages: ["agent-images/dirmanifest"] }),
|
||||
fs,
|
||||
);
|
||||
expect(result.records[0]!.status).toBe("not_manifest");
|
||||
expect(result.records[0]!.detail).toContain("directory");
|
||||
expect(fs._copyCalls).toHaveLength(0);
|
||||
});
|
||||
|
||||
// A9 (banked 16ebb8af lesson): two declared images sharing basename →
|
||||
// first wins (status=routed), second flagged status=conflict.
|
||||
it("two declared images sharing basename → first routed, second conflict (truthful routedCount)", () => {
|
||||
const fs = mockFs({
|
||||
dirs: [
|
||||
`${BUNDLE_ROOT}/a/seat-a`,
|
||||
`${BUNDLE_ROOT}/b/seat-a`,
|
||||
],
|
||||
files: [
|
||||
`${BUNDLE_ROOT}/a/seat-a/manifest.yaml`,
|
||||
`${BUNDLE_ROOT}/b/seat-a/manifest.yaml`,
|
||||
],
|
||||
});
|
||||
const result = routeAgentImages(
|
||||
makeInput({
|
||||
declaredAgentImages: ["a/seat-a", "b/seat-a"],
|
||||
}),
|
||||
fs,
|
||||
);
|
||||
expect(result.records).toHaveLength(2);
|
||||
expect(result.routedCount).toBe(1);
|
||||
expect(result.rejectedCount).toBe(1);
|
||||
expect(result.records[0]!.status).toBe("routed");
|
||||
expect(result.records[0]!.installedAt).toBe(`${TARGET}/seat-a`);
|
||||
expect(result.records[1]!.status).toBe("conflict");
|
||||
expect(result.records[1]!.detail).toContain("seat-a");
|
||||
expect(result.records[1]!.detail).toContain("collides");
|
||||
expect(fs._copyCalls).toHaveLength(1);
|
||||
expect(fs._copyCalls[0]!.src).toBe(`${BUNDLE_ROOT}/a/seat-a`);
|
||||
});
|
||||
|
||||
// A10: mixed list — routed + missing + unsafe + not_directory +
|
||||
// not_manifest + conflict aggregate
|
||||
it("mixed declared list aggregates correctly across all rejection classes", () => {
|
||||
const fs = mockFs({
|
||||
dirs: [
|
||||
`${BUNDLE_ROOT}/agent-images/ok`,
|
||||
`${BUNDLE_ROOT}/agent-images/halfimage`, // no manifest.yaml inside
|
||||
`${BUNDLE_ROOT}/agent-images/dup`,
|
||||
`${BUNDLE_ROOT}/elsewhere/dup`,
|
||||
],
|
||||
files: [
|
||||
`${BUNDLE_ROOT}/agent-images/ok/manifest.yaml`,
|
||||
`${BUNDLE_ROOT}/agent-images/seat-as-file`, // file at declared location
|
||||
`${BUNDLE_ROOT}/agent-images/dup/manifest.yaml`,
|
||||
`${BUNDLE_ROOT}/elsewhere/dup/manifest.yaml`,
|
||||
],
|
||||
});
|
||||
const result = routeAgentImages(
|
||||
makeInput({
|
||||
declaredAgentImages: [
|
||||
"agent-images/ok", // routed
|
||||
"agent-images/absent", // missing
|
||||
"../escape", // unsafe
|
||||
"agent-images/seat-as-file", // not_directory
|
||||
"agent-images/halfimage", // not_manifest (no manifest.yaml inside)
|
||||
"agent-images/dup", // routed (1st dup)
|
||||
"elsewhere/dup", // conflict (basename dup)
|
||||
],
|
||||
}),
|
||||
fs,
|
||||
);
|
||||
expect(result.records).toHaveLength(7);
|
||||
expect(result.routedCount).toBe(2);
|
||||
expect(result.rejectedCount).toBe(5);
|
||||
expect(result.records[0]!.status).toBe("routed");
|
||||
expect(result.records[1]!.status).toBe("missing");
|
||||
expect(result.records[2]!.status).toBe("unsafe");
|
||||
expect(result.records[3]!.status).toBe("not_directory");
|
||||
expect(result.records[4]!.status).toBe("not_manifest");
|
||||
expect(result.records[5]!.status).toBe("routed");
|
||||
expect(result.records[6]!.status).toBe("conflict");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user