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:
@@ -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 () => {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user