Files
tutti/packages/agent/gui/workspaceWorkflow/tuttiModePlan/useTuttiModePlanPanels.planIssue.spec.tsx
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

308 lines
9.9 KiB
TypeScript

import "@testing-library/jest-dom/vitest";
import { StrictMode } from "react";
import { render, screen, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import {
tuttiPlanIssueExecutionIsActive,
useTuttiModePlanPanels
} from "./useTuttiModePlanPanels";
import {
TuttiModePlanReviewRuntimeProvider,
type TuttiModePlanReviewRuntime,
type TuttiPlanIssueQueryResult,
type TuttiPlanIssueSnapshot,
type TuttiPlanIssueSource
} from "../workspaceWorkflowRuntime";
function snapshotFor(sessionId: string): TuttiPlanIssueSnapshot {
return {
workflowId: `workflow-${sessionId}`,
sourceTurnId: `turn-${sessionId}`,
issueId: `tutti-mode-plan-${sessionId}`,
topicId: "default",
title: `Issue for ${sessionId}`,
dispatchPaused: false,
tasks: [
{
taskId: "task-1",
title: "Only",
content: "",
status: "running",
sortIndex: 1,
parallelizable: false,
autoAccept: false,
dependencyTaskIds: []
}
]
};
}
describe("tuttiPlanIssueExecutionIsActive", () => {
it.each(["not_started", "running", "pending_acceptance"])(
"treats %s work as active while dispatch is open",
(status) => {
const issue = snapshotFor("session-1");
expect(
tuttiPlanIssueExecutionIsActive({
...issue,
tasks: [{ ...issue.tasks[0]!, status }]
})
).toBe(true);
}
);
it("ends aggregate work when dispatch is paused or every task is terminal", () => {
const issue = snapshotFor("session-1");
expect(
tuttiPlanIssueExecutionIsActive({ ...issue, dispatchPaused: true })
).toBe(false);
expect(
tuttiPlanIssueExecutionIsActive({
...issue,
tasks: ["completed", "failed", "canceled"].map((status, index) => ({
...issue.tasks[0]!,
taskId: `task-${index}`,
status
}))
})
).toBe(false);
});
});
interface DeferredLoad {
sessionId: string;
resolve: (result: TuttiPlanIssueQueryResult) => void;
}
function createHarness(): {
runtime: TuttiModePlanReviewRuntime;
loads: DeferredLoad[];
emitIssueUpdate: (issueId: string) => void;
emitWorkflowUpdate: (sourceSessionId: string) => void;
emitSessionSettled: (sourceSessionId: string) => void;
} {
const loads: DeferredLoad[] = [];
const listeners = new Set<(update: { issueId: string }) => void>();
const workflowListeners = new Set<
Parameters<TuttiModePlanReviewRuntime["subscribe"]>[1]
>();
const source: TuttiPlanIssueSource = {
getSessionPlanIssue({ sourceSessionId }) {
return new Promise((resolve) => {
loads.push({ sessionId: sourceSessionId, resolve });
});
},
subscribeIssueUpdates(_workspaceId, listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
cancelExecution: vi.fn().mockResolvedValue(undefined),
resolveTaskSession: vi.fn().mockResolvedValue(null)
};
const runtime: TuttiModePlanReviewRuntime = {
listPending: vi.fn().mockResolvedValue([]),
decide: vi.fn().mockResolvedValue(undefined),
subscribe: (_workspaceId, listener) => {
workflowListeners.add(listener);
return () => workflowListeners.delete(listener);
},
planIssues: source
};
return {
runtime,
loads,
emitIssueUpdate: (issueId) => {
for (const listener of listeners) listener({ issueId });
},
emitWorkflowUpdate: (sourceSessionId) => {
for (const listener of workflowListeners) {
listener({
kind: "workflow_updated",
workspaceId: "workspace-1",
workflowId: "workflow-1",
sourceSessionId,
checkpointId: "checkpoint-1",
changeKind: "operation_updated"
});
}
},
emitSessionSettled: (sourceSessionId) => {
for (const listener of workflowListeners) {
listener({
kind: "session_settled",
workspaceId: "workspace-1",
sourceSessionId
});
}
}
};
}
function Probe({ sessionId }: { sessionId: string }): React.JSX.Element {
const { planIssue } = useTuttiModePlanPanels({
decidedBy: "user-1",
workspaceId: "workspace-1",
sourceSessionId: sessionId
});
return (
<div data-testid="probe">{planIssue ? planIssue.issueId : "none"}</div>
);
}
// The plan-issue embed rides the review panels hook lifecycle (one scope, one
// effect); these probes pin the load/refresh semantics the embedded panel
// depends on.
describe("useTuttiModePlanPanels plan issue embed", () => {
it("surfaces the loaded issue under StrictMode double-mount", async () => {
const harness = createHarness();
render(
<StrictMode>
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-a" />
</TuttiModePlanReviewRuntimeProvider>
</StrictMode>
);
await waitFor(() => expect(harness.loads.length).toBeGreaterThan(0));
for (const load of [...harness.loads]) {
load.resolve({ kind: "issue", issue: snapshotFor(load.sessionId) });
}
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent(
"tutti-mode-plan-session-a"
)
);
});
it("re-reads the plan issue on workflow updates for this session", async () => {
// workspace.issue.updated fires during materialization, before the
// create_issue operation records its outcome; the workflow's
// operation_updated event is the authoritative post-completion signal.
// Without this re-read, an accepted plan's issue panel stays invisible
// until a remount.
const harness = createHarness();
render(
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-a" />
</TuttiModePlanReviewRuntimeProvider>
);
await waitFor(() => expect(harness.loads.length).toBe(1));
harness.loads[0]!.resolve(null);
harness.emitWorkflowUpdate("session-a");
await waitFor(() => expect(harness.loads.length).toBe(2));
harness.loads[1]!.resolve({
kind: "issue",
issue: snapshotFor("session-a")
});
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent(
"tutti-mode-plan-session-a"
)
);
// Another session's workflow event must not trigger a read for this one.
harness.emitWorkflowUpdate("session-b");
await new Promise((resolve) => setTimeout(resolve, 20));
expect(harness.loads.length).toBe(2);
});
it("re-reads the plan issue when this session's turn settles", async () => {
// A plan proposed mid-turn announces itself through one workflow event;
// if that single event is dropped, the host-relayed session_settled
// invalidation is the read-repair point.
const harness = createHarness();
render(
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-a" />
</TuttiModePlanReviewRuntimeProvider>
);
await waitFor(() => expect(harness.loads.length).toBe(1));
harness.loads[0]!.resolve(null);
harness.emitSessionSettled("session-a");
await waitFor(() => expect(harness.loads.length).toBe(2));
harness.loads[1]!.resolve({
kind: "issue",
issue: snapshotFor("session-a")
});
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent(
"tutti-mode-plan-session-a"
)
);
// Another session's settle must not trigger a read for this one.
harness.emitSessionSettled("session-b");
await new Promise((resolve) => setTimeout(resolve, 20));
expect(harness.loads.length).toBe(2);
});
it("loads the new scope when the conversation flips mid-flight", async () => {
const harness = createHarness();
const view = render(
<StrictMode>
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-a" />
</TuttiModePlanReviewRuntimeProvider>
</StrictMode>
);
await waitFor(() => expect(harness.loads.length).toBeGreaterThan(0));
const pendingA = [...harness.loads];
view.rerender(
<StrictMode>
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-b" />
</TuttiModePlanReviewRuntimeProvider>
</StrictMode>
);
// The stale loads resolve late and must be discarded.
for (const load of pendingA) {
load.resolve({ kind: "issue", issue: snapshotFor(load.sessionId) });
}
await waitFor(() =>
expect(
harness.loads.filter((load) => load.sessionId === "session-b").length
).toBeGreaterThan(0)
);
for (const load of harness.loads.filter(
(load) => load.sessionId === "session-b"
)) {
load.resolve({ kind: "issue", issue: snapshotFor(load.sessionId) });
}
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent(
"tutti-mode-plan-session-b"
)
);
});
it("retries via issue updates after an initially empty load", async () => {
const harness = createHarness();
render(
<StrictMode>
<TuttiModePlanReviewRuntimeProvider runtime={harness.runtime}>
<Probe sessionId="session-a" />
</TuttiModePlanReviewRuntimeProvider>
</StrictMode>
);
await waitFor(() => expect(harness.loads.length).toBeGreaterThan(0));
for (const load of [...harness.loads]) {
load.resolve(null);
}
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent("none")
);
const before = harness.loads.length;
harness.emitIssueUpdate("tutti-mode-plan-session-a");
await waitFor(() => expect(harness.loads.length).toBeGreaterThan(before));
for (const load of harness.loads.slice(before)) {
load.resolve({ kind: "issue", issue: snapshotFor(load.sessionId) });
}
await waitFor(() =>
expect(screen.getByTestId("probe")).toHaveTextContent(
"tutti-mode-plan-session-a"
)
);
});
});