test(FN-4912): complete Step 9 — add env materialization verification coverage

Fusion-Task-Id: FN-4912
Fusion-Task-Lineage: 943d0651-052a-41b5-8069-4c60f4db1ba7
This commit is contained in:
Fusion (runfusion.ai)
2026-05-17 23:19:55 -07:00
committed by gsxdsm
parent ffd4d83c09
commit 48caf28148
7 changed files with 418 additions and 3 deletions

View File

@@ -0,0 +1,56 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it, vi } from "vitest";
import { writeSecretsEnvFile } from "../../secrets-env-writer.js";
import { reapOrphanWorktrees } from "../../worktree-pool.js";
const dirs: string[] = [];
function tmpRepo(): string {
const root = mkdtempSync(join(tmpdir(), "secrets-rel-"));
dirs.push(root);
execFileSync("git", ["init"], { cwd: root });
return root;
}
afterEach(async () => {
await Promise.all(dirs.splice(0).map((d) => rm(d, { recursive: true, force: true })));
});
describe("reliability interactions: secrets env materialization", () => {
it("writer refuses non-ignored env path", async () => {
const root = tmpRepo();
const worktree = join(root, ".worktrees", "a");
mkdirSync(worktree, { recursive: true });
execFileSync("git", ["init"], { cwd: worktree });
const audit = { filesystem: vi.fn() };
const result = await writeSecretsEnvFile({
rootDir: root,
worktreePath: worktree,
taskId: "FN-1",
settings: { secretsEnv: { enabled: true, filename: ".env", requireGitignored: true } },
worktreeSource: "fresh",
audit,
secretsStore: { listEnvExportable: vi.fn().mockResolvedValue([{ id: "1", key: "A", exportKey: "ALPHA", scope: "project", plaintextValue: "v" }]) } as any,
});
expect(result.reason).toBe("not-gitignored");
expect(audit.filesystem).toHaveBeenCalledWith(expect.objectContaining({ type: "secret:env-write-skipped" }));
});
it("orphan reap reclaims orphaned env artifacts", async () => {
const root = tmpRepo();
const worktreesDir = join(root, ".worktrees");
const orphan = join(worktreesDir, "ghost");
mkdirSync(orphan, { recursive: true });
writeFileSync(join(orphan, ".env"), "A=1\n");
writeFileSync(join(orphan, ".fusion-secrets-env.fingerprint"), "abc\n.env\n");
const removed = await reapOrphanWorktrees(root);
expect(removed).toBe(1);
expect(existsSync(orphan)).toBe(false);
});
});

View File

