import { test, expect, describe, beforeEach, afterEach } from "vitest"; import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { readCommandResult, type CommandResultCacheCall } from "../runtime/result-cache.ts"; import { COMMAND_RESULT_CACHE_SCHEMA_VERSION, setCachedCommandResult, type CommandResultCacheKey, } from "../cache/command-result-cache.ts"; import type { CacheLayer, CacheEntry } from "../cache/cache-layer.ts"; import type { KernelCommandDefinition, KernelExecutionReport, KernelResultCacheContext, } from "@warpgogol/werkstatt-shared/kernel"; /* RFC-1057: Unit tests for cache output-existence guard in readCommandResult (relocated from tryCacheRead by RFC-1133). Verifies that cached results are invalidated when declared output files are missing from disk, forcing re-execution of the generating command. Not a contract source of truth — the module under test owns the authoritative behavior; fixtures are minimal scaffolding only. Fixtures stay inline and minimal so each case states its own setup. RFC-1311: Compass header sweep — declares the cache suite's contract for test-file scaffold coverage. */ // @ai-invariant fixtures must not couple to internals beyond the public surface under test function makeCommand(overrides: Partial = {}): KernelCommandDefinition { return { name: "test.generator", modulePath: "test", description: "Test generator", scope: "workspace", execute: () => undefined, reads: ["src/input.txt"], writes: ["dist/output.txt"], ...overrides, }; } function makeReport(): KernelExecutionReport { return { commandName: "test.generator", exitCode: 0, ok: true, summary: "generated output", timing: { durationMs: 10, exceededTimeout: false }, filesModified: [], metadata: makeCommand(), logs: [], }; } function makeKey(overrides: Partial = {}): CommandResultCacheKey { return { schemaVersion: COMMAND_RESULT_CACHE_SCHEMA_VERSION, commandName: "test.generator", siteName: null, inputsHash: "aaa", moduleHash: "bbb", flagsHash: "", ...overrides, }; } function makeCall(command: KernelCommandDefinition, tmpDir: string): CommandResultCacheCall { return { command, argv: [], flags: {}, baseDir: tmpDir, workspaceRoot: tmpDir, siteName: null, force: false, dryRun: false, }; } function makeCacheCtx(cache: CacheLayer): KernelResultCacheContext { return { layer: cache, moduleHashCache: new Map() }; } class MockCacheLayer implements CacheLayer { readonly available = true; private entry: CacheEntry | null = null; async get(_namespace: string, _key: string): Promise { return this.entry; } async set( _namespace: string, _key: string, data: unknown, mtime: number, contentHash: string, ): Promise { this.entry = { key: _key, data, mtime, contentHash, updatedAt: Date.now() }; } async clear(): Promise { this.entry = null; } async status() { return { available: true, dbPath: ":memory:", dbSizeBytes: 0, namespaces: [], }; } async close(): Promise {} } describe("RFC-1057: readCommandResult output-existence guard", () => { let tmpDir: string; beforeEach(async () => { tmpDir = await mkdtemp(join(tmpdir(), "rfc1057-")); }); afterEach(async () => { await rm(tmpDir, { recursive: true, force: true }); }); test("(a) cache hit when output file exists", async () => { const cache = new MockCacheLayer(); const cmd = makeCommand(); const inputPath = join(tmpDir, "src", "input.txt"); const outputPath = join(tmpDir, "dist", "output.txt"); await mkdir(join(tmpDir, "src"), { recursive: true }); await mkdir(join(tmpDir, "dist"), { recursive: true }); await writeFile(inputPath, "test"); await writeFile(outputPath, "generated"); // Populate cache using the real setCachedCommandResult await setCachedCommandResult(cache, makeKey(), makeReport()); const result = await readCommandResult(makeCacheCtx(cache), makeCall(cmd, tmpDir)); expect(result).not.toBeNull(); expect(result?.ok).toBe(true); }); test("(b) cache miss when output file is missing", async () => { const cache = new MockCacheLayer(); const cmd = makeCommand(); const inputPath = join(tmpDir, "src", "input.txt"); await mkdir(join(tmpDir, "src"), { recursive: true }); await writeFile(inputPath, "test"); // Note: dist/output.txt is NOT created await setCachedCommandResult(cache, makeKey(), makeReport()); const result = await readCommandResult(makeCacheCtx(cache), makeCall(cmd, tmpDir)); expect(result).toBeNull(); }); test("(c) cache miss when one of multiple output files is missing", async () => { const cache = new MockCacheLayer(); const cmd = makeCommand({ writes: ["dist/output1.txt", "dist/output2.txt"], }); const inputPath = join(tmpDir, "src", "input.txt"); await mkdir(join(tmpDir, "src"), { recursive: true }); await mkdir(join(tmpDir, "dist"), { recursive: true }); await writeFile(inputPath, "test"); await writeFile(join(tmpDir, "dist", "output1.txt"), "generated"); // output2.txt is missing await setCachedCommandResult(cache, makeKey(), makeReport()); const result = await readCommandResult(makeCacheCtx(cache), makeCall(cmd, tmpDir)); expect(result).toBeNull(); }); test("(d) no writes field — cache hit regardless of file existence", async () => { const cache = new MockCacheLayer(); const cmd = makeCommand({ writes: undefined }); const inputPath = join(tmpDir, "src", "input.txt"); await mkdir(join(tmpDir, "src"), { recursive: true }); await writeFile(inputPath, "test"); await setCachedCommandResult(cache, makeKey(), makeReport()); const result = await readCommandResult(makeCacheCtx(cache), makeCall(cmd, tmpDir)); expect(result).not.toBeNull(); }); test("(e) empty writes array — cache hit", async () => { const cache = new MockCacheLayer(); const cmd = makeCommand({ writes: [] }); const inputPath = join(tmpDir, "src", "input.txt"); await mkdir(join(tmpDir, "src"), { recursive: true }); await writeFile(inputPath, "test"); await setCachedCommandResult(cache, makeKey(), makeReport()); const result = await readCommandResult(makeCacheCtx(cache), makeCall(cmd, tmpDir)); expect(result).not.toBeNull(); }); });