taskchef 8.7.1 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +7 -10
- package/.mcp.json +3 -2
- package/README.md +31 -158
- package/docs/mcp-app.md +3 -3
- package/docs/spec.md +38 -746
- package/docs/taskchef-next-storage.md +2 -2
- package/index.js +2 -99
- package/package.json +12 -49
- package/skills/delegate/SKILL.md +44 -0
- package/skills/delegate/agents/openai.yaml +4 -0
- package/src/codex-app.js +3 -152
- package/src/codex-session-scanner.js +2 -2
- package/src/file-lock.js +25 -0
- package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
- package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
- package/src/mcp-app/react/main.test.tsx +3 -3
- package/src/mcp-app/react/main.tsx +7 -7
- package/src/mcp-app.js +2 -2
- package/src/mcp.js +4 -542
- package/src/next-cache.js +2 -2
- package/src/next-github.js +3 -3
- package/src/next-notifications.js +2 -2
- package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
- package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
- package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
- package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
- package/src/{dashboard/react → ui}/presentation.ts +1 -1
- package/src/version.js +0 -1
- package/BACKLOG.md +0 -48
- package/assets/e2e-benchmark-example.json +0 -50
- package/assets/taskchef-dispatcher-instructions.md +0 -44
- package/bin/taskchef.js +0 -20
- package/docs/advanced-guide.md +0 -497
- package/docs/dashboard-lifecycle.md +0 -149
- package/docs/dashboard-ui.md +0 -40
- package/docs/firstmate-taskchef-comparison.md +0 -283
- package/docs/images/ccusage-token-consumption.png +0 -0
- package/docs/images/dashboard-identity.jpg +0 -0
- package/docs/images/dashboard-token-pending.jpg +0 -0
- package/docs/images/interrupted-turn-recovery.jpg +0 -0
- package/docs/images/notification-event-snapshots.jpg +0 -0
- package/docs/images/readme-board.png +0 -0
- package/docs/images/readme-completed.png +0 -0
- package/docs/images/readme-list.png +0 -0
- package/docs/images/readme-needs-input.png +0 -0
- package/docs/images/readme-roles.png +0 -0
- package/docs/images/reported-work-duration-desktop.jpg +0 -0
- package/docs/images/reported-work-duration-mobile.jpg +0 -0
- package/docs/images/result-history-dashboard.jpg +0 -0
- package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
- package/docs/images/taskchef-unified-status-popup.jpg +0 -0
- package/docs/workflows.md +0 -476
- package/mcp/dashboard-session.js +0 -51
- package/scripts/benchmark-dispatch-prepare.js +0 -90
- package/scripts/e2e-benchmark.js +0 -206
- package/scripts/roles/resolve_roles.py +0 -293
- package/skills/taskchef-bootstrap/SKILL.md +0 -96
- package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
- package/skills/taskchef-bootstrap/references/project-index.md +0 -154
- package/skills/taskchef-copilot/SKILL.md +0 -75
- package/skills/taskchef-copilot/agents/openai.yaml +0 -4
- package/skills/taskchef-dashboard/SKILL.md +0 -24
- package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/SKILL.md +0 -102
- package/skills/taskchef-delegate/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/references/model-roles.md +0 -29
- package/skills/taskchef-executor/SKILL.md +0 -154
- package/skills/taskchef-executor/agents/openai.yaml +0 -4
- package/skills/taskchef-executor/references/compatibility.md +0 -22
- package/skills/taskchef-executor/references/ending-actions.md +0 -26
- package/src/backups.js +0 -359
- package/src/cli.js +0 -733
- package/src/config-audit.js +0 -99
- package/src/config-mutations.js +0 -106
- package/src/dashboard/actions.js +0 -136
- package/src/dashboard/app.js +0 -1172
- package/src/dashboard/dist/app.js +0 -269
- package/src/dashboard/dist/app.js.map +0 -1
- package/src/dashboard/dist/index.html +0 -16
- package/src/dashboard/dist/styles.css +0 -1
- package/src/dashboard/index.html +0 -148
- package/src/dashboard/react/App.test.tsx +0 -346
- package/src/dashboard/react/App.tsx +0 -515
- package/src/dashboard/react/api.test.ts +0 -40
- package/src/dashboard/react/api.ts +0 -103
- package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
- package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
- package/src/dashboard/react/components/ModelSettings.tsx +0 -106
- package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
- package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
- package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
- package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
- package/src/dashboard/react/components/TaskDetail.tsx +0 -271
- package/src/dashboard/react/components/UsagePanel.tsx +0 -96
- package/src/dashboard/react/index.html +0 -15
- package/src/dashboard/react/main.tsx +0 -12
- package/src/dashboard/react/overlay-state.test.ts +0 -110
- package/src/dashboard/react/overlay-state.ts +0 -60
- package/src/dashboard/react/presentation.test.tsx +0 -571
- package/src/dashboard/styles.css +0 -261
- package/src/dashboard-control.js +0 -34
- package/src/dashboard-manager.js +0 -339
- package/src/dashboard-session-process.js +0 -88
- package/src/dashboard-session.js +0 -86
- package/src/dashboard.js +0 -1303
- package/src/delegation.js +0 -277
- package/src/github.js +0 -107
- package/src/model-roles.js +0 -70
- package/src/recorded-usage-tracker.js +0 -92
- package/src/usage-tracker.js +0 -650
- package/src/usage.js +0 -367
- package/src/workspace-path.js +0 -48
- package/src/workspace.js +0 -3629
- /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
- /package/src/{dashboard → ui}/github-links.js +0 -0
- /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
- /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
- /package/src/{dashboard → ui}/state.js +0 -0
- /package/src/{dashboard/react → ui}/styles.css +0 -0
- /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
- /package/src/{dashboard → ui}/time.js +0 -0
- /package/src/{dashboard/react → ui}/types.ts +0 -0
|
@@ -1,571 +0,0 @@
|
|
|
1
|
-
import { MantineProvider } from "@mantine/core";
|
|
2
|
-
import { cleanup, render, screen, within } from "@testing-library/react";
|
|
3
|
-
import { afterEach, describe, expect, test } from "vitest";
|
|
4
|
-
import { fixtureTask } from "./fixtures";
|
|
5
|
-
import {
|
|
6
|
-
durationBetween,
|
|
7
|
-
formatCompactTokens,
|
|
8
|
-
formatEstimatedCost,
|
|
9
|
-
formatReportedDuration,
|
|
10
|
-
taskReportedWorkView,
|
|
11
|
-
turnReportedWorkView,
|
|
12
|
-
turnUsageMetricsView,
|
|
13
|
-
turnUsageView,
|
|
14
|
-
usageStillCalculating,
|
|
15
|
-
usageView,
|
|
16
|
-
} from "./presentation";
|
|
17
|
-
import { ShimmerText } from "./components/ShimmerText";
|
|
18
|
-
import { GitHubLinks } from "./components/GitHubLinks";
|
|
19
|
-
import { LinkedText } from "./components/LinkedText";
|
|
20
|
-
import { UsagePanel } from "./components/UsagePanel";
|
|
21
|
-
import { TaskCard } from "./components/TaskCard";
|
|
22
|
-
import { TaskBoard } from "./components/TaskBoard";
|
|
23
|
-
|
|
24
|
-
afterEach(cleanup);
|
|
25
|
-
|
|
26
|
-
test("Next cards show saved replies and keep diagnostics off the card", () => {
|
|
27
|
-
const parent = fixtureTask({
|
|
28
|
-
status: "needs_input", updatedBy: "Local Codex database", replyExcerpt: "**CI passed.** The PR is ready for review.",
|
|
29
|
-
summary: "Latest turn ended. Chat remains open for input or review.",
|
|
30
|
-
observed: { archive: false, lastTurnEvent: "completed", lastTurnEventAt: null, recentFileActivity: false, directChildCount: 133 },
|
|
31
|
-
});
|
|
32
|
-
const empty = fixtureTask({ id: "empty", title: "Empty", replyExcerpt: null, observed: { archive: false, lastTurnEvent: "inProgress", lastTurnEventAt: null, recentFileActivity: true, directChildCount: 0 } });
|
|
33
|
-
render(<MantineProvider>
|
|
34
|
-
<TaskCard task={parent} onOpenCodex={() => {}} onOpenDetail={() => {}} />
|
|
35
|
-
<TaskBoard tasks={[parent, empty]} completedLimit={5} onMoreCompleted={() => {}} onOpenCodex={() => {}} onOpenDetail={() => {}} />
|
|
36
|
-
</MantineProvider>);
|
|
37
|
-
expect(screen.getAllByText("CI passed.")).toHaveLength(2);
|
|
38
|
-
expect(screen.queryByText(/direct subagent/)).not.toBeInTheDocument();
|
|
39
|
-
expect(screen.queryByText(/Latest turn ended/)).not.toBeInTheDocument();
|
|
40
|
-
expect(screen.queryByText("No reply text to show for this turn.")).not.toBeInTheDocument();
|
|
41
|
-
expect(screen.queryByText("Request")).not.toBeInTheDocument();
|
|
42
|
-
expect(screen.queryByText("Result")).not.toBeInTheDocument();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("observed list cards show the prompt while running and the response otherwise", () => {
|
|
46
|
-
const task = fixtureTask({ status: "working", promptExcerpt: "Current request", replyExcerpt: "Previous result", observed: { archive: false, lastTurnEvent: "inProgress", lastTurnEventAt: null, recentFileActivity: true, directChildCount: 0 } });
|
|
47
|
-
const { rerender } = render(<MantineProvider><TaskCard task={task} onOpenCodex={() => {}} onOpenDetail={() => {}} /></MantineProvider>);
|
|
48
|
-
expect(screen.getByText("Current request")).toBeInTheDocument();
|
|
49
|
-
expect(screen.queryByText("Previous result")).not.toBeInTheDocument();
|
|
50
|
-
rerender(<MantineProvider><TaskCard task={{ ...task, status: "needs_input" }} onOpenCodex={() => {}} onOpenDetail={() => {}} /></MantineProvider>);
|
|
51
|
-
expect(screen.getByText("Previous result")).toBeInTheDocument();
|
|
52
|
-
expect(screen.queryByText("Current request")).not.toBeInTheDocument();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
describe("token and working presentation", () => {
|
|
56
|
-
test("keeps pending, calculating, ready cost, and unavailable wording distinct", () => {
|
|
57
|
-
expect(usageView(fixtureTask()).label).toBe("Token usage pending");
|
|
58
|
-
expect(usageView(fixtureTask({
|
|
59
|
-
status: "completed",
|
|
60
|
-
usage: { generationTurnRef: "turn-one", status: "calculating" },
|
|
61
|
-
})).label).toBe("Calculating token usage");
|
|
62
|
-
expect(usageView(fixtureTask({
|
|
63
|
-
status: "completed",
|
|
64
|
-
usage: {
|
|
65
|
-
generationTurnRef: "turn-one",
|
|
66
|
-
status: "available",
|
|
67
|
-
task: { totalTokens: 12345, estimatedCostUsd: 0.1234 },
|
|
68
|
-
},
|
|
69
|
-
})).label).toBe("12.35K tokens · Estimated cost $0.12");
|
|
70
|
-
expect(usageView(fixtureTask({ status: "completed", usage: { status: "unavailable" } })).label)
|
|
71
|
-
.toBe("Token usage unavailable");
|
|
72
|
-
expect(usageView(fixtureTask({
|
|
73
|
-
status: "completed",
|
|
74
|
-
usage: { status: "unavailable", reason: "This task has no linked Codex thread." },
|
|
75
|
-
})).label).toBe("This task has no linked Codex thread.");
|
|
76
|
-
expect(usageView(fixtureTask({ status: "completed", usage: undefined })).label)
|
|
77
|
-
.toBe("Calculating token usage");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("keeps cumulative usage visible as known so far during a newer working turn", () => {
|
|
81
|
-
const view = usageView(fixtureTask({
|
|
82
|
-
status: "working",
|
|
83
|
-
turnRef: "turn-two",
|
|
84
|
-
usage: {
|
|
85
|
-
generationTurnRef: "turn-one",
|
|
86
|
-
status: "calculating",
|
|
87
|
-
task: { totalTokens: 12345, estimatedCostUsd: 0.1234 },
|
|
88
|
-
},
|
|
89
|
-
}));
|
|
90
|
-
expect(view.kind).toBe("ready");
|
|
91
|
-
expect(view.label).toBe("12.35K tokens · Estimated cost $0.12 · known so far");
|
|
92
|
-
if (view.kind === "ready") expect(view.knownSoFar).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
test("keeps cumulative usage visible while a newer terminal generation is calculating", () => {
|
|
96
|
-
const view = usageView(fixtureTask({
|
|
97
|
-
status: "completed",
|
|
98
|
-
turnRef: "turn-two",
|
|
99
|
-
usage: {
|
|
100
|
-
generationTurnRef: "turn-one",
|
|
101
|
-
status: "calculating",
|
|
102
|
-
task: { totalTokens: 12345, estimatedCostUsd: 0.1234 },
|
|
103
|
-
},
|
|
104
|
-
}));
|
|
105
|
-
expect(view.kind).toBe("ready");
|
|
106
|
-
expect(view.label).toBe("12.35K tokens · Estimated cost $0.12 · known so far");
|
|
107
|
-
if (view.kind === "ready") expect(view.knownSoFar).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("distinguishes available, pending, calculating, and unavailable per-turn usage", () => {
|
|
111
|
-
const task = fixtureTask({
|
|
112
|
-
usage: {
|
|
113
|
-
status: "available",
|
|
114
|
-
turns: {
|
|
115
|
-
"turn-one": { status: "available", totalTokens: 330, estimatedCostUsd: 0.12 },
|
|
116
|
-
},
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
const completedTurn = {
|
|
120
|
-
...task.turns![0],
|
|
121
|
-
result: {
|
|
122
|
-
status: "completed" as const,
|
|
123
|
-
summary: "Done.",
|
|
124
|
-
updatedAt: task.updatedAt,
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
expect(turnUsageView(task, completedTurn).label).toBe("330 tokens · Estimated cost $0.12");
|
|
128
|
-
expect(turnUsageView(task, { ...completedTurn, result: null }).label).toBe("Turn usage pending");
|
|
129
|
-
expect(turnUsageView({
|
|
130
|
-
...task,
|
|
131
|
-
usage: { status: "calculating", turns: { "turn-one": { status: "calculating" } } },
|
|
132
|
-
}, completedTurn).label).toBe("Calculating turn usage");
|
|
133
|
-
expect(turnUsageView({ ...task, usage: { status: "unavailable", turns: {} } }, completedTurn).label)
|
|
134
|
-
.toBe("Turn usage unavailable");
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
test("polling is reserved for terminal usage that is actually calculating", () => {
|
|
138
|
-
expect(usageStillCalculating(fixtureTask())).toBe(false);
|
|
139
|
-
expect(usageStillCalculating(fixtureTask({
|
|
140
|
-
status: "completed",
|
|
141
|
-
usage: { generationTurnRef: "turn-one", status: "calculating" },
|
|
142
|
-
}))).toBe(true);
|
|
143
|
-
expect(usageStillCalculating(fixtureTask({
|
|
144
|
-
status: "completed",
|
|
145
|
-
usage: { status: "unavailable", turns: { old: { status: "calculating" } } },
|
|
146
|
-
}))).toBe(true);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("marks working text with the approved text shimmer state", () => {
|
|
150
|
-
render(<ShimmerText>In progress</ShimmerText>);
|
|
151
|
-
expect(screen.getByText("In progress")).toHaveClass("taskchef-shimmer");
|
|
152
|
-
expect(screen.getByText("In progress")).toHaveAttribute("data-animation", "text-shimmer");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test("summarizes ready usage and reported work as one operator-facing metric group", () => {
|
|
156
|
-
const completedTurn = {
|
|
157
|
-
...fixtureTask().turns![0],
|
|
158
|
-
result: {
|
|
159
|
-
status: "completed" as const,
|
|
160
|
-
summary: "Done.",
|
|
161
|
-
updatedAt: "2026-08-30T08:18:32.000Z",
|
|
162
|
-
},
|
|
163
|
-
};
|
|
164
|
-
render(
|
|
165
|
-
<MantineProvider>
|
|
166
|
-
<UsagePanel task={fixtureTask({
|
|
167
|
-
status: "completed",
|
|
168
|
-
turns: [completedTurn],
|
|
169
|
-
usage: {
|
|
170
|
-
generationTurnRef: "turn-one",
|
|
171
|
-
status: "available",
|
|
172
|
-
task: {
|
|
173
|
-
inputTokens: 100,
|
|
174
|
-
cachedInputTokens: 200,
|
|
175
|
-
outputTokens: 30,
|
|
176
|
-
reasoningOutputTokens: 10,
|
|
177
|
-
totalTokens: 330,
|
|
178
|
-
estimatedCostUsd: 0.12,
|
|
179
|
-
models: { "gpt-5.6-sol": {}, "gpt-5.6-luna": {} },
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
})} />
|
|
183
|
-
</MantineProvider>,
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
expect(screen.getByText("Tokens").nextSibling).toHaveTextContent("330");
|
|
187
|
-
expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("$0.12");
|
|
188
|
-
expect(screen.getByText("Duration").nextSibling).toHaveTextContent("18m 32s");
|
|
189
|
-
expect(screen.getByLabelText(/reported work 18m 32s.*wall-clock/i)).toBeVisible();
|
|
190
|
-
expect(screen.getByText("Models").nextSibling).toHaveTextContent("gpt-5.6-sol, gpt-5.6-luna");
|
|
191
|
-
expect(screen.queryByText("Cache ratio")).not.toBeInTheDocument();
|
|
192
|
-
expect(screen.getByText("Tokens").closest(".taskchef-metrics")?.children).toHaveLength(4);
|
|
193
|
-
expect(screen.queryByText(/API-equivalent/i)).not.toBeInTheDocument();
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("uses n/a for missing ready usage metrics with explanatory accessibility", () => {
|
|
197
|
-
render(
|
|
198
|
-
<MantineProvider>
|
|
199
|
-
<UsagePanel task={fixtureTask({
|
|
200
|
-
status: "completed",
|
|
201
|
-
usage: {
|
|
202
|
-
status: "available",
|
|
203
|
-
task: { totalTokens: 330, estimatedCostUsd: null },
|
|
204
|
-
},
|
|
205
|
-
})} />
|
|
206
|
-
</MantineProvider>,
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent("n/a");
|
|
210
|
-
expect(screen.getByLabelText("Estimated cost unavailable")).toBeVisible();
|
|
211
|
-
expect(screen.getByText("Models").nextSibling).toHaveTextContent("n/a");
|
|
212
|
-
expect(screen.getByLabelText("Models unavailable")).toBeVisible();
|
|
213
|
-
expect(screen.queryByText("Cache ratio")).not.toBeInTheDocument();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
test("shimmers known usage values during a running turn without changing card titles", () => {
|
|
217
|
-
const { container, rerender } = render(
|
|
218
|
-
<MantineProvider>
|
|
219
|
-
<UsagePanel task={fixtureTask({
|
|
220
|
-
usage: {
|
|
221
|
-
status: "available",
|
|
222
|
-
task: { totalTokens: 330, estimatedCostUsd: 0.12 },
|
|
223
|
-
},
|
|
224
|
-
})} />
|
|
225
|
-
</MantineProvider>,
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
expect(screen.getByText("Tokens")).toBeVisible();
|
|
229
|
-
expect(screen.queryByText(/known so far/i)).not.toBeInTheDocument();
|
|
230
|
-
expect(container.querySelectorAll(".taskchef-shimmer")).toHaveLength(2);
|
|
231
|
-
|
|
232
|
-
rerender(
|
|
233
|
-
<MantineProvider>
|
|
234
|
-
<UsagePanel task={fixtureTask({
|
|
235
|
-
usage: {
|
|
236
|
-
status: "available",
|
|
237
|
-
task: { totalTokens: 330, estimatedCostUsd: null },
|
|
238
|
-
},
|
|
239
|
-
})} />
|
|
240
|
-
</MantineProvider>,
|
|
241
|
-
);
|
|
242
|
-
expect(container.querySelectorAll(".taskchef-shimmer")).toHaveLength(1);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test("keeps reported work visible across pending, calculating, and unavailable usage", () => {
|
|
246
|
-
for (const [task, expected, accessibleCost] of [
|
|
247
|
-
[fixtureTask(), "Pending", "Estimated cost pending"],
|
|
248
|
-
[
|
|
249
|
-
fixtureTask({ status: "completed", usage: { status: "calculating" } }),
|
|
250
|
-
"Calculating",
|
|
251
|
-
"Estimated cost calculating",
|
|
252
|
-
],
|
|
253
|
-
[
|
|
254
|
-
fixtureTask({ status: "completed", usage: { status: "unavailable" } }),
|
|
255
|
-
"n/a",
|
|
256
|
-
"Estimated cost unavailable: Token usage unavailable",
|
|
257
|
-
],
|
|
258
|
-
] as const) {
|
|
259
|
-
render(<MantineProvider><UsagePanel task={task} /></MantineProvider>);
|
|
260
|
-
expect(screen.getByText("Tokens").nextSibling).toHaveTextContent(expected);
|
|
261
|
-
expect(screen.getByText("Estimated cost").nextSibling).toHaveTextContent(expected);
|
|
262
|
-
expect(screen.getByLabelText(accessibleCost)).toBeVisible();
|
|
263
|
-
expect(screen.getByText("Duration").nextSibling).toHaveTextContent("n/a");
|
|
264
|
-
expect(screen.getByText("Models").nextSibling).toHaveTextContent("n/a");
|
|
265
|
-
expect(screen.getByText("Tokens").closest(".taskchef-metrics")?.children).toHaveLength(4);
|
|
266
|
-
cleanup();
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
test("uses the same borderless link treatment wherever links are rendered", () => {
|
|
271
|
-
render(<MantineProvider><GitHubLinks task={fixtureTask()} /></MantineProvider>);
|
|
272
|
-
expect(screen.getByRole("link")).toHaveClass("taskchef-github-link");
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test("formats compact token boundaries without scientific notation", () => {
|
|
276
|
-
expect([
|
|
277
|
-
0,
|
|
278
|
-
999,
|
|
279
|
-
1_000,
|
|
280
|
-
1_100,
|
|
281
|
-
12_345,
|
|
282
|
-
999_999,
|
|
283
|
-
1_320_000,
|
|
284
|
-
1_000_000_000,
|
|
285
|
-
].map((value) => formatCompactTokens(value, "en-US"))).toEqual([
|
|
286
|
-
"0",
|
|
287
|
-
"999",
|
|
288
|
-
"1K",
|
|
289
|
-
"1.1K",
|
|
290
|
-
"12.35K",
|
|
291
|
-
"1M",
|
|
292
|
-
"1.32M",
|
|
293
|
-
"1B",
|
|
294
|
-
]);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test("formats estimates to two decimals across zero, sub-cent, missing, and large values", () => {
|
|
298
|
-
expect(formatEstimatedCost(0, "en-US")).toBe("$0.00");
|
|
299
|
-
expect(formatEstimatedCost(0.0049, "en-US")).toBe("$0.00");
|
|
300
|
-
expect(formatEstimatedCost(12.344, "en-US")).toBe("$12.34");
|
|
301
|
-
expect(formatEstimatedCost(12.345, "en-US")).toBe("$12.35");
|
|
302
|
-
expect(formatEstimatedCost(null, "en-US")).toBe("—");
|
|
303
|
-
expect(formatEstimatedCost(Number.NaN, "en-US")).toBe("—");
|
|
304
|
-
expect(formatEstimatedCost(1_234_567_890.125, "en-US")).toBe("$1,234,567,890.13");
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test("exposes full token precision and the unrounded estimate on ready metric cards", () => {
|
|
308
|
-
render(
|
|
309
|
-
<MantineProvider>
|
|
310
|
-
<UsagePanel task={fixtureTask({
|
|
311
|
-
status: "completed",
|
|
312
|
-
usage: {
|
|
313
|
-
status: "available",
|
|
314
|
-
task: { totalTokens: 1_324_567, estimatedCostUsd: 12.3449 },
|
|
315
|
-
},
|
|
316
|
-
})} />
|
|
317
|
-
</MantineProvider>,
|
|
318
|
-
);
|
|
319
|
-
expect(screen.getByLabelText("1,324,567 tokens")).toHaveTextContent("1.32M");
|
|
320
|
-
expect(screen.getByLabelText("1,324,567 tokens")).toHaveAttribute("title", "1,324,567 tokens");
|
|
321
|
-
expect(screen.getByLabelText("Estimated cost $12.34")).toHaveTextContent("$12.34");
|
|
322
|
-
expect(screen.getByLabelText("Estimated cost $12.34")).toHaveAttribute(
|
|
323
|
-
"title",
|
|
324
|
-
"Unrounded estimate: $12.3449",
|
|
325
|
-
);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
test("groups, deduplicates, and naturally sorts compact related links by repository", () => {
|
|
329
|
-
const task = fixtureTask({
|
|
330
|
-
relatedGitHubLinks: [
|
|
331
|
-
["taskchef", "83", "pull"],
|
|
332
|
-
["guzuoshou-workspace", "124", "issue"],
|
|
333
|
-
["taskchef", "79", "issue"],
|
|
334
|
-
["guzuoshou-workspace", "108", "issue"],
|
|
335
|
-
["taskchef", "80", "issue"],
|
|
336
|
-
["guzuoshou-workspace", "109", "pull"],
|
|
337
|
-
["taskchef", "79", "pull"],
|
|
338
|
-
].map(([repository, number, type]) => ({
|
|
339
|
-
label: `favoyang/${repository}#${number}`,
|
|
340
|
-
number,
|
|
341
|
-
owner: "favoyang",
|
|
342
|
-
repository,
|
|
343
|
-
type: type as "issue" | "pull",
|
|
344
|
-
url: `https://github.com/favoyang/${repository}/${type === "pull" ? "pull" : "issues"}/${number}`,
|
|
345
|
-
})),
|
|
346
|
-
});
|
|
347
|
-
const { container } = render(<MantineProvider><GitHubLinks task={task} /></MantineProvider>);
|
|
348
|
-
const groups = container.querySelectorAll(".taskchef-github-group");
|
|
349
|
-
expect(groups).toHaveLength(2);
|
|
350
|
-
expect(within(groups[0] as HTMLElement).getAllByRole("link").map((link) => link.textContent))
|
|
351
|
-
.toEqual(["guzuoshou-workspace #108", "#109", "#124"]);
|
|
352
|
-
expect(within(groups[1] as HTMLElement).getAllByRole("link").map((link) => link.textContent))
|
|
353
|
-
.toEqual(["taskchef #79", "#80", "#83"]);
|
|
354
|
-
const first = screen.getByRole("link", { name: /favoyang\/taskchef Issue #79/ });
|
|
355
|
-
expect(first).toHaveAttribute("href", "https://github.com/favoyang/taskchef/issues/79");
|
|
356
|
-
expect(first).toHaveAttribute("target", "_blank");
|
|
357
|
-
expect(first).toHaveAttribute("rel", "noopener noreferrer");
|
|
358
|
-
expect(screen.queryByText(/PR #|Issue #/)).not.toBeInTheDocument();
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
test("keeps established inline request and result link labels", () => {
|
|
362
|
-
const task = fixtureTask({
|
|
363
|
-
project: {
|
|
364
|
-
name: "TaskChef Preview",
|
|
365
|
-
path: "/tmp/taskchef-preview-project",
|
|
366
|
-
githubRepos: ["https://github.com/favoyang/taskchef"],
|
|
367
|
-
},
|
|
368
|
-
relatedGitHubRepository: "favoyang/taskchef",
|
|
369
|
-
});
|
|
370
|
-
const { container } = render(
|
|
371
|
-
<MantineProvider>
|
|
372
|
-
<div>
|
|
373
|
-
<LinkedText task={task} text="Review https://github.com/favoyang/taskchef/pull/83." />
|
|
374
|
-
</div>
|
|
375
|
-
</MantineProvider>,
|
|
376
|
-
);
|
|
377
|
-
expect(within(container).getByRole("link", { name: /favoyang\/taskchef PR #83/ }))
|
|
378
|
-
.toHaveTextContent("taskchef PR #83");
|
|
379
|
-
});
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
describe("reported wall-clock work presentation", () => {
|
|
383
|
-
test("covers formatting, derivation, unavailable boundaries, usage states, and accessibility", () => {
|
|
384
|
-
expect(formatReportedDuration(500)).toBe("<1s");
|
|
385
|
-
expect(formatReportedDuration(32_999)).toBe("32s");
|
|
386
|
-
expect(formatReportedDuration((18 * 60 + 32) * 1_000 + 999)).toBe("18m 32s");
|
|
387
|
-
expect(formatReportedDuration((2 * 60 + 14) * 60 * 1_000 + 59_999)).toBe("2h 14m");
|
|
388
|
-
expect(formatReportedDuration((27 * 60 + 59) * 60 * 1_000)).toBe("1d 3h");
|
|
389
|
-
expect(formatReportedDuration(-1)).toBeNull();
|
|
390
|
-
expect(formatReportedDuration(Number.NaN)).toBeNull();
|
|
391
|
-
|
|
392
|
-
{
|
|
393
|
-
const task = fixtureTask({
|
|
394
|
-
status: "working",
|
|
395
|
-
turns: [
|
|
396
|
-
{
|
|
397
|
-
requestSummary: "First turn",
|
|
398
|
-
startedAt: "2026-08-30T08:00:00.000Z",
|
|
399
|
-
turnRef: "turn-one",
|
|
400
|
-
turnId: null,
|
|
401
|
-
result: {
|
|
402
|
-
status: "completed",
|
|
403
|
-
summary: "First done",
|
|
404
|
-
updatedAt: "2026-08-30T08:18:32.000Z",
|
|
405
|
-
},
|
|
406
|
-
},
|
|
407
|
-
{
|
|
408
|
-
requestSummary: "Second turn after a long idle gap",
|
|
409
|
-
startedAt: "2026-08-30T18:00:00.000Z",
|
|
410
|
-
turnRef: "turn-two",
|
|
411
|
-
turnId: null,
|
|
412
|
-
result: {
|
|
413
|
-
status: "failed",
|
|
414
|
-
summary: "Second done",
|
|
415
|
-
updatedAt: "2026-08-30T20:14:00.000Z",
|
|
416
|
-
},
|
|
417
|
-
},
|
|
418
|
-
{
|
|
419
|
-
requestSummary: "Active follow-up",
|
|
420
|
-
startedAt: "2026-08-31T08:00:00.000Z",
|
|
421
|
-
turnRef: "turn-three",
|
|
422
|
-
turnId: null,
|
|
423
|
-
result: null,
|
|
424
|
-
},
|
|
425
|
-
],
|
|
426
|
-
});
|
|
427
|
-
const view = taskReportedWorkView(task);
|
|
428
|
-
expect(view.kind).toBe("available");
|
|
429
|
-
expect(view.value).toBe("2h 32m");
|
|
430
|
-
expect(view.title).toMatch(/idle gaps are excluded/i);
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
{
|
|
434
|
-
const invalid = fixtureTask({
|
|
435
|
-
status: "completed",
|
|
436
|
-
turns: [{
|
|
437
|
-
requestSummary: "Historical turn",
|
|
438
|
-
startedAt: "not-a-date",
|
|
439
|
-
turnRef: "turn-one",
|
|
440
|
-
turnId: null,
|
|
441
|
-
result: { status: "completed", summary: "Done", updatedAt: "2026-08-30T08:00:00.000Z" },
|
|
442
|
-
}],
|
|
443
|
-
});
|
|
444
|
-
expect(taskReportedWorkView(invalid)).toMatchObject({ kind: "unavailable", value: "n/a" });
|
|
445
|
-
expect(durationBetween("2026-08-30T08:01:00.000Z", "2026-08-30T08:00:00.000Z")).toBeNull();
|
|
446
|
-
expect(durationBetween("2026-02-30T08:00:00.000Z", "2026-03-02T08:01:00.000Z")).toBeNull();
|
|
447
|
-
expect(durationBetween("March 2, 2026 08:00:00 UTC", "2026-03-02T08:01:00.000Z")).toBeNull();
|
|
448
|
-
expect(turnReportedWorkView(invalid.turns![0])).toMatchObject({
|
|
449
|
-
kind: "unavailable",
|
|
450
|
-
label: "Elapsed",
|
|
451
|
-
value: "n/a",
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
{
|
|
456
|
-
const terminalResult = {
|
|
457
|
-
status: "completed" as const,
|
|
458
|
-
summary: "Done.",
|
|
459
|
-
updatedAt: "2026-08-30T08:00:00.000Z",
|
|
460
|
-
};
|
|
461
|
-
const legacyTurn = {
|
|
462
|
-
requestSummary: null,
|
|
463
|
-
startedAt: terminalResult.updatedAt,
|
|
464
|
-
turnRef: "legacy-turn",
|
|
465
|
-
turnId: null,
|
|
466
|
-
provenance: { kind: "legacy" },
|
|
467
|
-
result: terminalResult,
|
|
468
|
-
};
|
|
469
|
-
const terminalOnlyTurn = {
|
|
470
|
-
...legacyTurn,
|
|
471
|
-
turnRef: "terminal-only-turn",
|
|
472
|
-
provenance: { kind: "mcp" },
|
|
473
|
-
};
|
|
474
|
-
for (const turn of [legacyTurn, terminalOnlyTurn]) {
|
|
475
|
-
expect(turnReportedWorkView(turn)).toMatchObject({ kind: "unavailable", value: "n/a" });
|
|
476
|
-
expect(taskReportedWorkView(fixtureTask({ status: "completed", turns: [turn] })))
|
|
477
|
-
.toMatchObject({ kind: "unavailable", value: "n/a" });
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
{
|
|
482
|
-
const observed = {
|
|
483
|
-
requestSummary: "Perform the observed turn.",
|
|
484
|
-
startedAt: "2026-08-30T08:00:00.000Z",
|
|
485
|
-
turnRef: "observed-turn",
|
|
486
|
-
turnId: null,
|
|
487
|
-
provenance: { kind: "mcp" },
|
|
488
|
-
result: {
|
|
489
|
-
status: "completed" as const,
|
|
490
|
-
summary: "Done.",
|
|
491
|
-
updatedAt: "2026-08-30T08:00:00.000Z",
|
|
492
|
-
},
|
|
493
|
-
};
|
|
494
|
-
expect(turnReportedWorkView(observed)).toMatchObject({ kind: "unavailable", value: "n/a" });
|
|
495
|
-
expect(turnReportedWorkView({
|
|
496
|
-
...observed,
|
|
497
|
-
result: { ...observed.result, updatedAt: "2026-08-30T08:00:00.500Z" },
|
|
498
|
-
})).toMatchObject({ kind: "available", value: "<1s" });
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
{
|
|
502
|
-
const compactTerminal = fixtureTask({
|
|
503
|
-
status: "completed",
|
|
504
|
-
turns: undefined,
|
|
505
|
-
latestTurn: {
|
|
506
|
-
...fixtureTask().latestTurn!,
|
|
507
|
-
result: {
|
|
508
|
-
status: "completed",
|
|
509
|
-
summary: "Done.",
|
|
510
|
-
updatedAt: "2026-08-30T08:18:32.000Z",
|
|
511
|
-
},
|
|
512
|
-
},
|
|
513
|
-
});
|
|
514
|
-
expect(taskReportedWorkView(compactTerminal)).toMatchObject({
|
|
515
|
-
kind: "unavailable",
|
|
516
|
-
value: "n/a",
|
|
517
|
-
});
|
|
518
|
-
expect(taskReportedWorkView({
|
|
519
|
-
...compactTerminal,
|
|
520
|
-
reportedWork: { terminalTurns: 1, validTurns: 1, totalMilliseconds: 1_112_000 },
|
|
521
|
-
})).toMatchObject({ kind: "available", value: "18m 32s" });
|
|
522
|
-
render(<MantineProvider><UsagePanel task={compactTerminal} /></MantineProvider>);
|
|
523
|
-
expect(screen.getByText("Duration").nextSibling).toHaveTextContent("n/a");
|
|
524
|
-
cleanup();
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
{
|
|
528
|
-
const priorResult = {
|
|
529
|
-
status: "completed" as const,
|
|
530
|
-
summary: "Prior turn done.",
|
|
531
|
-
updatedAt: "2026-08-30T08:18:32.000Z",
|
|
532
|
-
turnRef: "turn-one",
|
|
533
|
-
turnId: null,
|
|
534
|
-
};
|
|
535
|
-
expect(taskReportedWorkView(fixtureTask({
|
|
536
|
-
status: "working",
|
|
537
|
-
turnRef: "turn-two",
|
|
538
|
-
turns: undefined,
|
|
539
|
-
lastResult: priorResult,
|
|
540
|
-
results: undefined,
|
|
541
|
-
}))).toMatchObject({ kind: "unavailable", value: "n/a" });
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
{
|
|
545
|
-
render(
|
|
546
|
-
<MantineProvider>
|
|
547
|
-
<UsagePanel task={fixtureTask({
|
|
548
|
-
status: "completed",
|
|
549
|
-
usage: { status: "unavailable", reason: "No matching cached boundary." },
|
|
550
|
-
})} />
|
|
551
|
-
</MantineProvider>,
|
|
552
|
-
);
|
|
553
|
-
expect(screen.getByText("No matching cached boundary.")).toBeVisible();
|
|
554
|
-
expect(screen.getByLabelText("No matching cached boundary.")).toBeVisible();
|
|
555
|
-
cleanup();
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
{
|
|
559
|
-
const task = fixtureTask();
|
|
560
|
-
expect(turnReportedWorkView(task.turns![0], Date.parse("2026-08-30T08:18:32.000Z"))).toMatchObject({
|
|
561
|
-
label: "Elapsed so far",
|
|
562
|
-
value: "18m 32s",
|
|
563
|
-
});
|
|
564
|
-
expect(turnUsageMetricsView(task, task.turns![0])).toMatchObject({
|
|
565
|
-
animated: true,
|
|
566
|
-
cost: { value: "Pending" },
|
|
567
|
-
tokens: { value: "Pending" },
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
});
|
|
571
|
-
});
|