@@ -0,0 +1,192 @@
import { mkdtempSync, readFileSync, statSync, symlinkSync, writeFileSync, existsSync } from "node:fs";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { describe, it, expect, vi, afterEach } from "vitest";
import { cleanupSecretsEnvFile, writeSecretsEnvFile } from "../secrets-env-writer.js";
const dirs: string[] = [];
function tmpWorktree(): string {
const dir = mkdtempSync(join(tmpdir(), "secrets-env-"));
dirs.push(dir);
return dir;
}
afterEach(async () => {
await Promise.all(dirs.splice(0).map((d) => rm(d, { recursive: true, force: true })));
});
describe("secrets-env-writer", () => {
it("skips silently when disabled", async () => {
const filesystem = vi.fn();
const result = await writeSecretsEnvFile({
rootDir: process.cwd(),
worktreePath: tmpWorktree(),
taskId: "FN-1",
settings: { secretsEnv: { enabled: false } },
worktreeSource: "fresh",
audit: { filesystem },
});
expect(result).toEqual({ outcome: "skipped", filename: ".env", reason: "disabled" });
expect(filesystem).not.toHaveBeenCalled();
});
it("skips when no store", async () => {
const filesystem = vi.fn();
const result = await writeSecretsEnvFile({
rootDir: process.cwd(),
worktreePath: tmpWorktree(),
taskId: "FN-1",
settings: { secretsEnv: { enabled: true } },
worktreeSource: "fresh",
audit: { filesystem },
execFileImpl: ((_f: string, _a: string[], _o: any, cb: any) => cb(null)) as any,
});
expect(result.reason).toBe("no-store");
expect(filesystem).toHaveBeenCalledWith(expect.objectContaining({ type: "secret:env-write-skipped" }));
});
it("writes managed env and sidecar without plaintext in audit/logs", async () => {
const dir = tmpWorktree();
const filesystem = vi.fn();
const log = vi.fn();
const warn = vi.fn();
const secretValue = "SUPER_SECRET_VALUE";
const result = await writeSecretsEnvFile({
rootDir: process.cwd(),
worktreePath: dir,
taskId: "FN-1",
settings: { secretsEnv: { enabled: true, requireGitignored: false } },
worktreeSource: "fresh",
audit: { filesystem },
logger: { log, warn },
secretsStore: {
listEnvExportable: vi.fn().mockResolvedValue([
{ id: "1", key: "A", exportKey: "ALPHA", scope: "project", plaintextValue: secretValue },
{ id: "2", key: "B", exportKey: "BETA", scope: "global", plaintextValue: "x" },
]),
} as any,
});
expect(result.outcome).toBe("written");
const env = readFileSync(join(dir, ".env"), "utf8");
expect(env).toContain("ALPHA=");
expect(env).toContain("BETA=");
const sidecar = readFileSync(join(dir, ".fusion-secrets-env.fingerprint"), "utf8");
expect(sidecar).toContain(".env");
if (process.platform !== "win32") {
expect(statSync(join(dir, ".env")).mode & 0o777).toBe(0o600);
expect(statSync(join(dir, ".fusion-secrets-env.fingerprint")).mode & 0o777).toBe(0o600);
}
const outputBlob = JSON.stringify({ calls: filesystem.mock.calls, logs: log.mock.calls, warns: warn.mock.calls });
expect(outputBlob).not.toContain(secretValue);
});
it("merge is idempotent", async () => {
const dir = tmpWorktree();
writeFileSync(join(dir, ".env"), "EXISTING=1\n");
const secretsStore = {
listEnvExportable: vi.fn().mockResolvedValue([{ id: "1", key: "A", exportKey: "ALPHA", scope: "project", plaintextValue: "v" }]),
} as any;
await writeSecretsEnvFile({
rootDir: process.cwd(),
worktreePath: dir,
taskId: "FN-1",
settings: { secretsEnv: { enabled: true, requireGitignored: false, overwritePolicy: "merge" } },
worktreeSource: "fresh",
secretsStore,
});
const once = readFileSync(join(dir, ".env"), "utf8");
await writeSecretsEnvFile({
rootDir: process.cwd(),
worktreePath: dir,
taskId: "FN-1",
settings: { secretsEnv: { enabled: true, requireGitignored: false, overwritePolicy: "merge" } },
worktreeSource: "fresh",
secretsStore,
});
const twice = readFileSync(join(dir, ".env"), "utf8");
expect(twice).toBe(once);
});
it("rejects invalid filename and symlink", async () => {
const dir = tmpWorktree();
const filesystem = vi.fn();
const a = await writeSecretsEnvFile({
rootDir: process.cwd(),
worktreePath: dir,
taskId: "FN-1",
settings: { secretsEnv: { enabled: true, filename: "../x" } },
worktreeSource: "fresh",
audit: { filesystem },
secretsStore: { listEnvExportable: vi.fn() } as any,
});
expect(a.reason).toBe("invalid-filename");
writeFileSync(join(dir, "real.env"), "SAFE=1\n");
symlinkSync(join(dir, "real.env"), join(dir, ".env"));
const b = await writeSecretsEnvFile({
rootDir: process.cwd(),
worktreePath: dir,
taskId: "FN-1",
settings: { secretsEnv: { enabled: true, requireGitignored: false } },
worktreeSource: "fresh",
audit: { filesystem },
secretsStore: { listEnvExportable: vi.fn() } as any,
});
expect(b.reason).toBe("invalid-filename");
});
it("cleanup removes only fingerprint-matching env", async () => {
const dir = tmpWorktree();
const filesystem = vi.fn();
const secretsStore = {
listEnvExportable: vi.fn().mockResolvedValue([{ id: "1", key: "A", exportKey: "ALPHA", scope: "project", plaintextValue: "v" }]),
} as any;
await writeSecretsEnvFile({
rootDir: process.cwd(),
worktreePath: dir,
taskId: "FN-1",
settings: { secretsEnv: { enabled: true, requireGitignored: false } },
worktreeSource: "fresh",
secretsStore,
});
const cleaned = await cleanupSecretsEnvFile({
worktreePath: dir,
taskId: "FN-1",
expectedFingerprint: null,
filename: ".env",
audit: { filesystem },
});
expect(cleaned.outcome).toBe("cleaned");
expect(existsSync(join(dir, ".env"))).toBe(false);
expect(existsSync(join(dir, ".fusion-secrets-env.fingerprint"))).toBe(false);
await writeSecretsEnvFile({
rootDir: process.cwd(),
worktreePath: dir,
taskId: "FN-1",
settings: { secretsEnv: { enabled: true, requireGitignored: false } },
worktreeSource: "fresh",
secretsStore,
});
writeFileSync(join(dir, ".env"), "MUTATED=1\n");
const skipped = await cleanupSecretsEnvFile({
worktreePath: dir,
taskId: "FN-1",
expectedFingerprint: null,
filename: ".env",
audit: { filesystem },
});
expect(skipped.reason).toBe("fingerprint-mismatch");
expect(existsSync(join(dir, ".env"))).toBe(true);
expect(existsSync(join(dir, ".fusion-secrets-env.fingerprint"))).toBe(false);
});
});

View File

