Skip to content

Commit fb301d2

Browse files
saagar210claude
andcommitted
test(features): cover extracted queue hooks and helpers
Add focused tests for the four units lifted in this PR: - queueHelpers (formatTicketLabel / truncate / bandLabel) - useBatchTriageManager (seed from visible queue + run + empty-input error) - useDispatchManager (missing-item preview error + preview invoke payload + initial state) - useQueueOperationHandlers (claim / resolve-reopen transitions + keyboard J/C shortcut + INPUT-target short circuit) +15 tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c6c51b3 commit fb301d2

4 files changed

Lines changed: 459 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describe, expect, it } from "vitest";
2+
import { bandLabel, formatTicketLabel, truncate } from "./queueHelpers";
3+
import type { QueueItem } from "../../inbox/queueModel";
4+
import type { SavedDraft } from "../../../types/workspace";
5+
6+
function makeItem(
7+
state: QueueItem["meta"]["state"],
8+
owner: string,
9+
isAtRisk = false,
10+
): QueueItem {
11+
return {
12+
draft: { id: "d", ticket_id: null } as unknown as SavedDraft,
13+
meta: {
14+
owner,
15+
state,
16+
priority: "normal",
17+
updatedAt: "2026-04-01T00:00:00Z",
18+
},
19+
slaDueAt: "2026-04-02T00:00:00Z",
20+
isAtRisk,
21+
} as QueueItem;
22+
}
23+
24+
describe("formatTicketLabel", () => {
25+
it("prefers the ticket id when present", () => {
26+
expect(
27+
formatTicketLabel({
28+
id: "abc12345678",
29+
ticket_id: "INC-42",
30+
} as SavedDraft),
31+
).toBe("INC-42");
32+
});
33+
34+
it("falls back to a truncated draft id when there is no ticket id", () => {
35+
expect(
36+
formatTicketLabel({
37+
id: "abcdef0123456789",
38+
ticket_id: null,
39+
} as SavedDraft),
40+
).toBe("Draft abcdef01");
41+
});
42+
});
43+
44+
describe("truncate", () => {
45+
it("returns the original string when under the limit", () => {
46+
expect(truncate("short", 10)).toBe("short");
47+
});
48+
49+
it("adds an ellipsis when over the limit", () => {
50+
expect(truncate("abcdefghij", 5)).toBe("abcde...");
51+
});
52+
});
53+
54+
describe("bandLabel", () => {
55+
it("resolves the band from meta.state + owner + isAtRisk in priority order", () => {
56+
expect(bandLabel(makeItem("resolved", "alice"))).toBe("Resolved");
57+
expect(bandLabel(makeItem("open", "alice", true))).toBe("At Risk");
58+
expect(bandLabel(makeItem("open", "unassigned"))).toBe("Unassigned");
59+
expect(bandLabel(makeItem("in_progress", "alice"))).toBe("In Progress");
60+
expect(bandLabel(makeItem("open", "alice"))).toBe("Open");
61+
});
62+
});
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
// @vitest-environment jsdom
2+
import { act, renderHook, waitFor } from "@testing-library/react";
3+
import { afterEach, describe, expect, it, vi } from "vitest";
4+
import { useBatchTriageManager } from "./useBatchTriageManager";
5+
import type { QueueItem } from "../../inbox/queueModel";
6+
import type { SavedDraft } from "../../../types/workspace";
7+
8+
function makeItem(id: string, summary: string): QueueItem {
9+
return {
10+
draft: {
11+
id,
12+
ticket_id: `INC-${id}`,
13+
summary_text: summary,
14+
input_text: summary,
15+
} as unknown as SavedDraft,
16+
meta: {
17+
owner: "alice",
18+
state: "open",
19+
priority: "normal",
20+
updatedAt: "2026-04-01T00:00:00Z",
21+
},
22+
slaDueAt: "2026-04-02T00:00:00Z",
23+
isAtRisk: false,
24+
} as QueueItem;
25+
}
26+
27+
afterEach(() => {
28+
vi.clearAllMocks();
29+
});
30+
31+
describe("useBatchTriageManager", () => {
32+
it("seeds the input from the first 25 filtered items", () => {
33+
const { result } = renderHook(() =>
34+
useBatchTriageManager({
35+
filteredItems: [
36+
makeItem("1", "VPN down"),
37+
makeItem("2", "Password reset"),
38+
],
39+
operatorName: "alice",
40+
clusterTicketsForTriage: vi.fn(),
41+
listRecentTriageClusters: vi.fn(async () => []),
42+
setTriageHistory: vi.fn(),
43+
logEvent: vi.fn(),
44+
showSuccess: vi.fn(),
45+
showError: vi.fn(),
46+
}),
47+
);
48+
49+
act(() => result.current.handleSeedBatchTriage());
50+
51+
expect(result.current.batchTriageInput).toBe(
52+
"INC-1|VPN down\nINC-2|Password reset",
53+
);
54+
});
55+
56+
it("clusters tickets, refreshes history, and logs success", async () => {
57+
const cluster = vi.fn(async () => [
58+
{
59+
cluster_key: "outage",
60+
summary: "VPN outage",
61+
ticket_count: 1,
62+
ticket_ids: ["INC-1"],
63+
},
64+
]);
65+
const listRecent = vi.fn(async () => [
66+
{
67+
id: "c1",
68+
cluster_key: "outage",
69+
summary: "VPN outage",
70+
ticket_count: 2,
71+
} as unknown as never,
72+
]);
73+
const setTriageHistory = vi.fn();
74+
const showSuccess = vi.fn();
75+
const showError = vi.fn();
76+
77+
const { result } = renderHook(() =>
78+
useBatchTriageManager({
79+
filteredItems: [],
80+
operatorName: "alice",
81+
clusterTicketsForTriage: cluster as never,
82+
listRecentTriageClusters: listRecent as never,
83+
setTriageHistory,
84+
logEvent: vi.fn(),
85+
showSuccess,
86+
showError,
87+
}),
88+
);
89+
90+
act(() => {
91+
result.current.setBatchTriageInput("INC-1|VPN outage");
92+
});
93+
94+
await act(async () => {
95+
await result.current.handleRunBatchTriage();
96+
});
97+
98+
await waitFor(() => {
99+
expect(setTriageHistory).toHaveBeenCalled();
100+
});
101+
expect(cluster).toHaveBeenCalledWith([
102+
expect.objectContaining({ id: "INC-1", summary: "VPN outage" }),
103+
]);
104+
expect(showSuccess).toHaveBeenCalledWith("Batch triage completed");
105+
expect(showError).not.toHaveBeenCalled();
106+
});
107+
108+
it("surfaces an error when the input is empty", async () => {
109+
const showError = vi.fn();
110+
const cluster = vi.fn();
111+
112+
const { result } = renderHook(() =>
113+
useBatchTriageManager({
114+
filteredItems: [],
115+
operatorName: "alice",
116+
clusterTicketsForTriage: cluster as never,
117+
listRecentTriageClusters: vi.fn(async () => []),
118+
setTriageHistory: vi.fn(),
119+
logEvent: vi.fn(),
120+
showSuccess: vi.fn(),
121+
showError,
122+
}),
123+
);
124+
125+
await act(async () => {
126+
await result.current.handleRunBatchTriage();
127+
});
128+
129+
expect(showError).toHaveBeenCalledWith(
130+
"Add at least one ticket before running batch triage",
131+
);
132+
expect(cluster).not.toHaveBeenCalled();
133+
});
134+
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// @vitest-environment jsdom
2+
import { act, renderHook, waitFor } from "@testing-library/react";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { useDispatchManager } from "./useDispatchManager";
5+
import type { QueueItem } from "../../inbox/queueModel";
6+
import type { SavedDraft } from "../../../types/workspace";
7+
8+
// Gate the initial dispatch-history fetch off so the hook's useEffect short-
9+
// circuits synchronously. Handler-level tests exercise the real dispatch
10+
// functions through hook args.
11+
vi.mock("../../revamp", () => ({
12+
resolveRevampFlags: () => ({
13+
ASSISTSUPPORT_REVAMP_INBOX: true,
14+
ASSISTSUPPORT_BATCH_TRIAGE: true,
15+
ASSISTSUPPORT_COLLABORATION_DISPATCH: false,
16+
}),
17+
}));
18+
19+
function makeItem(): QueueItem {
20+
return {
21+
draft: {
22+
id: "d1",
23+
ticket_id: "INC-1",
24+
input_text: "VPN outage",
25+
summary_text: "VPN",
26+
} as unknown as SavedDraft,
27+
meta: {
28+
owner: "alice",
29+
state: "open",
30+
priority: "normal",
31+
updatedAt: "2026-04-01T00:00:00Z",
32+
},
33+
slaDueAt: "2026-04-02T00:00:00Z",
34+
isAtRisk: false,
35+
} as QueueItem;
36+
}
37+
38+
// Stable listDispatchHistory reference shared across tests so the hook's
39+
// useEffect dependency never changes mid-test and can't drive a re-render loop.
40+
const stableListDispatchHistory = vi.fn(async () => []);
41+
42+
describe("useDispatchManager", () => {
43+
it("errors when previewing without a selected item", () => {
44+
const showError = vi.fn();
45+
const { result } = renderHook(() =>
46+
useDispatchManager({
47+
operatorName: "alice",
48+
previewCollaborationDispatch: vi.fn(),
49+
confirmCollaborationDispatch: vi.fn(),
50+
cancelCollaborationDispatch: vi.fn(),
51+
listDispatchHistory: stableListDispatchHistory,
52+
logEvent: vi.fn(),
53+
showSuccess: vi.fn(),
54+
showError,
55+
}),
56+
);
57+
58+
act(() => {
59+
result.current.handlePreviewDispatch(null);
60+
});
61+
62+
expect(showError).toHaveBeenCalledWith(
63+
"Select a work item before previewing a dispatch",
64+
);
65+
});
66+
67+
it("invokes previewCollaborationDispatch with the current item and dispatch target", async () => {
68+
const previewCall = vi.fn(async () => ({
69+
id: "dispatch-1",
70+
integration_type: "jira",
71+
destination_label: "Jira",
72+
title: "Preview title",
73+
status: "preview",
74+
created_at: "2026-04-01T00:00:00Z",
75+
}));
76+
77+
const { result } = renderHook(() =>
78+
useDispatchManager({
79+
operatorName: "alice",
80+
previewCollaborationDispatch: previewCall as never,
81+
confirmCollaborationDispatch: vi.fn(),
82+
cancelCollaborationDispatch: vi.fn(),
83+
listDispatchHistory: stableListDispatchHistory,
84+
logEvent: vi.fn(),
85+
showSuccess: vi.fn(),
86+
showError: vi.fn(),
87+
}),
88+
);
89+
90+
act(() => {
91+
result.current.handlePreviewDispatch(makeItem());
92+
});
93+
94+
await waitFor(() => {
95+
expect(previewCall).toHaveBeenCalledWith(
96+
expect.objectContaining({
97+
integrationType: "jira",
98+
draftId: "d1",
99+
}),
100+
);
101+
});
102+
});
103+
104+
it("initializes with no dispatch preview or pending id", () => {
105+
const { result } = renderHook(() =>
106+
useDispatchManager({
107+
operatorName: "alice",
108+
previewCollaborationDispatch: vi.fn(),
109+
confirmCollaborationDispatch: vi.fn(),
110+
cancelCollaborationDispatch: vi.fn(),
111+
listDispatchHistory: stableListDispatchHistory,
112+
logEvent: vi.fn(),
113+
showSuccess: vi.fn(),
114+
showError: vi.fn(),
115+
}),
116+
);
117+
118+
expect(result.current.dispatchTarget).toBe("jira");
119+
expect(result.current.dispatchPreview).toBeNull();
120+
expect(result.current.pendingDispatchId).toBeNull();
121+
});
122+
});

0 commit comments

Comments
 (0)