@@ -8385,6 +8385,116 @@ describe("ClaudeAdapterV2 background wake turns", () => {
83858385 ),
83868386 );
83878387
8388+ it.effect.skipIf(process.getuid === undefined)(
8389+ "tails foreground Bash output under the physical cwd the query launched in",
8390+ () =>
8391+ Effect.scoped(
8392+ Effect.gen(function* () {
8393+ const fileSystem = yield* FileSystem.FileSystem;
8394+ const path = yield* Path.Path;
8395+ const root = yield* fileSystem.makeTempDirectoryScoped({
8396+ prefix: "t3-claude-task-output-cwd-",
8397+ });
8398+ const physicalCwd = path.join(root, "real", "project");
8399+ const launchCwd = path.join(root, "link");
8400+ yield* fileSystem.makeDirectory(physicalCwd, { recursive: true });
8401+ yield* fileSystem.symlink(physicalCwd, launchCwd);
8402+ // The session opens in /workspace; a relative TMPDIR puts Claude's
8403+ // task files under whichever cwd the CLI was launched in. Linux,
8404+ // because Claude Code's macOS build ignores TMPDIR.
8405+ const harness = yield* makeWakeHarnessWithOptions({ environment: { TMPDIR: "../tmp" } });
8406+ const now = yield* DateTime.now;
8407+ const toolUseId = "toolu_foreground_bash";
8408+ const taskId = "foreground-bash-task";
8409+ const tasks = path.join(
8410+ root,
8411+ "real",
8412+ "tmp",
8413+ `claude-${process.getuid?.()}`,
8414+ "project",
8415+ WAKE_NATIVE_SESSION,
8416+ "tasks",
8417+ );
8418+ yield* fileSystem.makeDirectory(tasks, { recursive: true });
8419+ yield* fileSystem.writeFileString(path.join(tasks, `${taskId}.output`), "line 1\n");
8420+
8421+ yield* harness.runtime.startTurn(
8422+ makeClaudeTestTurnInput({
8423+ threadId: harness.threadId,
8424+ providerThread: harness.providerThread,
8425+ now,
8426+ attemptId: RunAttemptId.make("attempt-claude-task-output-cwd"),
8427+ text: "Run the build.",
8428+ attachments: [],
8429+ runtimePolicy: { ...CLAUDE_TEST_RUNTIME_POLICY, cwd: launchCwd },
8430+ }),
8431+ );
8432+ assert.equal(harness.getOpenedOptions()?.cwd, launchCwd);
8433+ for (const frame of [
8434+ claudeSdkFrame({
8435+ type: "assistant",
8436+ message: {
8437+ model: "claude-sonnet-4-6",
8438+ id: "msg_foreground_bash",
8439+ type: "message",
8440+ role: "assistant",
8441+ content: [
8442+ {
8443+ type: "tool_use",
8444+ id: toolUseId,
8445+ name: "Bash",
8446+ input: { command: "vp run build", description: "Build" },
8447+ },
8448+ ],
8449+ stop_reason: null,
8450+ stop_sequence: null,
8451+ usage: { input_tokens: 1, output_tokens: 1 },
8452+ },
8453+ parent_tool_use_id: null,
8454+ uuid: "00000000-0000-4000-8000-000000000a01",
8455+ session_id: WAKE_NATIVE_SESSION,
8456+ }),
8457+ claudeSdkFrame({
8458+ type: "system",
8459+ subtype: "task_started",
8460+ task_id: taskId,
8461+ tool_use_id: toolUseId,
8462+ description: "vp run build",
8463+ is_backgrounded: false,
8464+ task_type: "local_bash",
8465+ uuid: "00000000-0000-4000-8000-000000000a02",
8466+ session_id: WAKE_NATIVE_SESSION,
8467+ }),
8468+ claudeSdkFrame({
8469+ type: "user",
8470+ message: {
8471+ role: "user",
8472+ content: [{ type: "tool_result", tool_use_id: toolUseId, content: "line 1" }],
8473+ },
8474+ parent_tool_use_id: null,
8475+ uuid: "00000000-0000-4000-8000-000000000a03",
8476+ session_id: WAKE_NATIVE_SESSION,
8477+ tool_use_result: { stdout: "line 1\n", stderr: "", interrupted: false },
8478+ }),
8479+ makeResultFrame({ uuid: "00000000-0000-4000-8000-000000000a04", result: "Built." }),
8480+ ]) {
8481+ yield* Queue.offer(harness.sdkMessages, frame);
8482+ }
8483+ yield* harness.awaitTerminal(1);
8484+
8485+ const chunks = harness.events.flatMap((event) =>
8486+ event.type === "command_output.delta" ? [event.chunk] : [],
8487+ );
8488+ assert.deepEqual(chunks, ["line 1\n"]);
8489+ }).pipe(
8490+ Effect.provideService(HostProcess.Platform, "linux"),
8491+ Effect.provide(
8492+ Layer.mergeAll(IdAllocator.layer, McpProviderSessions.layer, NodeServices.layer),
8493+ ),
8494+ ),
8495+ ),
8496+ );
8497+
83888498 it.effect("preserves settled background work until an explicit stop", () =>
83898499 Effect.scoped(
83908500 Effect.gen(function* () {
0 commit comments