/* Unit tests for validate postbuild covering its documented public contract and regression-sensitive edge cases. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. RFC-0883: initial unit tests for runValidatePostbuild. RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared */ import { test, expect, beforeEach, afterEach, vi } from "vitest"; import fs from "node:fs/promises"; import path from "node:path"; import os from "node:os"; vi.mock("@warpgogol/werkstatt-engine/kernel", async (importOriginal) => { const original = await importOriginal(); return { ...original, executeKernelPipeline: vi.fn(), executeKernelCommand: vi.fn(), loadAppRuntime: vi.fn(), resolveSiteWorkspace: vi.fn(), }; }); vi.mock("./mission-io.ts", () => ({ readMissionManifest: vi.fn(), resolveMissionDir: vi.fn(), })); import { runValidatePostbuild } from "./validate-postbuild.ts"; import { executeKernelPipeline, executeKernelCommand, loadAppRuntime, resolveSiteWorkspace, } from "@warpgogol/werkstatt-engine/kernel"; import { readMissionManifest, resolveMissionDir } from "./mission-io.ts"; import { createDefaultIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; const mockExecuteKernelPipeline = vi.mocked(executeKernelPipeline); const mockExecuteKernelCommand = vi.mocked(executeKernelCommand); const mockLoadAppRuntime = vi.mocked(loadAppRuntime); const mockResolveSiteWorkspace = vi.mocked(resolveSiteWorkspace); const mockReadMissionManifest = vi.mocked(readMissionManifest); const mockResolveMissionDir = vi.mocked(resolveMissionDir); let tmpDir: string; beforeEach(async () => { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "test-validate-postbuild-")); mockExecuteKernelPipeline.mockReset(); mockExecuteKernelCommand.mockReset(); mockLoadAppRuntime.mockReset(); mockResolveSiteWorkspace.mockReset(); mockReadMissionManifest.mockReset(); mockResolveMissionDir.mockReset(); }); afterEach(async () => { await fs.rm(tmpDir, { recursive: true, force: true }); }); function makeContext(workspaceRoot: string) { const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), section: vi.fn(), getEvents: vi.fn(() => []), }; return { io: createDefaultIO().io, workspaceRoot, logger, } as never; } function makePipelineReport( ok: boolean, steps: Array<{ commandName: string; ok: boolean; exitCode: number; durationMs: number }>, ) { return { pipelineName: "sites-check.postbuild", exitCode: ok ? 0 : 1, ok, steps: steps.map((s) => ({ commandName: s.commandName, ok: s.ok, exitCode: s.exitCode, timing: { durationMs: s.durationMs, exceededTimeout: false }, })), timing: { pipeline: "sites-check.postbuild", totalDurationMs: steps.reduce((sum, s) => sum + s.durationMs, 0), stepCount: steps.length, slowestSteps: [], timeoutCount: 0, warningCount: 0, }, } as never; } test("dist/ missing → exit code 1 with error message", async () => { mockResolveSiteWorkspace.mockResolvedValue({ name: "test-bundle", directory: tmpDir, toolsDirectory: path.join(tmpDir, "tools"), configPath: path.join(tmpDir, "tools", "kernel.config.ts"), } as never); const result = await runValidatePostbuild( { flags: { site: "test-bundle" } } as never, makeContext(tmpDir), ); expect(result.exitCode).toBe(1); expect(result.summary).toContain("No dist/ found"); expect(result.data?.status).toBe("fail"); expect(result.data?.steps).toEqual([]); }); test("dist/ exists → runs all validators via executeKernelPipeline", async () => { await fs.mkdir(path.join(tmpDir, "dist"), { recursive: true }); mockResolveSiteWorkspace.mockResolvedValue({ name: "test-bundle", directory: tmpDir, toolsDirectory: path.join(tmpDir, "tools"), configPath: path.join(tmpDir, "tools", "kernel.config.ts"), } as never); mockExecuteKernelPipeline.mockResolvedValue( makePipelineReport(true, [ { commandName: "seo.technical.validate", ok: true, exitCode: 0, durationMs: 100 }, { commandName: "image.delivery.validate", ok: true, exitCode: 0, durationMs: 200 }, ]), ); const result = await runValidatePostbuild( { flags: { site: "test-bundle" } } as never, makeContext(tmpDir), ); expect(result.exitCode).toBe(0); expect(result.data?.status).toBe("pass"); expect(result.data?.steps).toHaveLength(2); expect(result.data?.steps[0]!.name).toBe("seo.technical.validate"); expect(result.data?.steps[0]!.status).toBe("ok"); expect(mockExecuteKernelPipeline).toHaveBeenCalledOnce(); }); test("dist/ exists with --skip-slow → slow steps skipped, fast steps run individually", async () => { await fs.mkdir(path.join(tmpDir, "dist"), { recursive: true }); mockResolveSiteWorkspace.mockResolvedValue({ name: "test-bundle", directory: tmpDir, toolsDirectory: path.join(tmpDir, "tools"), configPath: path.join(tmpDir, "tools", "kernel.config.ts"), } as never); const pipelineSteps = [ { command: "seo.technical.validate" }, { command: "image.delivery.validate" }, { command: "mobile.layout.check" }, { command: "lighthouse.budget.check" }, { command: "qa.independent.run" }, ]; mockLoadAppRuntime.mockResolvedValue({ registry: { pipelines: new Map([ ["sites-check.postbuild", { name: "sites-check.postbuild", steps: pipelineSteps }], ]), }, } as never); mockExecuteKernelCommand.mockResolvedValue({ commandName: "seo.technical.validate", ok: true, exitCode: 0, timing: { durationMs: 50, exceededTimeout: false }, } as never); const result = await runValidatePostbuild( { flags: { site: "test-bundle", "skip-slow": true } } as never, makeContext(tmpDir), ); expect(result.exitCode).toBe(0); expect(result.data?.status).toBe("pass"); const stepNames = result.data?.steps.map((s) => s.name); expect(stepNames).toContain("seo.technical.validate"); expect(stepNames).toContain("image.delivery.validate"); expect(stepNames).toContain("mobile.layout.check"); expect(stepNames).toContain("lighthouse.budget.check"); expect(stepNames).toContain("qa.independent.run"); const slowSteps = result.data?.steps.filter((s) => s.status === "skip"); expect(slowSteps).toHaveLength(3); expect(slowSteps?.map((s) => s.name).sort()).toEqual( ["lighthouse.budget.check", "mobile.layout.check", "qa.independent.run"].sort(), ); expect(mockExecuteKernelPipeline).not.toHaveBeenCalled(); expect(mockExecuteKernelCommand).toHaveBeenCalledTimes(2); }); test("--mission resolves workpiece via readMissionManifest", async () => { const missionDir = path.join(tmpDir, "missions", "test-bundle-m000001"); const workpieceDir = path.join(missionDir, "workpiece"); await fs.mkdir(path.join(workpieceDir, "dist"), { recursive: true }); mockReadMissionManifest.mockResolvedValue({ missionId: "test-bundle-m000001", systemId: "test-bundle", brief: "test", status: "materialized", } as never); mockResolveMissionDir.mockResolvedValue(missionDir); mockExecuteKernelPipeline.mockResolvedValue( makePipelineReport(true, [ { commandName: "seo.technical.validate", ok: true, exitCode: 0, durationMs: 100 }, ]), ); const result = await runValidatePostbuild( { flags: { mission: "test-bundle-m000001" } } as never, makeContext(tmpDir), ); expect(result.exitCode).toBe(0); expect(result.data?.status).toBe("pass"); expect(mockReadMissionManifest).toHaveBeenCalledWith( tmpDir, "test-bundle-m000001", expect.anything(), ); expect(mockResolveMissionDir).toHaveBeenCalledWith( tmpDir, "test-bundle-m000001", expect.anything(), ); const pipelineCall = mockExecuteKernelPipeline.mock.calls[0]![0] as { siteWorkspace: { directory: string }; siteName: string; }; expect(pipelineCall.siteWorkspace.directory).toBe(workpieceDir); expect(pipelineCall.siteName).toBe("test-bundle"); }); test("stale dist/ warning is always printed", async () => { await fs.mkdir(path.join(tmpDir, "dist"), { recursive: true }); mockResolveSiteWorkspace.mockResolvedValue({ name: "test-bundle", directory: tmpDir, toolsDirectory: path.join(tmpDir, "tools"), configPath: path.join(tmpDir, "tools", "kernel.config.ts"), } as never); mockExecuteKernelPipeline.mockResolvedValue( makePipelineReport(true, [ { commandName: "seo.technical.validate", ok: true, exitCode: 0, durationMs: 100 }, ]), ); const warnSpy = vi.fn(); const ctx = { io: createDefaultIO().io, workspaceRoot: tmpDir, logger: { info: vi.fn(), warn: warnSpy, error: vi.fn(), section: vi.fn(), getEvents: vi.fn(() => []), }, } as never; await runValidatePostbuild({ flags: { site: "test-bundle" } } as never, ctx); expect(warnSpy).toHaveBeenCalledWith( "dist/ may be stale — run mission.validate for a full check.", ); }); test("no --mission or --site → throws error", async () => { await expect(runValidatePostbuild({ flags: {} } as never, makeContext(tmpDir))).rejects.toThrow( "--mission or --site is required", ); }); test("step failure → exit code 1 with fail status", async () => { await fs.mkdir(path.join(tmpDir, "dist"), { recursive: true }); mockResolveSiteWorkspace.mockResolvedValue({ name: "test-bundle", directory: tmpDir, toolsDirectory: path.join(tmpDir, "tools"), configPath: path.join(tmpDir, "tools", "kernel.config.ts"), } as never); mockExecuteKernelPipeline.mockResolvedValue( makePipelineReport(false, [ { commandName: "seo.technical.validate", ok: true, exitCode: 0, durationMs: 100 }, { commandName: "image.delivery.validate", ok: false, exitCode: 1, durationMs: 200 }, ]), ); const result = await runValidatePostbuild( { flags: { site: "test-bundle" } } as never, makeContext(tmpDir), ); expect(result.exitCode).toBe(1); expect(result.data?.status).toBe("fail"); expect(result.data?.steps[1]!.status).toBe("fail"); });