// src/execution/__tests__/nested-visibility-env-propagation.test.ts // // SP-8 嵌套可见性修复验证(TC-3:PI_SUBAGENT_ROOT_SESSION_ID env 贯穿)。 // // 验证 env 身份贯穿机制: // TC-3a: 根进程 initSession:无 env → sessionRootId = sessionId(自己是 ROOT) // TC-3b: 子进程 initSession:有 env → sessionRootId = env 值(贯穿真 ROOT) // TC-3c: 孙进程 initSession:env 链全程贯穿 // TC-3d: 子进程 execute 创建的 record.rootSessionId = env 贯穿的真 ROOT // TC-3e: 子进程 collectRecords 用 sessionRootId 过滤 // // mock 策略与 recursive-visibility-baseline.test.ts 一致。 import type { PassThrough } from "node:stream"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; // ── mock modules ── vi.mock("node:child_process", async () => { const { EventEmitter } = await import("node:events"); const { PassThrough } = await import("node:stream"); class FakeChild extends EventEmitter { pid = 12345; stdout = new PassThrough(); stderr = new PassThrough(); killed = false; killSignal: string | undefined; kill(sig?: string): boolean { this.killed = true; this.killSignal = sig; return true; } } return { spawn: vi.fn(() => new FakeChild()), // buildEnvBlock 用 execFile 异步取 git branch:默认 err-first 兜底(catch → branch="") execFile: vi.fn( ( _cmd: string, _args: readonly string[], _opts: unknown, cb: (err: Error | null, stdout?: string, stderr?: string) => void, ) => cb(new Error("execFile not configured in this test")), ), }; }); vi.mock("node:fs", async () => { const actual = await import("node:fs"); return { default: { ...actual, mkdirSync: vi.fn(), existsSync: vi.fn(() => false), appendFileSync: vi.fn(), writeFileSync: vi.fn(), readdirSync: vi.fn(() => []), }, mkdirSync: vi.fn(), existsSync: vi.fn(() => false), appendFileSync: vi.fn(), writeFileSync: vi.fn(), readdirSync: vi.fn(() => []), promises: actual.promises, }; }); vi.mock("../alive-store.ts", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, writeAliveMarker: vi.fn(), removeAliveMarker: vi.fn(), }; }); vi.mock("../finalized-marker.ts", () => ({ writeFinalized: vi.fn(), readFinalized: vi.fn(() => false), })); vi.mock("../manifest-store.ts", () => { class FakeManifestStore { writeManifest = vi.fn(async () => {}); readManifest = vi.fn(async () => null); listAllSync = vi.fn(() => []); recoverTmpFiles = vi.fn(async () => []); } return { ManifestStore: vi.fn(function (_recordsDir: string) { return new FakeManifestStore(); }) }; }); vi.mock("../temp-prompt.ts", () => ({ writePromptToTempFile: vi.fn(async (agent: string) => { const safeName = agent.replace(/[^\w.-]+/g, "_"); return { dir: `/tmp/fake-${safeName}`, filePath: `/tmp/fake-${safeName}/prompt-${safeName}.md` }; }), cleanupTempPrompt: vi.fn(async () => {}), })); import { spawn } from "node:child_process"; import { ModelConfigService } from "../model-config-service.ts"; import type { ModelInfo, ModelRegistryLike } from "../model-resolver.ts"; import type { RecordStore } from "../record-store.ts"; import { createRecord } from "../execution-record.ts"; import { SubagentService } from "../subagent-service.ts"; const mockSpawn = vi.mocked(spawn); // ── env 名常量(与 subagent-service.ts 一致)── const ENV_ROOT_SESSION_ID = "PI_SUBAGENT_ROOT_SESSION_ID"; const ENV_SELF_RECORD_ID = "PI_SUBAGENT_SELF_RECORD_ID"; const ENV_DEPTH = "PI_SUBAGENT_DEPTH"; const ENV_FORK_DEPTH = "PI_SUBAGENT_FORK_DEPTH"; const ENV_ROOT_CWD = "PI_SUBAGENT_ROOT_CWD"; // ── helpers ── interface FakeChild { pid: number; stdout: PassThrough; stderr: PassThrough; killed: boolean; killSignal: string | undefined; kill(sig?: string): boolean; emit(event: string, ...args: unknown[]): boolean; } function lastSpawnedChild(): FakeChild { const result = mockSpawn.mock.results.at(-1); if (!result) throw new Error("spawn was not called yet"); return result.value as FakeChild; } function getLastSpawnEnv(): Record { return (mockSpawn.mock.calls.at(-1)?.[2]?.env as Record) ?? {}; } function sessionHeader(id = "env-prop-session"): Record { return { type: "session", id, timestamp: "2026-08-11T00-00-00-000Z", cwd: "/tmp/test" }; } function emitStdoutLine(child: FakeChild, obj: Record): void { child.stdout.write(`${JSON.stringify(obj)}\n`); } async function driveChildToCompletion(child: FakeChild, events: Record[] = []): Promise { emitStdoutLine(child, sessionHeader()); for (const e of events) emitStdoutLine(child, e); child.stdout.end(); child.stderr.end(); child.emit("close", 0); } function makeEmptyRegistry(): ModelRegistryLike { return { getAvailable: () => [], find: () => undefined, hasConfiguredAuth: () => true }; } function makePi() { return { sendMessage: vi.fn(), appendEntry: vi.fn(), events: { emit: vi.fn() } }; } const ctxModel: ModelInfo = { id: "m", name: "M", provider: "p", reasoning: false }; function setupService(sessionId: string, env?: Record): SubagentService { const agentDir = "/tmp/env-prop-it"; const modelService = new ModelConfigService({ agentDir }); modelService.initModel({ modelRegistry: makeEmptyRegistry(), sessionId, ctxModel, }); const service = new SubagentService({ cwd: agentDir, modelService, getMainSessionFile: () => "/mock/main-session.jsonl", }); service.initSession({ pi: makePi(), sessionId }); return service; } // ── 用例 ── describe("TC-3: PI_SUBAGENT_ROOT_SESSION_ID env 贯穿正确", () => { beforeEach(() => { vi.clearAllMocks(); for (const k of [ENV_ROOT_SESSION_ID, ENV_SELF_RECORD_ID, ENV_DEPTH, ENV_FORK_DEPTH, ENV_ROOT_CWD]) { delete process.env[k]; } }); afterEach(() => { for (const k of [ENV_ROOT_SESSION_ID, ENV_SELF_RECORD_ID, ENV_DEPTH, ENV_FORK_DEPTH, ENV_ROOT_CWD]) { delete process.env[k]; } vi.restoreAllMocks(); }); it("TC-3a: 根进程 initSession:无 env → sessionRootId = sessionId(自己是 ROOT)", () => { const service = setupService("root-it"); const sessionRootId = Reflect.get(service, "sessionRootId") as string; expect(sessionRootId).toBe("root-it"); }); it("TC-3b: 子进程 initSession:有 env PI_SUBAGENT_ROOT_SESSION_ID → sessionRootId = env 值(贯穿真 ROOT)", () => { process.env[ENV_ROOT_SESSION_ID] = "real-root-session"; const service = setupService("child-session"); const sessionRootId = Reflect.get(service, "sessionRootId") as string; expect(sessionRootId).toBe("real-root-session"); // env 贯穿,不是 child-session }); it("TC-3c: 孙进程 initSession:env 链全程贯穿,B 的 sessionRootId 仍为 ROOT", () => { // 模拟 B 子进程收到的 env(A 在 spawn B 时注入) process.env[ENV_ROOT_SESSION_ID] = "root-main-session"; process.env[ENV_SELF_RECORD_ID] = "sa-a-record"; process.env[ENV_DEPTH] = "1"; const service = setupService("b-session"); // B 的 sessionRootId 仍为 ROOT(env 贯穿,不被 B 自己的 sessionId 覆盖) const sessionRootId = Reflect.get(service, "sessionRootId") as string; expect(sessionRootId).toBe("root-main-session"); // B 的 execCtxBaseline 记录了 B 自己的身份(recordId + depth) const baseline = Reflect.get(service, "execCtxBaseline") as { recordId: string; depth: number }; expect(baseline.recordId).toBe("sa-a-record"); expect(baseline.depth).toBe(1); }); it("TC-3d: 子进程 execute 创建的 record.rootSessionId = env 贯穿的真 ROOT", async () => { process.env[ENV_ROOT_SESSION_ID] = "root-main-session"; process.env[ENV_SELF_RECORD_ID] = "sa-parent"; process.env[ENV_DEPTH] = "1"; const service = setupService("child-session"); // execute 创建 record(模拟 B 内创建 C) const handle = await service.execute({ task: "nested task", ctxModel, }); const store = Reflect.get(service, "store") as RecordStore; // collectRecords 用 sessionRootId 过滤(子进程应看到 ROOT 整棵树) const records = store.collectRecords(100, "all", "root-main-session"); const found = records.find((r) => r.id === handle.subagentId); expect(found).toBeDefined(); expect(found!.rootSessionId).toBe("root-main-session"); // 指向 ROOT,不是 child-session expect(found!.parentRecordId).toBe("sa-parent"); // 父链来自基线 expect(found!.depth).toBe(2); // 基线 depth=1 + 1 = 2 }); it("TC-3e: 子进程 collectRecords 用 sessionRootId 过滤(能看到 ROOT 整棵树)", () => { process.env[ENV_ROOT_SESSION_ID] = "root-main-session"; process.env[ENV_SELF_RECORD_ID] = "sa-parent"; process.env[ENV_DEPTH] = "1"; const service = setupService("child-session"); // 手动注入一条 running record(避免需要真正 spawn) const store = Reflect.get(service, "store") as RecordStore; const recordC = createRecord("sa-c", { agent: "worker", model: "test/model", mode: "background", task: "deep nested", startedAt: Date.now(), rootSessionId: "root-main-session", parentRecordId: "sa-parent", depth: 2, }); store.register(recordC); // collectRecords(不带 filter)用 this.sessionRootId 过滤 const viaService = service.collectRecords(100); const ids = viaService.map((r) => r.id); // C 的 record 归 ROOT(rootSessionId=root-main-session),能被子进程查到 expect(ids).toContain("sa-c"); expect(viaService.find((r) => r.id === "sa-c")!.rootSessionId).toBe("root-main-session"); }); });