feat(FN-3968): add tests for task env inheritance and isolation in spawn/ch

Adds test coverage for task environment propagation through the spawn flow, covering child agents inheriting task env, workflow step task env, and spawn hook env isolation cases in `pi-create-fn-agent.test.ts`, with minor additions to `executor-step-session.test.ts` and `executor-pause.test.ts`.

Fusion-Task-Id: FN-3968
This commit is contained in:
Fusion
2026-05-10 21:52:34 -07:00
committed by gsxdsm
parent a4b268ce58
commit a3c675fe19
3 changed files with 83 additions and 1 deletions

View File

@@ -350,14 +350,21 @@ describe("Agent Spawning", () => {
// createFnAgent is called at least twice: once for parent, once for child
expect(mockedCreateFnAgent.mock.calls.length).toBeGreaterThanOrEqual(2);
const parentCall = mockedCreateFnAgent.mock.calls.find(
(call: any) => !call[0].systemPrompt?.includes("child agent spawned")
);
// Find the child session call
const childCall = mockedCreateFnAgent.mock.calls.find(
(call: any) => call[0].systemPrompt?.includes("child agent spawned")
);
expect(parentCall).toBeDefined();
expect(childCall).toBeDefined();
expect(childCall![0].tools).toBe("coding");
expect(childCall![0].cwd).toContain("/.worktrees/swift-falcon");
expect(childCall![0].systemPrompt).toContain("FN-SPAWN");
expect(childCall![0].taskEnv).toEqual(parentCall![0].taskEnv);
});
it("respects per-parent maxSpawnedAgentsPerParent limit", async () => {

View File

@@ -279,6 +279,7 @@ describe("Workflow Steps Execution", () => {
expect(secondCall[0].tools).toBe("readonly");
expect(secondCall[0].systemPrompt).toContain("Docs Review");
expect(secondCall[0].systemPrompt).toContain("Review all docs and verify they are complete.");
expect(secondCall[0].taskEnv).toEqual(mockedCreateFnAgent.mock.calls[0][0].taskEnv);
// Task should move to in-review
expect(store.moveTask).toHaveBeenCalledWith("FN-001", "in-review");
@@ -816,6 +817,7 @@ describe("Workflow Steps Execution", () => {
it("executes script-mode workflow step successfully", async () => {
const store = createMockStore();
process.env.FN3968_SCRIPT_ENV = "workflow-script-env";
store.getSettings.mockResolvedValue({
maxConcurrent: 2,
@@ -906,6 +908,12 @@ describe("Workflow Steps Execution", () => {
);
const updatePayloads = store.updateTask.mock.calls.map((call: any[]) => call[1]);
expect(JSON.stringify(updatePayloads)).not.toContain("all tests passed");
const scriptExecCall = mockedExecSync.mock.calls.find(
(call: any[]) => typeof call[0] === "string" && call[0].includes("echo 'all tests passed'")
);
expect(scriptExecCall?.[1]?.env?.FN3968_SCRIPT_ENV).toBe("workflow-script-env");
delete process.env.FN3968_SCRIPT_ENV;
});
it("executes plugin script-mode workflow step successfully", async () => {

View File

@@ -841,6 +841,7 @@ describe("createFnAgent", () => {
const spawnHook = createBashToolMock.mock.calls.at(-1)?.[1]?.spawnHook;
const originalEnv = { PATH: "/base/bin", HOME: "/home/user" };
const processEnvBefore = { ...process.env };
const spawned = spawnHook({
command: "echo hi",
cwd: "/project",
@@ -857,6 +858,7 @@ describe("createFnAgent", () => {
},
});
expect(originalEnv).toEqual({ PATH: "/base/bin", HOME: "/home/user" });
expect(process.env).toEqual(processEnvBefore);
});
it("keeps bash tool default behavior when taskEnv is not provided", async () => {
@@ -871,6 +873,71 @@ describe("createFnAgent", () => {
expect(createBashToolMock).toHaveBeenCalledWith("/project", undefined);
});
it("keeps spawned env unchanged when taskEnv is empty", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/project",
systemPrompt: "test",
tools: "coding",
taskEnv: {},
});
const spawnHook = createBashToolMock.mock.calls.at(-1)?.[1]?.spawnHook;
const originalEnv = { HOME: "/home/user", PATH: "/bin" };
const spawned = spawnHook({ command: "env", cwd: "/project", env: originalEnv });
expect(spawned.env).toEqual({ HOME: "/home/user", PATH: "/bin" });
});
it("adds new task env keys absent from spawned env", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/project",
systemPrompt: "test",
tools: "coding",
taskEnv: { TASK_ONLY: "abc" },
});
const spawnHook = createBashToolMock.mock.calls.at(-1)?.[1]?.spawnHook;
const spawned = spawnHook({ command: "env", cwd: "/project", env: { HOME: "/home/user" } });
expect(spawned.env).toEqual({ HOME: "/home/user", TASK_ONLY: "abc" });
});
it("preserves undefined task env values explicitly in merged env", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/project",
systemPrompt: "test",
tools: "coding",
taskEnv: { TASK_OPTIONAL: undefined },
});
const spawnHook = createBashToolMock.mock.calls.at(-1)?.[1]?.spawnHook;
const spawned = spawnHook({ command: "env", cwd: "/project", env: { HOME: "/home/user" } });
expect(spawned.env).toEqual({ HOME: "/home/user", TASK_OPTIONAL: undefined });
});
it("injects PATH from task env when spawned env has no PATH", async () => {
const { createFnAgent } = await import("../pi.js");
await createFnAgent({
cwd: "/project",
systemPrompt: "test",
tools: "coding",
taskEnv: { PATH: "/task/bin" },
});
const spawnHook = createBashToolMock.mock.calls.at(-1)?.[1]?.spawnHook;
const spawned = spawnHook({ command: "env", cwd: "/project", env: { HOME: "/home/user" } });
expect(spawned.env).toEqual({ HOME: "/home/user", PATH: "/task/bin" });
});
it("refuses to start a coding agent in an unregistered worktree", async () => {
existsSyncMock.mockImplementation((path) => {
const value = String(path);