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);
});
});

View File

@@ -802,6 +802,7 @@ export class HeartbeatMonitor {
private selfImproveService?: SelfImproveServiceLike;
private approvalRequestStore?: ApprovalRequestStore;
private snapshotManager?: AutoClaimSnapshotManager;
private secretsStore?: Pick<import("@fusion/core").SecretsStore, "listEnvExportable">;
private trackedAgents: Map<string, TrackedAgent> = new Map();
private agentStartLocks: Map<string, Promise<unknown>> = new Map();
@@ -831,6 +832,7 @@ export class HeartbeatMonitor {
this.reflectionService = options.reflectionService;
this.selfImproveService = options.selfImproveService;
this.snapshotManager = options.snapshotManager ?? (this.taskStore ? new AutoClaimSnapshotManager({ taskStore: this.taskStore }) : undefined);
this.secretsStore = options.secretsStore;
}
getChatStore(): ChatStore | undefined {
@@ -2398,7 +2400,7 @@ export class HeartbeatMonitor {
audit,
runContext,
runInitCommand: false,
secretsStore: this.options.secretsStore,
secretsStore: this.secretsStore,
});
sessionCwd = acquisition.worktreePath;
} catch (worktreeErr) {

View File

@@ -309,7 +309,7 @@ export async function acquireTaskWorktree(opts: AcquireTaskWorktreeOptions): Pro
settings,
reason: RemovalReason.PoolPrune,
taskId: task.id,
audit,
audit: undefined,
});
} catch (removeErr) {
logger?.warn(`${task.id}: failed to remove unusable pooled worktree ${worktreePath}: ${formatError(removeErr)}`);

View File

@@ -2,7 +2,7 @@ import { exec } from "node:child_process";
import { promisify } from "node:util";
import { existsSync, lstatSync, readdirSync, rmSync, realpathSync } from "node:fs";
import { basename, join, relative, resolve, isAbsolute } from "node:path";
import type { Column, Settings, TaskStore, WorktrunkSettings } from "@fusion/core";
import type { Column, SecretsStore, Settings, TaskStore, WorktrunkSettings } from "@fusion/core";
import { assertCleanBranchAtBase, inspectBranchConflict } from "./branch-conflicts.js";
import { worktreePoolLog } from "./logger.js";
import { isInsideConfiguredWorktreesDir, resolveWorktreesDir } from "./worktree-paths.js";
@@ -15,6 +15,7 @@ import {
resolveWorktreeBackend as resolveWorktreeBackendViaSettings,
} from "./worktree-backend.js";
import { cleanupSecretsEnvFile } from "./secrets-env-writer.js";
import type { RunAuditor } from "./run-audit.js";
export {
NativeWorktreeBackend,
@@ -263,11 +264,18 @@ export class PoolDoubleLeaseError extends Error {
}
}
export interface WorktreePoolOptions {
auditFactory?: (taskId: string) => Pick<RunAuditor, "filesystem">;
secretsStore?: Pick<SecretsStore, "listEnvExportable">;
}
export class WorktreePool {
private idle = new Set<string>();
private leased = new Map<string, string>();
private invariantViolationHandler?: (violation: PoolInvariantViolation) => void;
constructor(_options: WorktreePoolOptions = {}) {}
/**
* Acquire an idle worktree from the pool.
*
@@ -655,6 +663,22 @@ export async function cleanupOrphanedWorktrees(
for (const worktreePath of candidates) {
try {
if (registeredWorktrees.has(resolve(worktreePath))) {
const orphanTaskId = `orphan:${basename(worktreePath)}`;
try {
await cleanupSecretsEnvFile({
worktreePath,
taskId: orphanTaskId,
expectedFingerprint: null,
filename: ".env",
audit: undefined,
logger: worktreePoolLog,
});
} catch (error) {
worktreePoolLog.warn(
`secrets-env cleanup failed for registered orphan ${worktreePath}: ${error instanceof Error ? error.message : String(error)}`,
);
}
await removeWorktreeViaBackend({
rootDir,
worktreePath,