story-kit: merge 225_story_surface_merge_conflicts_and_failures_in_the_web_ui
This commit is contained in:
@@ -25,6 +25,7 @@ export interface PipelineStageItem {
|
|||||||
story_id: string;
|
story_id: string;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
|
merge_failure: string | null;
|
||||||
agent: AgentAssignment | null;
|
agent: AgentAssignment | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ describe("AgentLozenge fixed intrinsic width", () => {
|
|||||||
story_id: "74_width_test",
|
story_id: "74_width_test",
|
||||||
name: "Width Test",
|
name: "Width Test",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -108,6 +109,7 @@ describe("LozengeFlyProvider fly-in visibility", () => {
|
|||||||
story_id: "74_hidden_test",
|
story_id: "74_hidden_test",
|
||||||
name: "Hidden Test",
|
name: "Hidden Test",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -143,6 +145,7 @@ describe("LozengeFlyProvider fly-in visibility", () => {
|
|||||||
story_id: "74_no_roster",
|
story_id: "74_no_roster",
|
||||||
name: "No Roster",
|
name: "No Roster",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: {
|
agent: {
|
||||||
agent_name: "unknown-agent",
|
agent_name: "unknown-agent",
|
||||||
model: null,
|
model: null,
|
||||||
@@ -208,6 +211,7 @@ describe("LozengeFlyProvider fly-in clone", () => {
|
|||||||
story_id: "74_portal_test",
|
story_id: "74_portal_test",
|
||||||
name: "Portal Test",
|
name: "Portal Test",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -248,6 +252,7 @@ describe("LozengeFlyProvider fly-in clone", () => {
|
|||||||
story_id: "74_clone_remove",
|
story_id: "74_clone_remove",
|
||||||
name: "Clone Remove",
|
name: "Clone Remove",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -294,6 +299,7 @@ describe("LozengeFlyProvider fly-in clone", () => {
|
|||||||
story_id: "74_reveal_test",
|
story_id: "74_reveal_test",
|
||||||
name: "Reveal Test",
|
name: "Reveal Test",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -362,6 +368,7 @@ describe("LozengeFlyProvider fly-out", () => {
|
|||||||
story_id: "74_fly_out_test",
|
story_id: "74_fly_out_test",
|
||||||
name: "Fly Out Test",
|
name: "Fly Out Test",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: "haiku", status: "completed" },
|
agent: { agent_name: "coder-1", model: "haiku", status: "completed" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -386,6 +393,7 @@ describe("LozengeFlyProvider fly-out", () => {
|
|||||||
story_id: "74_fly_out_test",
|
story_id: "74_fly_out_test",
|
||||||
name: "Fly Out Test",
|
name: "Fly Out Test",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -417,6 +425,7 @@ describe("AgentLozenge idle vs active appearance", () => {
|
|||||||
story_id: "74_running_color",
|
story_id: "74_running_color",
|
||||||
name: "Running",
|
name: "Running",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -440,6 +449,7 @@ describe("AgentLozenge idle vs active appearance", () => {
|
|||||||
story_id: "74_pending_color",
|
story_id: "74_pending_color",
|
||||||
name: "Pending",
|
name: "Pending",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "pending" },
|
agent: { agent_name: "coder-1", model: null, status: "pending" },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -463,6 +473,7 @@ describe("AgentLozenge idle vs active appearance", () => {
|
|||||||
story_id: "74_pulse_dot",
|
story_id: "74_pulse_dot",
|
||||||
name: "Pulse",
|
name: "Pulse",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -513,6 +524,7 @@ describe("hiddenRosterAgents: assigned agents are absent from roster", () => {
|
|||||||
story_id: "85_assign_test",
|
story_id: "85_assign_test",
|
||||||
name: "Assign Test",
|
name: "Assign Test",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -533,6 +545,7 @@ describe("hiddenRosterAgents: assigned agents are absent from roster", () => {
|
|||||||
story_id: "85_no_agent",
|
story_id: "85_no_agent",
|
||||||
name: "No Agent",
|
name: "No Agent",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -554,6 +567,7 @@ describe("hiddenRosterAgents: assigned agents are absent from roster", () => {
|
|||||||
story_id: "85_transition_test",
|
story_id: "85_transition_test",
|
||||||
name: "Transition",
|
name: "Transition",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -613,6 +627,7 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
|
|||||||
story_id: "85_flyout_hidden",
|
story_id: "85_flyout_hidden",
|
||||||
name: "Fly-out Hidden",
|
name: "Fly-out Hidden",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "completed" },
|
agent: { agent_name: "coder-1", model: null, status: "completed" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -623,6 +638,7 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
|
|||||||
story_id: "85_flyout_hidden",
|
story_id: "85_flyout_hidden",
|
||||||
name: "Fly-out Hidden",
|
name: "Fly-out Hidden",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -664,6 +680,7 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
|
|||||||
story_id: "85_flyout_reveal",
|
story_id: "85_flyout_reveal",
|
||||||
name: "Fly-out Reveal",
|
name: "Fly-out Reveal",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "completed" },
|
agent: { agent_name: "coder-1", model: null, status: "completed" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -674,6 +691,7 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
|
|||||||
story_id: "85_flyout_reveal",
|
story_id: "85_flyout_reveal",
|
||||||
name: "Fly-out Reveal",
|
name: "Fly-out Reveal",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -746,6 +764,7 @@ describe("LozengeFlyProvider agent swap (name change)", () => {
|
|||||||
story_id: "109_swap_test",
|
story_id: "109_swap_test",
|
||||||
name: "Swap Test",
|
name: "Swap Test",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -756,6 +775,7 @@ describe("LozengeFlyProvider agent swap (name change)", () => {
|
|||||||
story_id: "109_swap_test",
|
story_id: "109_swap_test",
|
||||||
name: "Swap Test",
|
name: "Swap Test",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-2", model: "haiku", status: "running" },
|
agent: { agent_name: "coder-2", model: "haiku", status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -835,6 +855,7 @@ describe("LozengeFlyProvider fly-out without roster element", () => {
|
|||||||
story_id: "109_no_roster_flyout",
|
story_id: "109_no_roster_flyout",
|
||||||
name: "No Roster Flyout",
|
name: "No Roster Flyout",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: {
|
agent: {
|
||||||
agent_name: "orphan-agent",
|
agent_name: "orphan-agent",
|
||||||
model: null,
|
model: null,
|
||||||
@@ -849,6 +870,7 @@ describe("LozengeFlyProvider fly-out without roster element", () => {
|
|||||||
story_id: "109_no_roster_flyout",
|
story_id: "109_no_roster_flyout",
|
||||||
name: "No Roster Flyout",
|
name: "No Roster Flyout",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -919,6 +941,7 @@ describe("FlyingLozengeClone initial non-flying render", () => {
|
|||||||
story_id: "109_nontransition_test",
|
story_id: "109_nontransition_test",
|
||||||
name: "Non-transition Test",
|
name: "Non-transition Test",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -993,6 +1016,7 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
|
|||||||
story_id: "137_rapid_swap",
|
story_id: "137_rapid_swap",
|
||||||
name: "Rapid Swap",
|
name: "Rapid Swap",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1003,6 +1027,7 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
|
|||||||
story_id: "137_rapid_swap",
|
story_id: "137_rapid_swap",
|
||||||
name: "Rapid Swap",
|
name: "Rapid Swap",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-2", model: "haiku", status: "running" },
|
agent: { agent_name: "coder-2", model: "haiku", status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1068,6 +1093,7 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
|
|||||||
story_id: "137_reveal_last",
|
story_id: "137_reveal_last",
|
||||||
name: "Reveal Last",
|
name: "Reveal Last",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1078,6 +1104,7 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
|
|||||||
story_id: "137_reveal_last",
|
story_id: "137_reveal_last",
|
||||||
name: "Reveal Last",
|
name: "Reveal Last",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: "coder-2", model: null, status: "running" },
|
agent: { agent_name: "coder-2", model: null, status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -1162,6 +1189,7 @@ describe("Bug 137: animations remain functional through sustained agent activity
|
|||||||
story_id: "137_sustained",
|
story_id: "137_sustained",
|
||||||
name: "Sustained",
|
name: "Sustained",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: { agent_name: agentName, model: null, status: "running" },
|
agent: { agent_name: agentName, model: null, status: "running" },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "42_story_no_agent",
|
story_id: "42_story_no_agent",
|
||||||
name: "No Agent Story",
|
name: "No Agent Story",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -30,6 +31,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "43_story_with_agent",
|
story_id: "43_story_with_agent",
|
||||||
name: "Active Story",
|
name: "Active Story",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: {
|
agent: {
|
||||||
agent_name: "coder-1",
|
agent_name: "coder-1",
|
||||||
model: "sonnet",
|
model: "sonnet",
|
||||||
@@ -48,6 +50,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "44_story_no_model",
|
story_id: "44_story_no_model",
|
||||||
name: "No Model Story",
|
name: "No Model Story",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: {
|
agent: {
|
||||||
agent_name: "coder-2",
|
agent_name: "coder-2",
|
||||||
model: null,
|
model: null,
|
||||||
@@ -65,6 +68,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "45_story_pending",
|
story_id: "45_story_pending",
|
||||||
name: "Pending Story",
|
name: "Pending Story",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: {
|
agent: {
|
||||||
agent_name: "coder-1",
|
agent_name: "coder-1",
|
||||||
model: "haiku",
|
model: "haiku",
|
||||||
@@ -82,6 +86,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "59_story_current_work_panel",
|
story_id: "59_story_current_work_panel",
|
||||||
name: "Current Work Panel",
|
name: "Current Work Panel",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -95,6 +100,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "1_story_bad",
|
story_id: "1_story_bad",
|
||||||
name: null,
|
name: null,
|
||||||
error: "Missing front matter",
|
error: "Missing front matter",
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -108,6 +114,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "10_story_some_feature",
|
story_id: "10_story_some_feature",
|
||||||
name: "Some Feature",
|
name: "Some Feature",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -123,6 +130,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "11_bug_broken_thing",
|
story_id: "11_bug_broken_thing",
|
||||||
name: "Broken Thing",
|
name: "Broken Thing",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -138,6 +146,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "12_spike_investigate_perf",
|
story_id: "12_spike_investigate_perf",
|
||||||
name: "Investigate Perf",
|
name: "Investigate Perf",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -153,6 +162,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "13_task_do_something",
|
story_id: "13_task_do_something",
|
||||||
name: "Do Something",
|
name: "Do Something",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -168,6 +178,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "20_story_uniform_border",
|
story_id: "20_story_uniform_border",
|
||||||
name: "Uniform Border",
|
name: "Uniform Border",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -186,6 +197,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "21_bug_uniform_border",
|
story_id: "21_bug_uniform_border",
|
||||||
name: "Uniform Border Bug",
|
name: "Uniform Border Bug",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -201,6 +213,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "22_spike_uniform_border",
|
story_id: "22_spike_uniform_border",
|
||||||
name: "Uniform Border Spike",
|
name: "Uniform Border Spike",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -216,6 +229,7 @@ describe("StagePanel", () => {
|
|||||||
story_id: "23_task_uniform_border",
|
story_id: "23_task_uniform_border",
|
||||||
name: "Uniform Border Task",
|
name: "Uniform Border Task",
|
||||||
error: null,
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
agent: null,
|
agent: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -224,4 +238,42 @@ describe("StagePanel", () => {
|
|||||||
expect(card.style.borderLeft).not.toContain("3px");
|
expect(card.style.borderLeft).not.toContain("3px");
|
||||||
expect(card.style.borderLeft).toBe(card.style.borderTop);
|
expect(card.style.borderLeft).toBe(card.style.borderTop);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows merge failure icon and reason when merge_failure is set", () => {
|
||||||
|
const items: PipelineStageItem[] = [
|
||||||
|
{
|
||||||
|
story_id: "30_story_merge_failed",
|
||||||
|
name: "Failed Merge Story",
|
||||||
|
error: null,
|
||||||
|
merge_failure: "Squash merge failed: conflicts in Cargo.lock",
|
||||||
|
agent: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
render(<StagePanel title="Merge" items={items} />);
|
||||||
|
expect(
|
||||||
|
screen.getByTestId("merge-failure-icon-30_story_merge_failed"),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.getByTestId("merge-failure-reason-30_story_merge_failed"),
|
||||||
|
).toHaveTextContent("Squash merge failed: conflicts in Cargo.lock");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show merge failure elements when merge_failure is null", () => {
|
||||||
|
const items: PipelineStageItem[] = [
|
||||||
|
{
|
||||||
|
story_id: "31_story_no_failure",
|
||||||
|
name: "Clean Story",
|
||||||
|
error: null,
|
||||||
|
merge_failure: null,
|
||||||
|
agent: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
render(<StagePanel title="Merge" items={items} />);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId("merge-failure-icon-31_story_no_failure"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId("merge-failure-reason-31_story_no_failure"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -166,17 +166,24 @@ export function StagePanel({
|
|||||||
const itemType = getWorkItemType(item.story_id);
|
const itemType = getWorkItemType(item.story_id);
|
||||||
const borderColor = TYPE_COLORS[itemType];
|
const borderColor = TYPE_COLORS[itemType];
|
||||||
const typeLabel = TYPE_LABELS[itemType];
|
const typeLabel = TYPE_LABELS[itemType];
|
||||||
|
const hasMergeFailure = Boolean(item.merge_failure);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${title}-${item.story_id}`}
|
key={`${title}-${item.story_id}`}
|
||||||
data-testid={`card-${item.story_id}`}
|
data-testid={`card-${item.story_id}`}
|
||||||
style={{
|
style={{
|
||||||
border: item.agent
|
border: hasMergeFailure
|
||||||
|
? "1px solid #6e1b1b"
|
||||||
|
: item.agent
|
||||||
? "1px solid #2a3a4a"
|
? "1px solid #2a3a4a"
|
||||||
: "1px solid #2a2a2a",
|
: "1px solid #2a2a2a",
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
padding: "8px 12px",
|
padding: "8px 12px",
|
||||||
background: item.agent ? "#161e2a" : "#191919",
|
background: hasMergeFailure
|
||||||
|
? "#1f1010"
|
||||||
|
: item.agent
|
||||||
|
? "#161e2a"
|
||||||
|
: "#191919",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "2px",
|
gap: "2px",
|
||||||
@@ -184,6 +191,19 @@ export function StagePanel({
|
|||||||
>
|
>
|
||||||
<div style={{ flex: 1 }}>
|
<div style={{ flex: 1 }}>
|
||||||
<div style={{ fontWeight: 600, fontSize: "0.9em" }}>
|
<div style={{ fontWeight: 600, fontSize: "0.9em" }}>
|
||||||
|
{hasMergeFailure && (
|
||||||
|
<span
|
||||||
|
data-testid={`merge-failure-icon-${item.story_id}`}
|
||||||
|
title="Merge failed"
|
||||||
|
style={{
|
||||||
|
color: "#f85149",
|
||||||
|
marginRight: "6px",
|
||||||
|
fontStyle: "normal",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{itemNumber && (
|
{itemNumber && (
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
@@ -222,6 +242,20 @@ export function StagePanel({
|
|||||||
{item.error}
|
{item.error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{item.merge_failure && (
|
||||||
|
<div
|
||||||
|
data-testid={`merge-failure-reason-${item.story_id}`}
|
||||||
|
style={{
|
||||||
|
fontSize: "0.8em",
|
||||||
|
color: "#f85149",
|
||||||
|
marginTop: "4px",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{item.merge_failure}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{item.agent && (
|
{item.agent && (
|
||||||
<AgentLozenge agent={item.agent} storyId={item.story_id} />
|
<AgentLozenge agent={item.agent} storyId={item.story_id} />
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::http::workflow::{
|
|||||||
validate_story_dirs,
|
validate_story_dirs,
|
||||||
};
|
};
|
||||||
use crate::worktree;
|
use crate::worktree;
|
||||||
use crate::io::story_metadata::{parse_front_matter, parse_unchecked_todos};
|
use crate::io::story_metadata::{parse_front_matter, parse_unchecked_todos, write_merge_failure};
|
||||||
use crate::workflow::{evaluate_acceptance_with_coverage, TestCaseResult, TestStatus};
|
use crate::workflow::{evaluate_acceptance_with_coverage, TestCaseResult, TestStatus};
|
||||||
use poem::handler;
|
use poem::handler;
|
||||||
use poem::http::StatusCode;
|
use poem::http::StatusCode;
|
||||||
@@ -1668,6 +1668,28 @@ fn tool_report_merge_failure(args: &Value, ctx: &AppContext) -> Result<String, S
|
|||||||
slog!("[mergemaster] Merge failure reported for '{story_id}': {reason}");
|
slog!("[mergemaster] Merge failure reported for '{story_id}': {reason}");
|
||||||
ctx.agents.set_merge_failure_reported(story_id);
|
ctx.agents.set_merge_failure_reported(story_id);
|
||||||
|
|
||||||
|
// Persist the failure reason to the story file's front matter so it
|
||||||
|
// survives server restarts and is visible in the web UI.
|
||||||
|
if let Ok(project_root) = ctx.state.get_project_root() {
|
||||||
|
let story_file = project_root
|
||||||
|
.join(".story_kit")
|
||||||
|
.join("work")
|
||||||
|
.join("4_merge")
|
||||||
|
.join(format!("{story_id}.md"));
|
||||||
|
if story_file.exists() {
|
||||||
|
if let Err(e) = write_merge_failure(&story_file, reason) {
|
||||||
|
slog_warn!(
|
||||||
|
"[mergemaster] Failed to persist merge_failure to story file for '{story_id}': {e}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
slog_warn!(
|
||||||
|
"[mergemaster] Story file not found in 4_merge/ for '{story_id}'; \
|
||||||
|
merge_failure not persisted to front matter"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
"Merge failure for '{story_id}' recorded. Story remains in work/4_merge/. Reason: {reason}"
|
"Merge failure for '{story_id}' recorded. Story remains in work/4_merge/. Reason: {reason}"
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub struct UpcomingStory {
|
|||||||
pub story_id: String,
|
pub story_id: String,
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
|
/// Merge failure reason persisted to front matter by the mergemaster agent.
|
||||||
|
pub merge_failure: Option<String>,
|
||||||
/// Active agent working on this item, if any.
|
/// Active agent working on this item, if any.
|
||||||
pub agent: Option<AgentAssignment>,
|
pub agent: Option<AgentAssignment>,
|
||||||
}
|
}
|
||||||
@@ -115,12 +117,12 @@ fn load_stage_items(
|
|||||||
.to_string();
|
.to_string();
|
||||||
let contents = fs::read_to_string(&path)
|
let contents = fs::read_to_string(&path)
|
||||||
.map_err(|e| format!("Failed to read story file {}: {e}", path.display()))?;
|
.map_err(|e| format!("Failed to read story file {}: {e}", path.display()))?;
|
||||||
let (name, error) = match parse_front_matter(&contents) {
|
let (name, error, merge_failure) = match parse_front_matter(&contents) {
|
||||||
Ok(meta) => (meta.name, None),
|
Ok(meta) => (meta.name, None, meta.merge_failure),
|
||||||
Err(e) => (None, Some(e.to_string())),
|
Err(e) => (None, Some(e.to_string()), None),
|
||||||
};
|
};
|
||||||
let agent = agent_map.get(&story_id).cloned();
|
let agent = agent_map.get(&story_id).cloned();
|
||||||
stories.push(UpcomingStory { story_id, name, error, agent });
|
stories.push(UpcomingStory { story_id, name, error, merge_failure, agent });
|
||||||
}
|
}
|
||||||
|
|
||||||
stories.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
stories.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
||||||
|
|||||||
@@ -612,6 +612,7 @@ mod tests {
|
|||||||
story_id: "10_story_test".to_string(),
|
story_id: "10_story_test".to_string(),
|
||||||
name: Some("Test".to_string()),
|
name: Some("Test".to_string()),
|
||||||
error: None,
|
error: None,
|
||||||
|
merge_failure: None,
|
||||||
agent: None,
|
agent: None,
|
||||||
};
|
};
|
||||||
let resp = WsResponse::PipelineState {
|
let resp = WsResponse::PipelineState {
|
||||||
@@ -748,12 +749,14 @@ mod tests {
|
|||||||
story_id: "1_story_a".to_string(),
|
story_id: "1_story_a".to_string(),
|
||||||
name: Some("Story A".to_string()),
|
name: Some("Story A".to_string()),
|
||||||
error: None,
|
error: None,
|
||||||
|
merge_failure: None,
|
||||||
agent: None,
|
agent: None,
|
||||||
}],
|
}],
|
||||||
current: vec![UpcomingStory {
|
current: vec![UpcomingStory {
|
||||||
story_id: "2_story_b".to_string(),
|
story_id: "2_story_b".to_string(),
|
||||||
name: Some("Story B".to_string()),
|
name: Some("Story B".to_string()),
|
||||||
error: None,
|
error: None,
|
||||||
|
merge_failure: None,
|
||||||
agent: None,
|
agent: None,
|
||||||
}],
|
}],
|
||||||
qa: vec![],
|
qa: vec![],
|
||||||
@@ -762,6 +765,7 @@ mod tests {
|
|||||||
story_id: "50_story_done".to_string(),
|
story_id: "50_story_done".to_string(),
|
||||||
name: Some("Done Story".to_string()),
|
name: Some("Done Story".to_string()),
|
||||||
error: None,
|
error: None,
|
||||||
|
merge_failure: None,
|
||||||
agent: None,
|
agent: None,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
@@ -913,6 +917,7 @@ mod tests {
|
|||||||
story_id: "10_story_x".to_string(),
|
story_id: "10_story_x".to_string(),
|
||||||
name: Some("Story X".to_string()),
|
name: Some("Story X".to_string()),
|
||||||
error: None,
|
error: None,
|
||||||
|
merge_failure: None,
|
||||||
agent: Some(crate::http::workflow::AgentAssignment {
|
agent: Some(crate::http::workflow::AgentAssignment {
|
||||||
agent_name: "coder-1".to_string(),
|
agent_name: "coder-1".to_string(),
|
||||||
model: Some("claude-3-5-sonnet".to_string()),
|
model: Some("claude-3-5-sonnet".to_string()),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::path::Path;
|
|||||||
pub struct StoryMetadata {
|
pub struct StoryMetadata {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub coverage_baseline: Option<String>,
|
pub coverage_baseline: Option<String>,
|
||||||
|
pub merge_failure: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -27,6 +28,7 @@ impl std::fmt::Display for StoryMetaError {
|
|||||||
struct FrontMatter {
|
struct FrontMatter {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
coverage_baseline: Option<String>,
|
coverage_baseline: Option<String>,
|
||||||
|
merge_failure: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaError> {
|
pub fn parse_front_matter(contents: &str) -> Result<StoryMetadata, StoryMetaError> {
|
||||||
@@ -58,6 +60,7 @@ fn build_metadata(front: FrontMatter) -> StoryMetadata {
|
|||||||
StoryMetadata {
|
StoryMetadata {
|
||||||
name: front.name,
|
name: front.name,
|
||||||
coverage_baseline: front.coverage_baseline,
|
coverage_baseline: front.coverage_baseline,
|
||||||
|
merge_failure: front.merge_failure,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +77,24 @@ pub fn write_coverage_baseline(path: &Path, coverage_pct: f64) -> Result<(), Str
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Write or update a `merge_failure:` field in the YAML front matter of a story file.
|
||||||
|
///
|
||||||
|
/// The reason is stored as a quoted YAML string so that colons, hashes, and newlines
|
||||||
|
/// in the failure message do not break front-matter parsing.
|
||||||
|
/// If no front matter is present, this is a no-op (returns Ok).
|
||||||
|
pub fn write_merge_failure(path: &Path, reason: &str) -> Result<(), String> {
|
||||||
|
let contents =
|
||||||
|
fs::read_to_string(path).map_err(|e| format!("Failed to read story file: {e}"))?;
|
||||||
|
|
||||||
|
// Produce a YAML-safe inline quoted string: collapse newlines, escape inner quotes.
|
||||||
|
let escaped = reason.replace('"', "\\\"").replace('\n', " ").replace('\r', "");
|
||||||
|
let yaml_value = format!("\"{escaped}\"");
|
||||||
|
|
||||||
|
let updated = set_front_matter_field(&contents, "merge_failure", &yaml_value);
|
||||||
|
fs::write(path, &updated).map_err(|e| format!("Failed to write story file: {e}"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert or update a key: value pair in the YAML front matter of a markdown string.
|
/// Insert or update a key: value pair in the YAML front matter of a markdown string.
|
||||||
///
|
///
|
||||||
/// If no front matter (opening `---`) is found, returns the content unchanged.
|
/// If no front matter (opening `---`) is found, returns the content unchanged.
|
||||||
|
|||||||
Reference in New Issue
Block a user