import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; import { clearArtifactSchemaCacheForTests, loadSchema, validateArtifact, } from "./artifact-schema.js"; import { DevflowError } from "./errors.js"; const artifactRoot = resolve( dirname(fileURLToPath(import.meta.url)), "../../../docs/schemas/artifacts", ); const repoRoot = resolve(artifactRoot, "../../.."); const hash64 = "a".repeat(64); const runId = "00000000-0000-4000-8000-000000000001"; const originalCwd = process.cwd(); describe("artifact schema registry", () => { afterEach(() => { process.chdir(originalCwd); clearArtifactSchemaCacheForTests(); }); it("loads the first locked artifact schemas from docs/schemas/artifacts", () => { clearArtifactSchemaCacheForTests(); expect(loadSchema("dev/spec@1", { root: artifactRoot })).toMatchObject({ $id: "dev/spec@1", }); expect(loadSchema("dev/phase-plan@1", { root: artifactRoot })).toMatchObject({ $id: "dev/phase-plan@1", }); expect(loadSchema("common/final-report@1", { root: artifactRoot })).toMatchObject({ $id: "common/final-report@1", }); expect(Object.isFrozen(loadSchema("dev/spec@1", { root: artifactRoot }))).toBe(true); }); it("finds the default schema root from package subdirectories", () => { process.chdir(resolve(repoRoot, "packages/core")); expect(loadSchema("dev/spec@1")).toMatchObject({ $id: "dev/spec@1" }); }); it("validates dev/spec@1 artifacts and returns compact validation errors", () => { expect( validateArtifact( "dev/spec@1", { summary: "Add a binding algorithm", requirements: [{ id: "REQ-1", description: "Bind every role" }], acceptanceCriteria: ["All roles have bindings"], risks: [], }, { root: artifactRoot }, ), ).toEqual({ ok: true }); const result = validateArtifact( "dev/spec@1", { summary: "Missing requirements", requirements: [], acceptanceCriteria: [], risks: [], }, { root: artifactRoot }, ); expect(result.ok).toBe(false); if (!result.ok) { expect(result.errors.map((error) => error.keyword)).toContain("minItems"); expect(result.errors[0]).toMatchObject({ instancePath: expect.any(String), schemaPath: expect.any(String), params: expect.any(Object), }); } }); it("validates dev/phase-plan@1 artifacts", () => { expect( validateArtifact( "dev/phase-plan@1", { phases: [ { key: "implement", title: "Implement", objective: "Implement the requested behavior", roles: ["implementer"], expectedArtifact: { path: "artifacts/spec.json", schema: "dev/spec@1", }, tasks: [ { id: "task-1", title: "Edit code", role: "implementer", writeSet: ["packages/core/src/**", "**/*.ts"], }, ], }, ], }, { root: artifactRoot }, ), ).toEqual({ ok: true }); const invalidSchemaId = validateArtifact( "dev/phase-plan@1", { phases: [ { key: "implement", title: "Implement", objective: "Implement the requested behavior", roles: ["implementer"], expectedArtifact: { path: "artifacts/spec.json", schema: "../secret@1", }, }, ], }, { root: artifactRoot }, ); expect(invalidSchemaId.ok).toBe(false); if (!invalidSchemaId.ok) { expect(invalidSchemaId.errors.map((error) => error.keyword)).toContain("pattern"); } for (const path of [ "../../secrets.json", "/etc/passwd", "artifacts/../outside.json", "C:/outside.json", "ok\n/../../outside.json", "ok\r/../../outside.json", ]) { const invalidPath = validateArtifact( "dev/phase-plan@1", { phases: [ { key: "implement", title: "Implement", objective: "Implement the requested behavior", roles: ["implementer"], expectedArtifact: { path, schema: "dev/spec@1", }, }, ], }, { root: artifactRoot }, ); expect(invalidPath.ok).toBe(false); if (!invalidPath.ok) { expect(invalidPath.errors.map((error) => error.keyword)).toContain("pattern"); } } const missingWriteSet = validateArtifact( "dev/phase-plan@1", { phases: [ { key: "implement", title: "Implement", objective: "Implement the requested behavior", roles: ["implementer"], tasks: [ { id: "task-1", title: "Edit code", role: "implementer", }, ], }, ], }, { root: artifactRoot }, ); expect(missingWriteSet.ok).toBe(false); if (!missingWriteSet.ok) { expect(missingWriteSet.errors.map((error) => error.keyword)).toContain("required"); } for (const writeSet of [ "../../**", "/etc/**", "src/../secrets/**", "C:/outside/**", "src\n/../../outside", "src\r/../../outside", "..\\outside\\**", "!/etc/**", "!../*", "{../*,packages/core/src/**}", "{..,packages}/**", "@(../*)", "!(../*)", "src/[ab]/**", ]) { const invalidWriteSet = validateArtifact( "dev/phase-plan@1", { phases: [ { key: "implement", title: "Implement", objective: "Implement the requested behavior", roles: ["implementer"], tasks: [ { id: "task-1", title: "Edit code", role: "implementer", writeSet: [writeSet], }, ], }, ], }, { root: artifactRoot }, ); expect(invalidWriteSet.ok).toBe(false); if (!invalidWriteSet.ok) { expect(invalidWriteSet.errors.map((error) => error.keyword)).toContain("pattern"); } } }); it("validates common/final-report@1 minimum fields", () => { expect( validateArtifact( "common/final-report@1", { runId, templateHash: hash64, bindings: [{ roleId: "implementer", personaHash: hash64, backend: "fake" }], inputs: {}, phases: [], approvals: [], findings: [], commands: [{ kind: "test", argv: ["pnpm", "test"], exit_code: 0 }], artifacts: [], events: { tail: [] }, unresolved: [], endedAt: "2026-05-09T00:00:00.000Z", status: "completed", }, { root: artifactRoot }, ), ).toEqual({ ok: true }); const result = validateArtifact( "common/final-report@1", { runId: "not-a-uuid", templateHash: hash64, bindings: [{ roleId: "implementer", personaHash: hash64, backend: "fake" }], inputs: {}, phases: [], approvals: [], findings: [], commands: [], artifacts: [], events: { tail: [] }, unresolved: [], endedAt: "2026-99-99T99:99:99Z", status: "executing", }, { root: artifactRoot }, ); expect(result.ok).toBe(false); if (!result.ok) { expect(result.errors.map((error) => error.keyword)).toEqual( expect.arrayContaining(["format", "enum"]), ); } }); it("fails fatally for unknown or malformed schema ids", () => { expect(() => loadSchema("dev/unknown@1", { root: artifactRoot })).toThrow(DevflowError); expect(() => loadSchema("../secret@1", { root: artifactRoot })).toThrow( /artifact_schema_unknown/, ); }); it("fails fatally when schema files are malformed", () => { const root = mkdtempSync(join(tmpdir(), "devflow-artifact-schemas-")); const devDir = join(root, "dev"); mkdirSync(devDir, { recursive: true }); writeFileSync( join(devDir, "bad@1.json"), JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", $id: "dev/wrong@1", type: "object", }), ); expect(() => loadSchema("dev/bad@1", { root })).toThrow(/artifact_schema_load_failed/); }); it("wraps registry root, JSON parse, and path layout load failures", () => { expect(() => loadSchema("dev/spec@1", { root: join(tmpdir(), "missing-artifact-root") }), ).toThrow(/artifact_schema_load_failed/); const badJsonRoot = mkdtempSync(join(tmpdir(), "devflow-artifact-schemas-")); mkdirSync(join(badJsonRoot, "dev"), { recursive: true }); writeFileSync(join(badJsonRoot, "dev", "bad@1.json"), "{"); expect(() => loadSchema("dev/bad@1", { root: badJsonRoot })).toThrow( /artifact_schema_load_failed/, ); const badPathRoot = mkdtempSync(join(tmpdir(), "devflow-artifact-schemas-")); mkdirSync(join(badPathRoot, "bad"), { recursive: true }); writeFileSync( join(badPathRoot, "bad", "schema.json"), JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", $id: "bad/schema", type: "object", }), ); expect(() => loadSchema("dev/missing@1", { root: badPathRoot })).toThrow( /artifact_schema_load_failed/, ); }); it("does not load schemas from a target-controlled cwd shadow root", () => { const shadowRoot = mkdtempSync(join(tmpdir(), "devflow-shadow-schemas-")); const shadowSchemaDir = join(shadowRoot, "docs", "schemas", "artifacts", "dev"); mkdirSync(shadowSchemaDir, { recursive: true }); writeFileSync(join(shadowRoot, "package.json"), JSON.stringify({ name: "devflow" })); writeFileSync( join(shadowSchemaDir, "spec@1.json"), JSON.stringify({ $schema: "https://json-schema.org/draft/2020-12/schema", $id: "dev/spec@1", title: "SHADOW", type: "object", }), ); process.chdir(shadowRoot); expect(loadSchema("dev/spec@1")).toMatchObject({ title: "Devflow Development Specification" }); }); });