@@ -0,0 +1,84 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
const { writeSecretsEnvFile } = vi.hoisted(() => ({ writeSecretsEnvFile: vi.fn() }));
vi.mock("../secrets-env-writer.js", () => ({
writeSecretsEnvFile,
}));
vi.mock("../worktree-pool.js", async () => {
const actual = await vi.importActual<any>("../worktree-pool.js");
return {
...actual,
classifyTaskWorktree: vi.fn().mockResolvedValue({ ok: true }),
isInsideWorktreesDir: vi.fn().mockReturnValue(true),
};
});
vi.mock("../worktree-db-hydrate.js", () => ({
hydrateWorktreeDb: vi.fn().mockResolvedValue({ degraded: false, tasksCopied: 0, documentsCopied: 0 }),
}));
import { acquireTaskWorktree } from "../worktree-acquisition.js";
describe("worktree-acquisition secrets env hook", () => {
const task = { id: "FN-1", title: "t", description: "d", branch: null, worktree: null } as any;
let store: any;
beforeEach(() => {
writeSecretsEnvFile.mockReset().mockResolvedValue({ outcome: "skipped", filename: ".env", reason: "disabled" });
store = { updateTask: vi.fn().mockResolvedValue(undefined), logEntry: vi.fn().mockResolvedValue(undefined) };
});
it("calls writer on pool", async () => {
await acquireTaskWorktree({
task,
rootDir: process.cwd(),
store,
settings: { recycleWorktrees: true, secretsEnv: { enabled: true } } as any,
pool: {
acquire: () => "/tmp/pool",
prepareForTask: vi.fn().mockResolvedValue({ branch: "fusion/fn-1", worktreePath: "/tmp/pool", reclaimed: false }),
release: vi.fn(),
} as any,
createWorktree: vi.fn(),
secretsStore: undefined,
});
expect(writeSecretsEnvFile).toHaveBeenCalledWith(expect.objectContaining({ worktreeSource: "pool", secretsStore: undefined }));
});
it("calls writer on fresh", async () => {
await acquireTaskWorktree({
task,
rootDir: process.cwd(),
store,
settings: { secretsEnv: { enabled: true } } as any,
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/fresh", branch: "fusion/fn-1" }),
secretsStore: undefined,
});
expect(writeSecretsEnvFile).toHaveBeenCalledWith(expect.objectContaining({ worktreeSource: "fresh" }));
});
it("does not call writer for existing resume", async () => {
await acquireTaskWorktree({
task: { ...task, branch: "fusion/fn-1", worktree: process.cwd() },
rootDir: process.cwd(),
store,
settings: { secretsEnv: { enabled: true } } as any,
createWorktree: vi.fn(),
});
expect(writeSecretsEnvFile).not.toHaveBeenCalled();
});
it("isolates writer failures", async () => {
writeSecretsEnvFile.mockRejectedValueOnce(new Error("boom"));
await expect(acquireTaskWorktree({
task,
rootDir: process.cwd(),
store,
settings: { secretsEnv: { enabled: true } } as any,
createWorktree: vi.fn().mockResolvedValue({ path: "/tmp/fresh", branch: "fusion/fn-1" }),
logger: { log: vi.fn(), warn: vi.fn(), error: vi.fn() },
})).resolves.toMatchObject({ source: "fresh" });
});
});

View File

@@ -0,0 +1,57 @@
import { mkdirSync, mkdtempSync, writeFileSync, existsSync } from "node:fs";
import { rm } from "node:fs/promises";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { afterEach, describe, expect, it, vi } from "vitest";
const cleanupSecretsEnvFile = vi.fn();
vi.mock("../secrets-env-writer.js", () => ({
cleanupSecretsEnvFile,
}));
const dirs: string[] = [];
function tmpRoot(): string {
const root = mkdtempSync(join(tmpdir(), "pool-cleanup-"));
dirs.push(root);
return root;
}
afterEach(async () => {
cleanupSecretsEnvFile.mockReset().mockResolvedValue({ outcome: "cleaned", reason: "fingerprint-match" });
await Promise.all(dirs.splice(0).map((d) => rm(d, { recursive: true, force: true })));
});
describe("worktree-pool secrets cleanup hooks", () => {
it("reapOrphanWorktrees invokes cleanup before removal", async () => {
cleanupSecretsEnvFile.mockResolvedValue({ outcome: "cleaned", reason: "fingerprint-match" });
const root = tmpRoot();
const worktrees = join(root, ".worktrees");
const orphan = join(worktrees, "orphan-1");
mkdirSync(orphan, { recursive: true });
writeFileSync(join(orphan, ".env"), "A=1\n");
const mod = await import("../worktree-pool.js");
const removed = await mod.reapOrphanWorktrees(root);
expect(removed).toBe(1);
expect(cleanupSecretsEnvFile).toHaveBeenCalledWith(expect.objectContaining({
worktreePath: orphan,
taskId: "orphan:orphan-1",
}));
expect(existsSync(orphan)).toBe(false);
});
it("cleanup failures do not block orphan removal", async () => {
cleanupSecretsEnvFile.mockRejectedValueOnce(new Error("cleanup failed"));
const root = tmpRoot();
const orphan = join(root, ".worktrees", "orphan-2");
mkdirSync(orphan, { recursive: true });
const mod = await import("../worktree-pool.js");
const removed = await mod.reapOrphanWorktrees(root);
expect(removed).toBe(1);
expect(existsSync(orphan)).toBe(false);
});
});