/*
Unit tests for site workspace resolver covering resolveSiteWorkspace; RFC-1025: cache clone fallback; discoverSiteWorkspaces.
- 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 site 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 { resolveSiteWorkspace, discoverSiteWorkspaces } from "../site-workspace-resolver.ts";
async function makeWorkspace(): Promise {
const testRoot = await fs.mkdtemp(path.join(os.tmpdir(), "resolver-test-"));
const dir = path.join(testRoot, "workspace");
await fs.mkdir(path.join(dir, "apps"), { recursive: true });
await fs.mkdir(path.join(dir, "missions"), { recursive: true });
await fs.mkdir(path.join(dir, "systems"), { recursive: true });
return dir;
}
async function writeApp(wsRoot: string, id: string): Promise {
const appDir = path.join(wsRoot, "apps", id);
await fs.mkdir(appDir, { recursive: true });
await fs.writeFile(
path.join(appDir, "package.json"),
JSON.stringify({ name: `@warpgogol/${id}` }),
);
}
async function writeWorkpiece(wsRoot: string, missionId: string, siteId: string): Promise {
const wpDir = path.join(wsRoot, "missions", missionId, "workpiece");
await fs.mkdir(wpDir, { recursive: true });
await fs.writeFile(
path.join(wpDir, "package.json"),
JSON.stringify({ name: `@warpgogol/${siteId}` }),
);
}
// Real cache clones carry system-config.yaml (written by writeSystemFiles)
// and tools/kernel.config.ts, but never package.json — the generated manifest
// is gitignored and exists only in materialized workpieces.
async function writeCacheCloneKernelConfig(wsRoot: string, siteId: string): Promise {
const cacheDir = path.join(wsRoot, "..", "systems-cache", siteId);
await fs.mkdir(path.join(cacheDir, "tools"), { recursive: true });
await fs.writeFile(path.join(cacheDir, "tools", "kernel.config.ts"), "export default {};\n");
}
async function writeSystemFiles(
wsRoot: string,
systems: { id: string; currentMission?: string }[],
): Promise {
const { stringify: stringifyYaml } = await import("yaml");
for (const sys of systems) {
const cacheDir = path.join(wsRoot, "..", "systems-cache", sys.id);
await fs.mkdir(cacheDir, { recursive: true });
const config = {
schemaVersion: "system-config/v1",
id: sys.id,
cosmicStar: "Vega",
mirrors: [{ path: `./systems/${sys.id}`, storageType: "non-bare" }],
pinnedPlatform: "4.5.0",
status: "active",
registeredAt: "2026-01-01T00:00:00Z",
notes: "",
};
await fs.writeFile(path.join(cacheDir, "system-config.yaml"), stringifyYaml(config) + "\n");
if (sys.currentMission) {
const state = {
schemaVersion: "system-state/v1",
systemId: sys.id,
currentMission: sys.currentMission,
};
await fs.writeFile(path.join(cacheDir, "system-state.yaml"), stringifyYaml(state) + "\n");
}
}
}
describe("resolveSiteWorkspace", () => {
test("resolves from apps/ when no registry entry", async () => {
const ws = await makeWorkspace();
await writeApp(ws, "demo");
const result = await resolveSiteWorkspace(ws, "demo");
expect(result.source).toBe("apps");
expect(result.name).toBe("demo");
expect(result.missionId).toBeNull();
});
test("resolves from mission workpiece when registry has currentMission", async () => {
const ws = await makeWorkspace();
await writeSystemFiles(ws, [{ id: "demo", currentMission: "demo-m000001" }]);
await writeWorkpiece(ws, "demo-m000001", "demo");
const result = await resolveSiteWorkspace(ws, "demo");
expect(result.source).toBe("mission");
expect(result.name).toBe("demo");
expect(result.missionId).toBe("demo-m000001");
});
test("prefers mission workpiece when both apps/ and workpiece exist with currentMission", async () => {
const ws = await makeWorkspace();
await writeSystemFiles(ws, [{ id: "demo", currentMission: "demo-m000001" }]);
await writeApp(ws, "demo");
await writeWorkpiece(ws, "demo-m000001", "demo");
const result = await resolveSiteWorkspace(ws, "demo");
expect(result.source).toBe("mission");
expect(result.name).toBe("demo");
expect(result.missionId).toBe("demo-m000001");
});
test("throws for unknown site id", async () => {
const ws = await makeWorkspace();
await writeApp(ws, "demo");
await expect(resolveSiteWorkspace(ws, "nope")).rejects.toThrow("Unknown site id");
});
});
describe("RFC-1025: cache clone fallback", () => {
test("AC-1: resolves cache clone when no mission and no apps/", async () => {
const ws = await makeWorkspace();
await writeSystemFiles(ws, [{ id: "demo" }]);
await writeCacheCloneKernelConfig(ws, "demo");
const result = await resolveSiteWorkspace(ws, "demo");
expect(result.source).toBe("cache-clone");
expect(result.name).toBe("demo");
expect(result.missionId).toBeNull();
});
test("AC-2: prefers mission workpiece over cache clone", async () => {
const ws = await makeWorkspace();
await writeSystemFiles(ws, [{ id: "demo", currentMission: "demo-m000001" }]);
await writeWorkpiece(ws, "demo-m000001", "demo");
await writeCacheCloneKernelConfig(ws, "demo");
const result = await resolveSiteWorkspace(ws, "demo");
expect(result.source).toBe("mission");
expect(result.missionId).toBe("demo-m000001");
});
test("AC-3: throws Unknown site id when no cache clone, no mission, no apps/", async () => {
// A stray systems-cache/ directory without system-config.yaml is not
// a cache clone — the marker file is what makes it resolvable.
const ws = await makeWorkspace();
await fs.mkdir(path.join(ws, "..", "systems-cache", "demo"), { recursive: true });
await expect(resolveSiteWorkspace(ws, "demo")).rejects.toThrow("Unknown site id");
});
test("resolves a real cache clone — system-config.yaml only, no package.json", async () => {
// Regression: the resolver previously required package.json, which is
// gitignored and absent from every real clone — the fallback was dead.
// system-config.yaml (already written by writeSystemFiles) is the marker.
const ws = await makeWorkspace();
await writeSystemFiles(ws, [{ id: "demo" }]);
const result = await resolveSiteWorkspace(ws, "demo");
expect(result.source).toBe("cache-clone");
expect(result.configPath).toBeUndefined();
expect(result.packageName).toBeUndefined();
});
test("AC-4: discoverSiteWorkspaces includes cache clone-resolvable sites", async () => {
const ws = await makeWorkspace();
await writeSystemFiles(ws, [{ id: "alpha" }]);
await writeCacheCloneKernelConfig(ws, "alpha");
const results = await discoverSiteWorkspaces(ws);
expect(results.length).toBe(1);
expect(results[0]!.source).toBe("cache-clone");
expect(results[0]!.name).toBe("alpha");
});
});
describe("discoverSiteWorkspaces", () => {
test("discovers apps/ sites", async () => {
const ws = await makeWorkspace();
await writeApp(ws, "alpha");
await writeApp(ws, "beta");
const results = await discoverSiteWorkspaces(ws);
expect(results.length).toBe(2);
expect(results[0]!.name).toBe("alpha");
expect(results[1]!.name).toBe("beta");
});
test("discovers mission workpieces from registry", async () => {
const ws = await makeWorkspace();
await writeSystemFiles(ws, [{ id: "gamma", currentMission: "gamma-m000001" }]);
await writeWorkpiece(ws, "gamma-m000001", "gamma");
const results = await discoverSiteWorkspaces(ws);
expect(results.length).toBe(1);
expect(results[0]!.source).toBe("mission");
});
test("prefers mission workpiece during discovery when currentMission is set", async () => {
const ws = await makeWorkspace();
await writeSystemFiles(ws, [{ id: "delta", currentMission: "delta-m000001" }]);
await writeApp(ws, "delta");
await writeWorkpiece(ws, "delta-m000001", "delta");
const results = await discoverSiteWorkspaces(ws);
expect(results.length).toBe(1);
expect(results[0]!.source).toBe("mission");
expect(results[0]!.missionId).toBe("delta-m000001");
});
test("cache clone outranks apps/ when workpiece exists but registry has no currentMission", async () => {
const ws = await makeWorkspace();
await writeSystemFiles(ws, [{ id: "delta" }]);
await writeApp(ws, "delta");
await writeWorkpiece(ws, "delta-m000001", "delta");
// Without currentMission, tryResolveMissionWorkpiece returns null, so the
// workpiece on disk is ignored. The cache clone (system-config.yaml
// written by writeSystemFiles) outranks apps/ — read-only mirrors have no
// dual-representation write ambiguity.
const results = await discoverSiteWorkspaces(ws);
expect(results.length).toBe(1);
expect(results[0]!.source).toBe("cache-clone");
});
test("returns empty array when no sites exist", async () => {
const ws = await makeWorkspace();
const results = await discoverSiteWorkspaces(ws);
expect(results).toEqual([]);
});
});