/* Unit tests for command result cache covering buildCommandResultCacheKey; parseCommandResultCacheKey; computeInputsHash. 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 command suite's contract for test-file scaffold coverage. */ // @ai-invariant fixtures must not couple to kernel internals beyond the public surface under test import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { test, expect, describe } from "vitest"; import { COMMAND_RESULT_CACHE_NAMESPACE, COMMAND_RESULT_CACHE_SCHEMA_VERSION, buildCommandResultCacheKey, computeInputsHash, computeModuleHash, getCachedCommandResult, parseCommandResultCacheKey, setCachedCommandResult, type CommandResultCacheKey, } from "../command-result-cache.ts"; import { NoopCacheLayer } from "../noop-cache-layer.ts"; import type { KernelCommandDefinition, KernelExecutionReport, } from "@warpgogol/werkstatt-shared/kernel"; import { byteHash, stableJsonHash } from "@warpgogol/werkstatt-shared/fingerprint"; import { buildWorkspaceTreeIndex } from "../workspace-tree-index.ts"; import { getOrComputeModuleHash, resolveModuleSrcDir } from "../../runtime/result-cache.ts"; function makeReport(overrides: Partial = {}): KernelExecutionReport { return { commandName: "test.command", exitCode: 0, ok: true, metadata: { name: "test.command", modulePath: "test", description: "test", scope: "workspace", execute: () => undefined, }, logs: [], timing: { durationMs: 10, exceededTimeout: false }, ...overrides, }; } function makeKey(overrides: Partial = {}): CommandResultCacheKey { return { schemaVersion: COMMAND_RESULT_CACHE_SCHEMA_VERSION, commandName: "test.command", siteName: null, inputsHash: "abc", moduleHash: "def", flagsHash: "", ...overrides, }; } describe("buildCommandResultCacheKey", () => { test("includes schema version, command name, site name, hashes", () => { const key1 = buildCommandResultCacheKey(makeKey({ inputsHash: "aaa" })); const key2 = buildCommandResultCacheKey(makeKey({ inputsHash: "bbb" })); expect(key1).not.toBe(key2); }); test("schema version bump changes key", () => { const key1 = buildCommandResultCacheKey(makeKey()); const key2 = buildCommandResultCacheKey( makeKey({ schemaVersion: COMMAND_RESULT_CACHE_SCHEMA_VERSION + 1 }), ); expect(key1).not.toBe(key2); }); test("site name affects key", () => { const key1 = buildCommandResultCacheKey(makeKey({ siteName: null })); const key2 = buildCommandResultCacheKey(makeKey({ siteName: "warpgogol-com" })); expect(key1).not.toBe(key2); }); }); describe("parseCommandResultCacheKey", () => { test("round-trips plain hashes", () => { const key = buildCommandResultCacheKey( makeKey({ siteName: "app", inputsHash: "abc", moduleHash: "def" }), ); expect(parseCommandResultCacheKey(COMMAND_RESULT_CACHE_NAMESPACE, key)).toEqual({ commandName: "test.command", siteName: "app", inputsHash: "abc", moduleHash: "def", flagsHash: "", }); }); test("round-trips algo-prefixed hashes (sha256:hex)", () => { // Real keys embed "sha256:" hashes — a naive split(":") truncates // them (inputsHash → "sha256", moduleHash → inputs hex). RFC-1127. const key = buildCommandResultCacheKey( makeKey({ siteName: "app", inputsHash: "sha256:aaa111", moduleHash: "sha256:bbb222", }), ); expect(parseCommandResultCacheKey(COMMAND_RESULT_CACHE_NAMESPACE, key)).toEqual({ commandName: "test.command", siteName: "app", inputsHash: "sha256:aaa111", moduleHash: "sha256:bbb222", flagsHash: "", }); }); test("handles empty siteName with prefixed hashes", () => { const key = buildCommandResultCacheKey( makeKey({ siteName: null, inputsHash: "sha256:aaa", moduleHash: "sha256:bbb" }), ); const parsed = parseCommandResultCacheKey(COMMAND_RESULT_CACHE_NAMESPACE, key); expect(parsed?.siteName).toBeNull(); expect(parsed?.moduleHash).toBe("sha256:bbb"); }); test("returns null for other namespaces and malformed keys", () => { expect(parseCommandResultCacheKey("other_ns", "1:cmd:site:a:b")).toBeNull(); expect(parseCommandResultCacheKey(COMMAND_RESULT_CACHE_NAMESPACE, "1:cmd")).toBeNull(); }); }); describe("computeInputsHash", () => { test("empty reads returns stable hash", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "cache-test-")); const h1 = await computeInputsHash([], ws, ws); const h2 = await computeInputsHash([], ws, ws); expect(h1.hash).toBe(h2.hash); }); test("hash changes when file content changes", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "cache-test-")); const filePath = path.join(ws, "input.md"); await fs.writeFile(filePath, "content v1"); const h1 = await computeInputsHash(["input.md"], ws, ws); await fs.writeFile(filePath, "content v2"); const h2 = await computeInputsHash(["input.md"], ws, ws); expect(h1.hash).not.toBe(h2.hash); }); test("hash is stable for unchanged content", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "cache-test-")); const filePath = path.join(ws, "input.md"); await fs.writeFile(filePath, "stable content"); const h1 = await computeInputsHash(["input.md"], ws, ws); const h2 = await computeInputsHash(["input.md"], ws, ws); expect(h1.hash).toBe(h2.hash); }); test("glob patterns match multiple files", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "cache-test-")); await fs.writeFile(path.join(ws, "a.md"), "a"); await fs.writeFile(path.join(ws, "b.md"), "b"); const h1 = await computeInputsHash(["*.md"], ws, ws); expect(h1.hash).toBeTruthy(); await fs.writeFile(path.join(ws, "c.md"), "c"); const h2 = await computeInputsHash(["*.md"], ws, ws); expect(h1.hash).not.toBe(h2.hash); }); }); describe("computeModuleHash", () => { test("directory hash changes when a file changes", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "mod-hash-")); await fs.writeFile(path.join(dir, "a.ts"), "export const x = 1;"); const h1 = await computeModuleHash(dir); await fs.writeFile(path.join(dir, "a.ts"), "export const x = 2;"); const h2 = await computeModuleHash(dir); expect(h1).not.toBe(h2); }); test("returns fallback for nonexistent directory", async () => { const h = await computeModuleHash(path.join(os.tmpdir(), "nonexistent-dir-xyz")); expect(h).toBeTruthy(); }); test("RFC-0637: with modulePaths hashes only listed paths", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "mod-hash-")); await fs.writeFile(path.join(dir, "a.ts"), "export const x = 1;"); await fs.writeFile(path.join(dir, "b.ts"), "export const y = 1;"); const h1 = await computeModuleHash(dir, ["a.ts"]); await fs.writeFile(path.join(dir, "b.ts"), "export const y = 2;"); const h2 = await computeModuleHash(dir, ["a.ts"]); expect(h1).toBe(h2); }); test("RFC-0637: without modulePaths hashes full src/ (existing behavior)", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "mod-hash-")); await fs.writeFile(path.join(dir, "a.ts"), "export const x = 1;"); await fs.writeFile(path.join(dir, "b.ts"), "export const y = 1;"); const h1 = await computeModuleHash(dir); await fs.writeFile(path.join(dir, "b.ts"), "export const y = 2;"); const h2 = await computeModuleHash(dir); expect(h1).not.toBe(h2); }); test("RFC-0637: different modulePaths values produce different hashes", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "mod-hash-")); await fs.writeFile(path.join(dir, "a.ts"), "export const x = 1;"); await fs.writeFile(path.join(dir, "b.ts"), "export const y = 1;"); const h1 = await computeModuleHash(dir, ["a.ts"]); const h2 = await computeModuleHash(dir, ["b.ts"]); expect(h1).not.toBe(h2); }); test("RFC-0637: non-existent path in modulePaths is silently skipped", async () => { const dir = await fs.mkdtemp(path.join(os.tmpdir(), "mod-hash-")); await fs.writeFile(path.join(dir, "a.ts"), "export const x = 1;"); const h = await computeModuleHash(dir, ["nonexistent.ts"]); expect(h).toBeTruthy(); }); }); describe("getCachedCommandResult / setCachedCommandResult", () => { test("miss returns null on noop cache", async () => { const cache = new NoopCacheLayer("/tmp/test.db", "test"); const result = await getCachedCommandResult(cache, makeKey()); expect(result).toBeNull(); }); test("noop cache does not store", async () => { const cache = new NoopCacheLayer("/tmp/test.db", "test"); await setCachedCommandResult(cache, makeKey(), makeReport()); const result = await getCachedCommandResult(cache, makeKey()); expect(result).toBeNull(); }); }); describe("RFC-0685: computeInputsHash with tree index", () => { test("tree index produces same hash as filesystem walk", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "cache-test-")); await fs.writeFile(path.join(ws, "a.md"), "content a"); await fs.writeFile(path.join(ws, "b.md"), "content b"); const treeIndex = await buildWorkspaceTreeIndex(ws); const withoutTree = await computeInputsHash(["*.md"], ws, ws); const withTree = await computeInputsHash(["*.md"], ws, ws, treeIndex); expect(withTree.hash).toBe(withoutTree.hash); }); test("tree index produces same metadata as filesystem walk", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "cache-test-")); await fs.writeFile(path.join(ws, "a.md"), "content a"); const treeIndex = await buildWorkspaceTreeIndex(ws); const withoutTree = await computeInputsHash(["a.md"], ws, ws); const withTree = await computeInputsHash(["a.md"], ws, ws, treeIndex); expect(withTree.metadata).toHaveLength(1); expect(withTree.metadata[0]!.path).toBe(withoutTree.metadata[0]!.path); expect(withTree.metadata[0]!.size).toBe(withoutTree.metadata[0]!.size); }); test("metadata entries have path, mtimeMs, and size", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "cache-test-")); await fs.writeFile(path.join(ws, "input.md"), "test content"); const result = await computeInputsHash(["input.md"], ws, ws); expect(result.metadata).toHaveLength(1); const entry = result.metadata[0]!; expect(entry.path).toBe("input.md"); expect(typeof entry.mtimeMs).toBe("number"); expect(entry.size).toBe(12); }); test("metadata is sorted by path", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "cache-test-")); await fs.writeFile(path.join(ws, "c.md"), "c"); await fs.writeFile(path.join(ws, "a.md"), "a"); await fs.writeFile(path.join(ws, "b.md"), "b"); const result = await computeInputsHash(["*.md"], ws, ws); expect(result.metadata.map((m) => m.path)).toEqual(["a.md", "b.md", "c.md"]); }); }); describe("RFC-0685: byte-mode fingerprint selection", () => { test("md files use byte mode (hash changes with content only)", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "cache-test-")); await fs.writeFile(path.join(ws, "doc.md"), "hello"); const h1 = await computeInputsHash(["doc.md"], ws, ws); await fs.writeFile(path.join(ws, "doc.md"), "world"); const h2 = await computeInputsHash(["doc.md"], ws, ws); expect(h1.hash).not.toBe(h2.hash); }); test("ts files use semantic mode (whitespace-insensitive)", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "cache-test-")); await fs.writeFile(path.join(ws, "mod.ts"), "export const x = 1;"); const h1 = await computeInputsHash(["mod.ts"], ws, ws); await fs.writeFile(path.join(ws, "mod.ts"), "export const x = 1;"); const h2 = await computeInputsHash(["mod.ts"], ws, ws); expect(h1.hash).toBe(h2.hash); }); }); describe("RFC-0685: getCachedCommandResult wrapper format", () => { test("returns null on noop cache", async () => { const cache = new NoopCacheLayer("/tmp/test.db", "test"); const result = await getCachedCommandResult(cache, makeKey()); expect(result).toBeNull(); }); test("noop cache does not store wrapper", async () => { const cache = new NoopCacheLayer("/tmp/test.db", "test"); const metadata = [{ path: "input.md", mtimeMs: Date.now(), size: 100 }]; await setCachedCommandResult(cache, makeKey(), makeReport(), metadata); const result = await getCachedCommandResult(cache, makeKey()); expect(result).toBeNull(); }); }); describe("COMMAND_RESULT_CACHE_NAMESPACE", () => { test("is command_results", () => { expect(COMMAND_RESULT_CACHE_NAMESPACE).toBe("command_results"); }); }); describe("RFC-1193: bounded module hashing", () => { const makeCmd = (modulePath?: string, modulePaths?: string[]): KernelCommandDefinition => ({ name: "test.cacheable", modulePath, modulePaths, reads: ["docs/**/*.md"], execute: () => undefined, }) as unknown as KernelCommandDefinition; test("resolveModuleSrcDir returns unpatched when modulePath is absent", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-src-")); const resolved = await resolveModuleSrcDir(makeCmd(), ws); expect(resolved).toEqual({ fallbackReason: "unpatched" }); }); test("resolveModuleSrcDir returns unrooted when modulePath has no /src/ or /os/", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-src-")); const resolved = await resolveModuleSrcDir(makeCmd("packages/x/kernel/mod.ts"), ws); expect(resolved).toEqual({ fallbackReason: "unrooted" }); }); test("resolveModuleSrcDir returns missing when derived dir does not exist", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-src-")); const resolved = await resolveModuleSrcDir(makeCmd("packages/x/src/m.ts"), ws); expect(resolved).toEqual({ fallbackReason: "missing" }); }); test("resolveModuleSrcDir resolves /os/ roots to a real directory", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-src-")); await fs.mkdir(path.join(ws, "packages/forge/os"), { recursive: true }); const resolved = await resolveModuleSrcDir(makeCmd("packages/forge/os/rfc/rfc.module.ts"), ws); expect(resolved.srcDir).toBe(path.join(ws, "packages/forge/os")); }); test("resolveModuleSrcDir resolves /src/ roots to a real directory", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-src-")); await fs.mkdir(path.join(ws, "packages/x/src"), { recursive: true }); const resolved = await resolveModuleSrcDir(makeCmd("packages/x/src/m.ts"), ws); expect(resolved.srcDir).toBe(path.join(ws, "packages/x/src")); }); test("getOrComputeModuleHash returns deterministic constant for fallbacks, not workspaceRoot", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-src-")); const cmd = makeCmd(); const cache = new Map(); const resolved = await resolveModuleSrcDir(cmd, ws); const h = await getOrComputeModuleHash(resolved, cmd, cache, ws); expect(h).toBe(byteHash("module-hash-fallback:unpatched")); // Repeat calls hit the per-run cache and stay identical. const h2 = await getOrComputeModuleHash(resolved, cmd, cache, ws); expect(h2).toBe(h); }); test("getOrComputeModuleHash fingerprints srcDir when resolvable", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-src-")); const srcDir = path.join(ws, "packages/x/src"); await fs.mkdir(srcDir, { recursive: true }); const cmd = makeCmd("packages/x/src/m.ts"); const cache = new Map(); const h = await getOrComputeModuleHash({ srcDir }, cmd, cache, ws); expect(h).toBeTruthy(); expect(h).not.toBe(byteHash("module-hash-fallback:missing")); }); }); describe("RFC-1194: modulePaths anchored to dirname(modulePath)", () => { const makeCmd = (modulePath: string, modulePaths: string[]): KernelCommandDefinition => ({ name: "test.cacheable", modulePath, modulePaths, reads: ["docs/**/*.md"], execute: () => undefined, }) as unknown as KernelCommandDefinition; test("AC-1: entry existing only under dirname(modulePath) changes the hash", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-anchor-")); const modDir = path.join(ws, "packages/x/src/checks"); await fs.mkdir(modDir, { recursive: true }); await fs.writeFile(path.join(modDir, "check.ts"), "export const a = 1;"); const cmd = makeCmd("packages/x/src/checks/mod.ts", ["check.ts"]); const cache = new Map(); const resolved = await resolveModuleSrcDir(cmd, ws); const h1 = await getOrComputeModuleHash(resolved, cmd, cache, ws); await fs.writeFile(path.join(modDir, "check.ts"), "export const a = 2;"); const cache2 = new Map(); const h2 = await getOrComputeModuleHash(resolved, cmd, cache2, ws); expect(h1).not.toBe(h2); expect(h1).not.toBe(byteHash("module-hash-fallback:missing")); }); test("AC-6: entry existing only under src/ root does not contribute", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-anchor-")); const modDir = path.join(ws, "packages/x/src/checks"); await fs.mkdir(modDir, { recursive: true }); // "lonely.ts" exists under src/ but NOT under src/checks/. await fs.writeFile(path.join(ws, "packages/x/src/lonely.ts"), "export const l = 1;"); const cmd = makeCmd("packages/x/src/checks/mod.ts", ["lonely.ts"]); const cache = new Map(); const resolved = await resolveModuleSrcDir(cmd, ws); const h = await getOrComputeModuleHash(resolved, cmd, cache, ws); // The entry resolves to nothing under dirname(modulePath) — silently // skipped at runtime (CRC-05 gates it statically), so the hash equals the // empty-allowlist constant. expect(h).toBe(stableJsonHash({ paths: [] })); }); test(".. entry resolving within workspaceRoot contributes to the hash", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-anchor-")); const modDir = path.join(ws, "packages/x/src/testing/contract"); await fs.mkdir(modDir, { recursive: true }); const helperDir = path.join(ws, "packages/x/src/checks"); await fs.mkdir(helperDir, { recursive: true }); await fs.writeFile(path.join(helperDir, "result-helpers.ts"), "export const h = 1;"); const cmd = makeCmd("packages/x/src/testing/contract/mod.ts", [ "../../checks/result-helpers.ts", ]); const cache = new Map(); const resolved = await resolveModuleSrcDir(cmd, ws); const h1 = await getOrComputeModuleHash(resolved, cmd, cache, ws); await fs.writeFile(path.join(helperDir, "result-helpers.ts"), "export const h = 2;"); const h2 = await getOrComputeModuleHash(resolved, cmd, new Map(), ws); expect(h1).not.toBe(h2); }); test(".. entry escaping workspaceRoot is never fingerprinted", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-anchor-")); const modDir = path.join(ws, "packages/x/src/checks"); await fs.mkdir(modDir, { recursive: true }); const cmd = makeCmd("packages/x/src/checks/mod.ts", ["../../../outside.ts"]); const cache = new Map(); const resolved = await resolveModuleSrcDir(cmd, ws); const h = await getOrComputeModuleHash(resolved, cmd, cache, ws); expect(h).toBe(stableJsonHash({ paths: [] })); }); test("unrooted modulePath with modulePaths still anchors to its dirname", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-anchor-")); const modDir = path.join(ws, "packages/x/kernel"); await fs.mkdir(modDir, { recursive: true }); await fs.writeFile(path.join(modDir, "helper.ts"), "export const k = 1;"); const cmd = makeCmd("packages/x/kernel/mod.ts", ["helper.ts"]); const cache = new Map(); const resolved = await resolveModuleSrcDir(cmd, ws); expect(resolved).toEqual({ fallbackReason: "unrooted" }); const h = await getOrComputeModuleHash(resolved, cmd, cache, ws); expect(h).not.toBe(byteHash("module-hash-fallback:unrooted")); }); test("modulePaths with missing dirname falls back to constant missing hash", async () => { const ws = await fs.mkdtemp(path.join(os.tmpdir(), "mod-anchor-")); const cmd = makeCmd("packages/x/src/checks/mod.ts", ["a.ts"]); const cache = new Map(); const resolved = await resolveModuleSrcDir(cmd, ws); const h = await getOrComputeModuleHash(resolved, cmd, cache, ws); expect(h).toBe(byteHash("module-hash-fallback:missing")); }); }); describe("COMMAND_RESULT_CACHE_SCHEMA_VERSION", () => { test("is 2 (RFC-1133: flagsHash appended to the key)", () => { expect(COMMAND_RESULT_CACHE_SCHEMA_VERSION).toBe(2); }); });