feat(workstate): dette de contrat DTO work-state (frontend)
Aligne le contrat DTO du panneau work-state (#5). - ajout summary: string|null à BackgroundCompletion, mappé depuis task.summary - affichage discret du résumé sur une tâche terminale - suppression du chemin de merge top-level mort des background tasks Tests verts : vitest workstate.test.tsx (21), suite complète (507), npm run build (exit 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -104,6 +104,11 @@ function formatTaskStatus(status: BackgroundCompletion["status"]): string {
|
||||
}
|
||||
}
|
||||
|
||||
/** Terminal (finished) task states that carry a result summary (ticket #5). */
|
||||
function isTerminalTaskStatus(status: BackgroundCompletion["status"]): boolean {
|
||||
return status === "completed" || status === "failed" || status === "cancelled";
|
||||
}
|
||||
|
||||
function taskStatusClass(status: BackgroundCompletion["status"]): string {
|
||||
if (status === "running" || status === "pending") return "bg-warning/15 text-warning";
|
||||
if (status === "completed" || status === "delivered") return "bg-success/10 text-success";
|
||||
@ -407,6 +412,15 @@ function BackgroundTaskRow({
|
||||
{shortTicket(task.taskId)}
|
||||
</code>
|
||||
</div>
|
||||
{task.summary && isTerminalTaskStatus(task.status) && (
|
||||
<p
|
||||
aria-label={`task ${shortTicket(task.taskId)} summary`}
|
||||
title={task.summary}
|
||||
className="mt-1 truncate pl-1 text-[11px] text-muted"
|
||||
>
|
||||
{task.summary}
|
||||
</p>
|
||||
)}
|
||||
{message && (
|
||||
<p role="alert" className="mt-1 text-xs text-danger">
|
||||
{message}
|
||||
|
||||
@ -208,6 +208,7 @@ describe("ProjectWorkStatePanel", () => {
|
||||
kind: "command",
|
||||
state: "failed",
|
||||
exitCode: 1,
|
||||
summary: "process failed",
|
||||
stderrTail: "boom",
|
||||
createdAtMs: 14,
|
||||
updatedAtMs: 15,
|
||||
@ -236,7 +237,10 @@ describe("ProjectWorkStatePanel", () => {
|
||||
// Optional fields omitted by the backend normalize to null (not undefined/NaN).
|
||||
expect(tasks[0]?.exitCode).toBeNull();
|
||||
expect(tasks[0]?.stdoutTail).toBeNull();
|
||||
expect(tasks[0]?.summary).toBeNull();
|
||||
expect(tasks[2]?.stderrTail).toBe("boom");
|
||||
// `summary` (ticket #5) is carried through when present.
|
||||
expect(tasks[2]?.summary).toBe("process failed");
|
||||
// updatedAtMs (backend-emitted) is carried through for chronological ordering.
|
||||
expect(tasks.map((t) => t.updatedAtMs)).toEqual([11, 13, 15, 17]);
|
||||
|
||||
@ -259,6 +263,72 @@ describe("ProjectWorkStatePanel", () => {
|
||||
expect(order.every((i) => i >= 0)).toBe(true);
|
||||
});
|
||||
|
||||
it("shows a terminal task summary and hides it when absent or non-terminal (#5)", async () => {
|
||||
const workState = new MockWorkStateGateway();
|
||||
workState._setProjectWorkState(PROJECT_ID, {
|
||||
agents: [
|
||||
{
|
||||
agentId: "agent-summary",
|
||||
name: "Reporter",
|
||||
profileId: "codex",
|
||||
busy: { state: "idle" },
|
||||
backgroundTasks: [
|
||||
{
|
||||
taskId: "run-with-summary",
|
||||
kind: "command",
|
||||
state: "running",
|
||||
summary: "should-not-appear-while-running",
|
||||
createdAtMs: 40,
|
||||
updatedAtMs: 40,
|
||||
},
|
||||
{
|
||||
taskId: "done-with-summary",
|
||||
kind: "command",
|
||||
state: "completed",
|
||||
summary: "Build succeeded in 12s",
|
||||
createdAtMs: 30,
|
||||
updatedAtMs: 30,
|
||||
},
|
||||
{
|
||||
taskId: "fail-with-summary",
|
||||
kind: "command",
|
||||
state: "failed",
|
||||
exitCode: 1,
|
||||
summary: "Compile error: missing semicolon",
|
||||
createdAtMs: 20,
|
||||
updatedAtMs: 20,
|
||||
},
|
||||
{
|
||||
taskId: "done-no-summary",
|
||||
kind: "command",
|
||||
state: "completed",
|
||||
createdAtMs: 10,
|
||||
updatedAtMs: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
renderPanel(workState);
|
||||
|
||||
const list = await screen.findByLabelText("Reporter background tasks");
|
||||
// Terminal tasks with a summary render it discreetly.
|
||||
expect(
|
||||
within(list).getByLabelText("task done-wit summary").textContent,
|
||||
).toBe("Build succeeded in 12s");
|
||||
expect(
|
||||
within(list).getByText("Compile error: missing semicolon"),
|
||||
).toBeTruthy();
|
||||
// A completed task without a summary shows no summary line.
|
||||
expect(within(list).queryByLabelText("task done-no- summary")).toBeNull();
|
||||
// A still-running task never shows its (non-terminal) summary.
|
||||
expect(within(list).queryByLabelText("task run-with summary")).toBeNull();
|
||||
expect(
|
||||
within(list).queryByText("should-not-appear-while-running"),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it("renders a legacy agent without tickets", async () => {
|
||||
const workState = new MockWorkStateGateway();
|
||||
workState._setProjectWorkState(PROJECT_ID, {
|
||||
@ -280,6 +350,47 @@ describe("ProjectWorkStatePanel", () => {
|
||||
expect(screen.queryByLabelText("Legacy tickets")).toBeNull();
|
||||
});
|
||||
|
||||
it("ignores a stray top-level backgroundTasks array (nested tasks intact) (#5)", async () => {
|
||||
// The backend nests tasks under their agent and emits no `ownerAgentId`; a
|
||||
// project-root `backgroundTasks` array is not part of the contract and must
|
||||
// not be merged in (dead path removed in ticket #5).
|
||||
const workState = new MockWorkStateGateway();
|
||||
workState._setProjectWorkState(PROJECT_ID, {
|
||||
agents: [
|
||||
{
|
||||
agentId: "agent-nested",
|
||||
name: "Nested",
|
||||
profileId: "codex",
|
||||
busy: { state: "idle" },
|
||||
backgroundTasks: [
|
||||
{
|
||||
taskId: "nested-1",
|
||||
kind: "command",
|
||||
state: "completed",
|
||||
createdAtMs: 1,
|
||||
updatedAtMs: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
backgroundTasks: [
|
||||
{
|
||||
taskId: "orphan-top-level",
|
||||
kind: "command",
|
||||
state: "completed",
|
||||
ownerAgentId: "agent-nested",
|
||||
createdAtMs: 3,
|
||||
updatedAtMs: 4,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const state = await workState.getProjectWorkState(PROJECT_ID);
|
||||
expect((state.agents[0]?.backgroundTasks ?? []).map((t) => t.taskId)).toEqual(
|
||||
["nested-1"],
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes missing top-level work state arrays", async () => {
|
||||
const workState = new MockWorkStateGateway();
|
||||
workState._setProjectWorkState(PROJECT_ID, {});
|
||||
|
||||
Reference in New Issue
Block a user