Files
ClaudeDo/tests/ClaudeDo.Data.Tests/PromptFilesTests.cs
T
mika kuns a7ff1b3a2f refactor(prompts): split the list-handler prompt per session phase
Both merge-helper ConPTY sessions loaded PromptKind.MergeHelper, so the
post-handoff session received the phase 0-2 dedupe/enhance instructions and was
told to ignore them by its brief alone.

Split into MergeHelperTriage (phases 0-2 + handoff) and MergeHelperExecute
(phases 3-5), so each session carries only its own phases. Consolidated the
generic ask-the-user rule to one place per prompt, scoped Phase 5's summary to
what the execute session actually knows, and moved the dedupe/enhance bilanz to
the triage handoff. Regression guards assert neither prompt carries the other's
phase headings and that the shared-checkout git rule stays in execute.
2026-08-06 21:00:41 +02:00

240 lines
9.3 KiB
C#

using ClaudeDo.Data;
namespace ClaudeDo.Data.Tests;
public class PromptFilesTests
{
[Fact]
public void RenderTemplate_replaces_known_tokens()
{
var outp = PromptFiles.RenderTemplate(
"Plan for {date}, cap {maxTasks}.",
new Dictionary<string, string> { ["date"] = "2026-06-04", ["maxTasks"] = "5" });
Assert.Equal("Plan for 2026-06-04, cap 5.", outp);
}
[Fact]
public void RenderTemplate_leaves_unknown_braces_intact()
{
var outp = PromptFiles.RenderTemplate(
"## {Wochentag}, {dd.MM.yyyy} — {start}",
new Dictionary<string, string> { ["start"] = "01.06.2026" });
Assert.Equal("## {Wochentag}, {dd.MM.yyyy} — 01.06.2026", outp);
}
[Fact]
public void DefaultFor_system_mentions_blocked_marker_and_scope()
{
var d = PromptFiles.DefaultFor(PromptKind.System);
Assert.Contains("CLAUDEDO_BLOCKED:", d);
Assert.Contains("unattended", d, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void DefaultFor_planning_initial_has_title_and_description_tokens()
{
var d = PromptFiles.DefaultFor(PromptKind.PlanningInitial);
Assert.Contains("{title}", d);
Assert.Contains("{description}", d);
}
[Fact]
public void PathFor_planning_is_planning_system_file()
{
Assert.EndsWith("planning-system.md", PromptFiles.PathFor(PromptKind.Planning));
}
[Fact]
public void PathFor_merge_helper_kinds_map_to_their_files()
{
Assert.EndsWith("merge-helper-triage.md", PromptFiles.PathFor(PromptKind.MergeHelperTriage));
Assert.EndsWith("merge-helper-execute.md", PromptFiles.PathFor(PromptKind.MergeHelperExecute));
Assert.EndsWith("merge-helper-initial.md", PromptFiles.PathFor(PromptKind.MergeHelperInitial));
Assert.EndsWith("merge-helper-handoff.md", PromptFiles.PathFor(PromptKind.MergeHelperHandoff));
}
[Fact]
public void DefaultFor_merge_helper_triage_covers_phases_0_to_2_only()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperTriage);
Assert.False(string.IsNullOrWhiteSpace(d));
Assert.Contains("## Phase 0", d);
Assert.Contains("## Phase 1", d);
Assert.Contains("## Phase 2", d);
// The run/review/merge phases belong to the handoff session's prompt, not this one —
// carrying them here is what made the handoff session redo dedupe work.
Assert.DoesNotContain("## Phase 3", d);
Assert.DoesNotContain("## Phase 4", d);
Assert.DoesNotContain("## Phase 5", d);
}
[Fact]
public void DefaultFor_merge_helper_execute_covers_phases_3_to_5_only()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperExecute);
Assert.False(string.IsNullOrWhiteSpace(d));
Assert.Contains("## Phase 3", d);
Assert.Contains("## Phase 4", d);
Assert.Contains("## Phase 5", d);
Assert.DoesNotContain("## Phase 0", d);
Assert.DoesNotContain("## Phase 1", d);
Assert.DoesNotContain("## Phase 2", d);
}
[Fact]
public void DefaultFor_merge_helper_triage_names_the_tools_its_phases_need()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperTriage);
Assert.Contains("batch_get_tasks", d); // phase 0
Assert.Contains("update_task", d); // phase 1 + 2
Assert.Contains("handoff_list_handler", d); // handoff
// Triage never runs or merges anything.
Assert.DoesNotContain("review_task", d);
Assert.DoesNotContain("continue_merge", d);
Assert.DoesNotContain("preview_merge_set", d);
}
[Fact]
public void DefaultFor_merge_helper_execute_names_the_tools_its_phases_need()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperExecute);
Assert.Contains("get_app_settings", d); // phase 3
Assert.Contains("update_task_status", d); // phase 3
Assert.Contains("wait_for_task_change", d); // phase 3
Assert.Contains("preview_merge_set", d); // phase 4
Assert.Contains("review_task", d); // phase 4
Assert.Contains("continue_merge", d); // phase 4
Assert.DoesNotContain("run_task_now(", d); // single override slot — must not batch-start
}
[Fact]
public void DefaultFor_merge_helper_execute_keeps_the_shared_checkout_git_guard()
{
// The handler session never gets PromptKind.System, so this is the ONLY place the
// never-`git add -A`-in-a-shared-checkout rule reaches a list-handler run.
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperExecute);
Assert.Contains("git add -- <the resolved paths>", d);
Assert.Contains("NEVER `git add -A`", d);
Assert.Contains("Never use raw `git merge`", d);
}
[Fact]
public void DefaultFor_merge_helper_only_asks_dedupe_questions_when_a_candidate_exists()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperTriage);
Assert.Contains("move straight to Phase 2", d);
Assert.Contains("do not ask the user to confirm the absence of duplicates", d);
Assert.Contains("Cancel nothing without an explicit answer", d);
Assert.DoesNotContain("whenever you are unsure", d);
}
[Fact]
public void DefaultFor_merge_helper_phase0_treats_brief_as_primary_source()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperTriage);
Assert.Contains("primary source", d, StringComparison.OrdinalIgnoreCase);
Assert.Contains("batch_get_tasks", d);
Assert.Contains("Do not act on any single task before you have read", d);
}
[Fact]
public void DefaultFor_merge_helper_allows_splitting_bundled_tasks_in_phase_2()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperTriage);
Assert.Contains("Propose splitting it to the user", d);
Assert.Contains("add_task/add_subtask", d);
Assert.Contains("do not invent requirements", d);
}
[Fact]
public void DefaultFor_merge_helper_checks_effective_max_turns_before_queuing()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperExecute);
Assert.Contains("effective max-turns", d);
Assert.Contains("get_list_config", d);
Assert.Contains("set_task_config", d);
Assert.Contains("get_task_config", d);
}
[Fact]
public void DefaultFor_merge_helper_checks_file_collisions_before_merge_order()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperExecute);
Assert.Contains("Default to the order the brief lists them", d);
Assert.Contains("tell the user which tasks collide", d);
Assert.Contains("NORMAL case, not a failure", d);
}
[Fact]
public void DefaultFor_merge_helper_initial_has_repo_token()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperInitial);
Assert.Contains("{repo}", d);
}
[Fact]
public void DefaultFor_merge_helper_initial_has_scope_and_tasks_tokens()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperInitial);
Assert.False(string.IsNullOrWhiteSpace(d));
Assert.Contains("{scope}", d);
Assert.Contains("{tasks}", d);
}
[Fact]
public void RenderTemplate_merge_helper_initial_substitutes_scope_and_tasks()
{
var outp = PromptFiles.RenderTemplate(
PromptFiles.DefaultFor(PromptKind.MergeHelperInitial),
new Dictionary<string, string> { ["scope"] = "All lists", ["tasks"] = "- [Idle] T1" });
Assert.Contains("Scope: All lists", outp);
Assert.Contains("- [Idle] T1", outp);
Assert.DoesNotContain("{scope}", outp);
Assert.DoesNotContain("{tasks}", outp);
}
[Fact]
public void DefaultFor_merge_helper_handoff_has_scope_repo_and_tasks_tokens()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperHandoff);
Assert.False(string.IsNullOrWhiteSpace(d));
Assert.Contains("{scope}", d);
Assert.Contains("{repo}", d);
Assert.Contains("{tasks}", d);
}
[Fact]
public void DefaultFor_merge_helper_handoff_points_at_phase_3()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperHandoff);
Assert.Contains("phase 3", d, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void RenderTemplate_merge_helper_handoff_substitutes_scope_repo_and_tasks()
{
var outp = PromptFiles.RenderTemplate(
PromptFiles.DefaultFor(PromptKind.MergeHelperHandoff),
new Dictionary<string, string>
{
["scope"] = "List: Bugs",
["repo"] = "C:\\repo",
["tasks"] = "- [WaitingForReview] T1 (id: abc)",
});
Assert.Contains("Scope: List: Bugs", outp);
Assert.Contains("Repo: C:\\repo", outp);
Assert.Contains("- [WaitingForReview] T1 (id: abc)", outp);
Assert.DoesNotContain("{scope}", outp);
Assert.DoesNotContain("{repo}", outp);
Assert.DoesNotContain("{tasks}", outp);
}
[Fact]
public void DefaultFor_merge_helper_tells_the_session_to_hand_off_after_phase_2()
{
var d = PromptFiles.DefaultFor(PromptKind.MergeHelperTriage);
Assert.Contains("handoff_list_handler", d);
Assert.Contains("do not continue into phase 3 yourself", d, StringComparison.OrdinalIgnoreCase);
}
}