using ClaudeDo.Worker.Planning; namespace ClaudeDo.Worker.Tests.Planning; public sealed class WindowsTerminalLauncherTests { private static PlanningSessionStartContext MakeStartCtx(string? wd = null) { var workingDir = wd ?? Path.GetTempPath(); var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); Directory.CreateDirectory(dir); return new PlanningSessionStartContext( ParentTaskId: "task-1", WorkingDir: workingDir, Token: "test-token", WorktreePath: workingDir, BranchName: "claudedo/planning/task1", Files: new PlanningSessionFiles( SessionDirectory: dir, SystemPromptPath: Path.Combine(dir, "system-prompt.md"), InitialPromptPath: Path.Combine(dir, "initial-prompt.txt"))); } [Fact] public async Task LaunchPlanningStartAsync_WorkingDirMissing_Throws() { var ctx = MakeStartCtx(wd: Path.Combine(Path.GetTempPath(), "nonexistent_" + Guid.NewGuid())); var sut = new WindowsTerminalLauncher(wtPath: "wt", claudePath: "claude"); var ex = await Assert.ThrowsAsync(() => sut.LaunchPlanningStartAsync(ctx, CancellationToken.None)); Assert.Contains("Working directory", ex.Message); } [Fact] public async Task LaunchPlanningStartAsync_WtMissing_Throws() { var ctx = MakeStartCtx(); File.WriteAllText(ctx.Files.SystemPromptPath, "sp"); File.WriteAllText(ctx.Files.InitialPromptPath, "ip"); var sut = new WindowsTerminalLauncher( wtPath: "C:/no/such/wt.exe", claudePath: "claude"); var ex = await Assert.ThrowsAsync(() => sut.LaunchPlanningStartAsync(ctx, CancellationToken.None)); Assert.Contains("Windows Terminal", ex.Message); } [Fact] public void BuildPlanningStartCommand_SeedsViaBriefFile_NotInlineText() { var ctx = MakeStartCtx(); var command = WindowsTerminalLauncher.BuildPlanningStartCommand("claude.exe", ctx); // The brief is read from a file: the kickoff references the brief path, and its // directory is exposed via --add-dir so the Read tool can open it. Assert.Contains(ctx.Files.InitialPromptPath, command); Assert.Contains($"--add-dir' '{ctx.Files.SessionDirectory}", command); // The positional kickoff must follow the single-value --append-system-prompt-file // flag, or a preceding variadic flag would swallow it. var appendIdx = command.IndexOf("--append-system-prompt-file", StringComparison.Ordinal); var addDirIdx = command.IndexOf("--add-dir", StringComparison.Ordinal); Assert.True(appendIdx > addDirIdx, "--append-system-prompt-file must come after --add-dir"); Assert.True(command.IndexOf(ctx.Files.InitialPromptPath, StringComparison.Ordinal) > appendIdx, "the kickoff prompt must be the last (positional) token"); // The legacy env-var indirection is gone. Assert.DoesNotContain("CLAUDEDO_LAUNCH_PROMPT", command); } [Fact] public void BuildPlanningStartArgs_HasPlanningFlagsAndKickoffLast() { var ctx = MakeStartCtx(); var args = WindowsTerminalLauncher.BuildPlanningStartArgs(ctx); Assert.Equal("--model", args[0]); var permIdx = args.ToList().IndexOf("--permission-mode"); Assert.True(permIdx >= 0); // Default mode, NOT plan mode: plan mode gates every MCP tool call regardless of the // allowlist, which would prompt on the first CreateChildTask. Assert.Equal("default", args[permIdx + 1]); Assert.Contains("--allowedTools", args); Assert.Contains(ctx.Files.SessionDirectory, args); Assert.Contains(ctx.Files.SystemPromptPath, args); // Kickoff positional is the last token and points at the brief file. Assert.Contains(ctx.Files.InitialPromptPath, args[^1]); } [Fact] public void BuildPlanningResumeArgs_DefaultModeAllowlistsMcpAndResumes() { var args = WindowsTerminalLauncher.BuildPlanningResumeArgs("sess-9"); Assert.Equal("--permission-mode", args[0]); Assert.Equal("default", args[1]); Assert.Equal("--allowedTools", args[2]); // The MCP planning tools must be re-allowlisted on resume: the CLI does not restore // --allowedTools across a --resume, so without it CreateChildTask would prompt again. Assert.Contains("mcp__claudedo__", args[3]); Assert.Equal(new[] { "--resume", "sess-9" }, args.Skip(4).ToArray()); } [Fact] public void BuildResumeArgs_ResumesSession() { var args = WindowsTerminalLauncher.BuildResumeArgs("sess-42"); Assert.Equal(new[] { "--resume", "sess-42" }, args); } }