@m8i-51/shoal 0.1.21 → 0.1.22

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.
@@ -1,155 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import * as fs from "fs";
3
-
4
- vi.mock("fs");
5
- vi.mock("path", async (importOriginal) => {
6
- const actual = await importOriginal<typeof import("path")>();
7
- return { ...actual, join: (...args: string[]) => args.join("/"), dirname: (p: string) => p.split("/").slice(0, -1).join("/") };
8
- });
9
-
10
- const mockCreateMessage = vi.fn();
11
- vi.mock("../llm-client.js", () => ({
12
- createLLMClient: vi.fn(() => ({
13
- client: { createMessage: mockCreateMessage },
14
- defaultModel: "mock-model",
15
- })),
16
- }));
17
-
18
- import { generateDiary, getDiaryPath } from "../diary";
19
-
20
- const DIARY_TEXT = "# 探索日誌 — run_123\n冒険が始まった。";
21
-
22
- beforeEach(() => {
23
- vi.mocked(fs.existsSync).mockReturnValue(false);
24
- vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
25
- vi.mocked(fs.writeFileSync).mockReturnValue(undefined);
26
- vi.mocked(fs.readdirSync).mockReturnValue([] as unknown as ReturnType<typeof fs.readdirSync>);
27
- mockCreateMessage.mockClear();
28
- mockCreateMessage.mockResolvedValue({
29
- content: [{ type: "text", text: DIARY_TEXT }],
30
- });
31
- });
32
-
33
- describe("getDiaryPath", () => {
34
- it("run_\\d+ 形式 + ファイル存在 → パスを返す", () => {
35
- vi.mocked(fs.existsSync).mockReturnValue(true);
36
- const result = getDiaryPath("run_123");
37
- expect(result).not.toBeNull();
38
- expect(result).toContain("run_123");
39
- });
40
-
41
- it("run_\\d+ 形式だがファイルなし → null", () => {
42
- vi.mocked(fs.existsSync).mockReturnValue(false);
43
- expect(getDiaryPath("run_123")).toBeNull();
44
- });
45
-
46
- it("不正な runId(英字)→ null", () => {
47
- expect(getDiaryPath("run_abc")).toBeNull();
48
- });
49
-
50
- it("パストラバーサル試みは null", () => {
51
- expect(getDiaryPath("../etc/passwd")).toBeNull();
52
- expect(getDiaryPath("run_123/../../../etc")).toBeNull();
53
- });
54
- });
55
-
56
- describe("generateDiary", () => {
57
- it("生成されたテキストを返す", async () => {
58
- const result = await generateDiary("run_123", []);
59
- expect(result).toBe(DIARY_TEXT);
60
- });
61
-
62
- it("生成テキストをファイルに書き込む", async () => {
63
- await generateDiary("run_123", []);
64
- expect(fs.writeFileSync).toHaveBeenCalled();
65
- const [, content] = vi.mocked(fs.writeFileSync).mock.calls[0];
66
- expect(content).toBe(DIARY_TEXT);
67
- });
68
-
69
- it("ファイル書き込み前にディレクトリを作成する", async () => {
70
- await generateDiary("run_123", []);
71
- expect(fs.mkdirSync).toHaveBeenCalledWith(expect.any(String), { recursive: true });
72
- });
73
-
74
- it("findings がない場合はプロンプトに「発見なし」を含める", async () => {
75
- vi.mocked(fs.existsSync).mockReturnValue(false);
76
- await generateDiary("run_123", []);
77
- const [params] = mockCreateMessage.mock.calls[0];
78
- const userContent = (params.messages[0].content as string);
79
- expect(userContent).toContain("発見なし");
80
- });
81
-
82
- it("findings がある場合はタイトルをプロンプトに含める", async () => {
83
- vi.mocked(fs.existsSync).mockReturnValue(true);
84
- vi.mocked(fs.readdirSync).mockReturnValue(["f1.json"] as unknown as ReturnType<typeof fs.readdirSync>);
85
- const finding = { id: "f1", runId: "run_123", agentId: "a", agentName: "A", role: "r", title: "Login is broken", body: "", category: "bug", timestamp: new Date().toISOString() };
86
- vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(finding) as unknown as ReturnType<typeof fs.readFileSync>);
87
-
88
- await generateDiary("run_123", []);
89
- const [params] = mockCreateMessage.mock.calls[0];
90
- expect(params.messages[0].content).toContain("Login is broken");
91
- });
92
-
93
- it("triage_result.json(timestamp を持たない集計ファイル)はプロンプトに混ぜない", async () => {
94
- vi.mocked(fs.existsSync).mockReturnValue(true);
95
- vi.mocked(fs.readdirSync).mockReturnValue(["f1.json", "triage_result.json"] as unknown as ReturnType<typeof fs.readdirSync>);
96
- const finding = { id: "f1", runId: "run_123", agentId: "a", agentName: "A", role: "r", title: "Login is broken", body: "", category: "bug", timestamp: new Date().toISOString() };
97
- const triageResult = { runId: "run_123", completedAt: "x", issued: [], skipped: [], unprocessed: [] };
98
- vi.mocked(fs.readFileSync).mockImplementation((p: unknown) => {
99
- const path = String(p);
100
- if (path.endsWith("triage_result.json")) return JSON.stringify(triageResult) as unknown as ReturnType<typeof fs.readFileSync>;
101
- return JSON.stringify(finding) as unknown as ReturnType<typeof fs.readFileSync>;
102
- });
103
-
104
- await generateDiary("run_123", []);
105
- const [params] = mockCreateMessage.mock.calls[0];
106
- expect(params.messages[0].content).not.toContain("undefined");
107
- });
108
-
109
- it("ログが空の場合はプロンプトに「イベントログなし」を含める", async () => {
110
- await generateDiary("run_123", []);
111
- const [params] = mockCreateMessage.mock.calls[0];
112
- expect(params.messages[0].content).toContain("イベントログなし");
113
- });
114
-
115
- it("navigate 行は最大 25 件まで抽出する", async () => {
116
- const lines = Array.from({ length: 30 }, (_, i) => ` → navigate({"path":"/page${i}"})`);
117
- await generateDiary("run_123", lines);
118
- const [params] = mockCreateMessage.mock.calls[0];
119
- const content: string = params.messages[0].content;
120
- const navCount = (content.match(/navigate/g) ?? []).length;
121
- expect(navCount).toBeLessThanOrEqual(25);
122
- });
123
-
124
- it("agent start/done 行はすべて抽出する", async () => {
125
- const lines = [
126
- "[explorer] Alice start",
127
- "[browser] Bob start",
128
- "[explorer] Alice done",
129
- ];
130
- await generateDiary("run_123", lines);
131
- const [params] = mockCreateMessage.mock.calls[0];
132
- const content: string = params.messages[0].content;
133
- expect(content).toContain("[explorer] Alice start");
134
- expect(content).toContain("[browser] Bob start");
135
- expect(content).toContain("[explorer] Alice done");
136
- });
137
-
138
- it("finding 発見ログは抽出される", async () => {
139
- const lines = [' → [findings] saved: "Login page crashes" (bug)'];
140
- await generateDiary("run_123", lines);
141
- const [params] = mockCreateMessage.mock.calls[0];
142
- expect(params.messages[0].content).toContain("[findings] saved");
143
- });
144
-
145
- it("navigate 行が 100 文字超えると切り詰められる", async () => {
146
- const longPath = "/very/long/path/" + "a".repeat(200);
147
- const lines = [` → navigate({"path":"${longPath}"})`];
148
- await generateDiary("run_123", lines);
149
- const [params] = mockCreateMessage.mock.calls[0];
150
- const content: string = params.messages[0].content;
151
- const lines2 = content.split("\n");
152
- const navLine = lines2.find((l) => l.includes("navigate"));
153
- expect(navLine!.length).toBeLessThanOrEqual(103); // 100 + "…"
154
- });
155
- });
@@ -1,106 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import * as fs from "fs";
3
-
4
- vi.mock("fs");
5
- vi.mock("path", async (importOriginal) => {
6
- const actual = await importOriginal<typeof import("path")>();
7
- return { ...actual, join: (...args: string[]) => args.join("/") };
8
- });
9
-
10
- import { loadPageHashes, updatePageHashes, hashContent } from "../page-cache";
11
-
12
- beforeEach(() => {
13
- vi.mocked(fs.existsSync).mockReturnValue(false);
14
- vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
15
- vi.mocked(fs.writeFileSync).mockReturnValue(undefined);
16
- });
17
-
18
- describe("hashContent", () => {
19
- it("同じ文字列は常に同じハッシュを返す", () => {
20
- expect(hashContent("hello")).toBe(hashContent("hello"));
21
- });
22
-
23
- it("異なる文字列は異なるハッシュを返す", () => {
24
- expect(hashContent("hello")).not.toBe(hashContent("world"));
25
- });
26
-
27
- it("空文字列も一意のハッシュを返す", () => {
28
- const h = hashContent("");
29
- expect(typeof h).toBe("string");
30
- expect(h.length).toBeGreaterThan(0);
31
- });
32
-
33
- it("16文字に切り詰められる", () => {
34
- expect(hashContent("any content").length).toBe(16);
35
- });
36
- });
37
-
38
- describe("loadPageHashes", () => {
39
- it("キャッシュファイルがない場合は空オブジェクトを返す", () => {
40
- vi.mocked(fs.existsSync).mockReturnValue(false);
41
- expect(loadPageHashes("localhost:3000")).toEqual({});
42
- });
43
-
44
- it("キャッシュファイルがある場合はパースして返す", () => {
45
- const data = { "/home": "abc123", "/about": "def456" };
46
- vi.mocked(fs.existsSync).mockReturnValue(true);
47
- vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(data) as unknown as ReturnType<typeof fs.readFileSync>);
48
- expect(loadPageHashes("localhost:3000")).toEqual(data);
49
- });
50
-
51
- it("壊れた JSON は空オブジェクトを返す(例外を投げない)", () => {
52
- vi.mocked(fs.existsSync).mockReturnValue(true);
53
- vi.mocked(fs.readFileSync).mockReturnValue("not valid json{{{" as unknown as ReturnType<typeof fs.readFileSync>);
54
- expect(() => loadPageHashes("localhost:3000")).not.toThrow();
55
- expect(loadPageHashes("localhost:3000")).toEqual({});
56
- });
57
-
58
- it("ホスト名の特殊文字(: や /)はファイルパスで - に変換される", () => {
59
- vi.mocked(fs.existsSync).mockReturnValue(true);
60
- vi.mocked(fs.readFileSync).mockReturnValue("{}" as unknown as ReturnType<typeof fs.readFileSync>);
61
- loadPageHashes("localhost:3000");
62
- const checkedPath = vi.mocked(fs.existsSync).mock.calls[0][0] as string;
63
- expect(checkedPath).not.toContain(":");
64
- expect(checkedPath).toContain("localhost-3000");
65
- });
66
- });
67
-
68
- describe("updatePageHashes", () => {
69
- it("空の updates を渡すと書き込みをしない", () => {
70
- updatePageHashes("localhost:3000", {});
71
- expect(fs.mkdirSync).not.toHaveBeenCalled();
72
- expect(fs.writeFileSync).not.toHaveBeenCalled();
73
- });
74
-
75
- it("既存データと新データをマージして書き込む", () => {
76
- const existing = { "/home": "old_hash", "/about": "about_hash" };
77
- vi.mocked(fs.existsSync).mockReturnValue(true);
78
- vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(existing) as unknown as ReturnType<typeof fs.readFileSync>);
79
-
80
- updatePageHashes("localhost:3000", { "/home": "new_hash", "/contact": "contact_hash" });
81
-
82
- const written = JSON.parse(vi.mocked(fs.writeFileSync).mock.calls[0][1] as string);
83
- expect(written["/home"]).toBe("new_hash"); // 上書き
84
- expect(written["/about"]).toBe("about_hash"); // 既存を保持
85
- expect(written["/contact"]).toBe("contact_hash"); // 新規追加
86
- });
87
-
88
- it("複数回呼ぶと蓄積される", () => {
89
- vi.mocked(fs.existsSync).mockReturnValue(false);
90
-
91
- updatePageHashes("localhost:3000", { "/a": "hash_a" });
92
- // 2回目: 1回目の書き込み内容を既存として読み込む
93
- vi.mocked(fs.existsSync).mockReturnValue(true);
94
- vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ "/a": "hash_a" }) as unknown as ReturnType<typeof fs.readFileSync>);
95
- updatePageHashes("localhost:3000", { "/b": "hash_b" });
96
-
97
- const lastWrite = JSON.parse(vi.mocked(fs.writeFileSync).mock.calls.at(-1)![1] as string);
98
- expect(lastWrite["/a"]).toBe("hash_a");
99
- expect(lastWrite["/b"]).toBe("hash_b");
100
- });
101
-
102
- it("書き込み前にディレクトリを作成する", () => {
103
- updatePageHashes("localhost:3000", { "/x": "hash_x" });
104
- expect(fs.mkdirSync).toHaveBeenCalledWith(expect.any(String), { recursive: true });
105
- });
106
- });
@@ -1,253 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import * as fs from "fs";
3
- import * as path from "path";
4
- import type { AgentLog, RegressionCheck } from "../types";
5
- import type { ScenarioOutcome } from "../scenario-designer";
6
-
7
- vi.mock("fs");
8
- vi.mock("path", async (importOriginal) => {
9
- const actual = await importOriginal<typeof path>();
10
- return { ...actual, join: (...args: string[]) => args.join("/") };
11
- });
12
-
13
- import { generateReport } from "../report";
14
- import type { RunLog, Finding } from "../types";
15
- import type { TriageResult } from "../triage";
16
- import type { ProductSpec } from "../product-discovery";
17
-
18
- function getSavedHtml(): string {
19
- const calls = vi.mocked(fs.writeFileSync).mock.calls;
20
- return calls[calls.length - 1][1] as string;
21
- }
22
-
23
- function makeRunLog(overrides: Partial<RunLog> = {}): RunLog {
24
- return {
25
- runId: "run_test",
26
- startedAt: "2026-04-27T00:00:00.000Z",
27
- completedAt: "2026-04-27T00:05:00.000Z",
28
- repo: "",
29
- agents: [],
30
- summary: {
31
- totalAgents: 0,
32
- completed: 0,
33
- errors: 0,
34
- iterationLimitReached: 0,
35
- totalActions: 0,
36
- totalIssuesPosted: 0,
37
- regressionChecked: 0,
38
- regressionFailed: 0,
39
- rateLimitRetries: 0,
40
- cost: { inputTokens: 0, outputTokens: 0, estimatedUSD: null },
41
- },
42
- ...overrides,
43
- };
44
- }
45
-
46
- function makeAgentLog(overrides: Partial<AgentLog> = {}): AgentLog {
47
- return {
48
- agentId: "a1",
49
- agentName: "Alice",
50
- agentType: "explorer",
51
- role: "tester",
52
- status: "completed",
53
- iterations: 3,
54
- actions: [],
55
- visitedPaths: [],
56
- issuesPosted: [],
57
- regressionChecks: [],
58
- error: null,
59
- startedAt: "2026-04-27T00:01:00.000Z",
60
- completedAt: "2026-04-27T00:03:00.000Z",
61
- ...overrides,
62
- };
63
- }
64
-
65
- function makeProductSpec(overrides: Partial<ProductSpec> = {}): ProductSpec {
66
- return {
67
- appName: "Test App",
68
- appDescription: "A test application",
69
- targetUsers: "Engineers",
70
- features: "Login, Dashboard",
71
- designContext: "",
72
- uiFeatures: "",
73
- appGoals: [],
74
- confidence: "high",
75
- sources: [],
76
- ...overrides,
77
- };
78
- }
79
-
80
- function makeFinding(overrides: Partial<Finding> = {}): Finding {
81
- return {
82
- id: "f1",
83
- runId: "run_test",
84
- agentId: "a1",
85
- agentName: "Alice",
86
- role: "tester",
87
- category: "bug",
88
- title: "Login button broken",
89
- body: "Clicking login does nothing",
90
- timestamp: "2026-04-27T00:01:00.000Z",
91
- ...overrides,
92
- };
93
- }
94
-
95
- const emptyTriage: TriageResult = { issued: [], skipped: [], unprocessed: [], issuesCreated: 0 };
96
-
97
- describe("generateReport", () => {
98
- beforeEach(() => {
99
- vi.mocked(fs.existsSync).mockReturnValue(false);
100
- vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
101
- vi.mocked(fs.writeFileSync).mockReturnValue(undefined);
102
- });
103
-
104
- it("ファイルパスを返す", () => {
105
- const result = generateReport(makeRunLog(), [], emptyTriage, makeProductSpec(), [], new Map());
106
- expect(result).toContain("report_run_test.html");
107
- });
108
-
109
- it("有効な HTML をファイルに書き出す", () => {
110
- generateReport(makeRunLog(), [], emptyTriage, makeProductSpec(), [], new Map());
111
- const html = getSavedHtml();
112
- expect(html).toContain("<!DOCTYPE html>");
113
- expect(html).toContain("</html>");
114
- });
115
-
116
- it("アプリ名がレポートに含まれる", () => {
117
- generateReport(makeRunLog(), [], emptyTriage, makeProductSpec({ appName: "MySpecialApp" }), [], new Map());
118
- expect(getSavedHtml()).toContain("MySpecialApp");
119
- });
120
-
121
- it("finding のタイトルが HTML エスケープされる", () => {
122
- const finding = makeFinding({ title: "XSS <script>alert(1)</script>" });
123
- generateReport(makeRunLog(), [finding], emptyTriage, makeProductSpec(), [], new Map());
124
- const html = getSavedHtml();
125
- expect(html).not.toContain("<script>alert(1)</script>");
126
- expect(html).toContain("&lt;script&gt;");
127
- });
128
-
129
- it("finding の body が HTML エスケープされる", () => {
130
- const finding = makeFinding({ body: 'Click <a href="javascript:void(0)" onclick="steal()">here</a>' });
131
- generateReport(makeRunLog(), [finding], emptyTriage, makeProductSpec(), [], new Map());
132
- const html = getSavedHtml();
133
- expect(html).not.toContain("<a href=");
134
- expect(html).toContain("&lt;a href=");
135
- });
136
-
137
- it("issued finding に → Issue バッジが付く", () => {
138
- const finding = makeFinding({ id: "f1" });
139
- const triage: TriageResult = { issued: ["f1"], skipped: [], unprocessed: [], issuesCreated: 1 };
140
- generateReport(makeRunLog(), [finding], triage, makeProductSpec(), [], new Map());
141
- expect(getSavedHtml()).toContain("→ Issue");
142
- });
143
-
144
- it("skipped finding に skipped バッジが付く", () => {
145
- const finding = makeFinding({ id: "f1" });
146
- const triage: TriageResult = { issued: [], skipped: ["f1"], unprocessed: [], issuesCreated: 0 };
147
- generateReport(makeRunLog(), [finding], triage, makeProductSpec(), [], new Map());
148
- expect(getSavedHtml()).toContain("skipped");
149
- });
150
-
151
- it("シナリオ付きの finding にシナリオタグが付く", () => {
152
- const finding = makeFinding({ agentId: "a1" });
153
- const scenario = { id: "s1", title: "New employee task", context: "", goal: "", constraints: "" };
154
- const agentAssignments = new Map([["a1", { scenario }]]);
155
- generateReport(makeRunLog(), [finding], emptyTriage, makeProductSpec(), [scenario], agentAssignments);
156
- const html = getSavedHtml();
157
- expect(html).toContain("New employee task");
158
- expect(html).toContain("scenario");
159
- });
160
-
161
- it("レンズ付きの finding にレンズタグが付く", () => {
162
- const finding = makeFinding({ agentId: "a1" });
163
- const agentAssignments = new Map([["a1", { lens: "Accessibility: keyboard navigation" }]]);
164
- generateReport(makeRunLog(), [finding], emptyTriage, makeProductSpec(), [], agentAssignments);
165
- const html = getSavedHtml();
166
- expect(html).toContain("Accessibility");
167
- expect(html).toContain("lens");
168
- });
169
-
170
- it("エージェントテーブルにエージェント名と status が含まれる", () => {
171
- const agent = makeAgentLog({ agentName: "Bob", status: "completed" });
172
- generateReport(makeRunLog({ agents: [agent] }), [], emptyTriage, makeProductSpec(), [], new Map());
173
- const html = getSavedHtml();
174
- expect(html).toContain("Bob");
175
- expect(html).toContain("completed");
176
- });
177
-
178
- it("regression エージェントに regression バッジが付く", () => {
179
- const agent = makeAgentLog({ agentType: "regression" });
180
- generateReport(makeRunLog({ agents: [agent] }), [], emptyTriage, makeProductSpec(), [], new Map());
181
- expect(getSavedHtml()).toContain("regression");
182
- });
183
-
184
- it("regression checks がある場合 Progress セクションが表示される", () => {
185
- const checks: RegressionCheck[] = [
186
- { issueNumber: 42, issueTitle: "Login button broken", status: "fixed", note: "", regressionUrl: null },
187
- ];
188
- const agent = makeAgentLog({ agentType: "regression", regressionChecks: checks });
189
- generateReport(makeRunLog({ agents: [agent] }), [], emptyTriage, makeProductSpec(), [], new Map());
190
- const html = getSavedHtml();
191
- expect(html).toContain("Progress");
192
- expect(html).toContain("#42");
193
- expect(html).toContain("Login button broken");
194
- expect(html).toContain("✓ fixed");
195
- });
196
-
197
- it("regression が再発した場合 regressed バッジが表示される", () => {
198
- const checks: RegressionCheck[] = [
199
- { issueNumber: 7, issueTitle: "Crash on submit", status: "regressed", note: "", regressionUrl: null },
200
- ];
201
- const agent = makeAgentLog({ agentType: "regression", regressionChecks: checks });
202
- generateReport(makeRunLog({ agents: [agent] }), [], emptyTriage, makeProductSpec(), [], new Map());
203
- expect(getSavedHtml()).toContain("⚠ regressed");
204
- });
205
-
206
- it("regression checks がない場合 Progress セクションは表示されない", () => {
207
- generateReport(makeRunLog(), [], emptyTriage, makeProductSpec(), [], new Map());
208
- expect(getSavedHtml()).not.toContain("Progress (");
209
- });
210
-
211
- it("ScenarioOutcomes が achieved の場合 achieved バッジが表示される", () => {
212
- const outcomes: ScenarioOutcome[] = [{
213
- scenarioId: "s1",
214
- scenarioTitle: "New employee task",
215
- agentId: "a1",
216
- agentName: "Alice",
217
- achieved: true,
218
- reason: "Completed successfully",
219
- }];
220
- generateReport(makeRunLog(), [], emptyTriage, makeProductSpec(), [], new Map(), outcomes);
221
- const html = getSavedHtml();
222
- expect(html).toContain("Scenario Outcomes");
223
- expect(html).toContain("achieved");
224
- expect(html).toContain("New employee task");
225
- });
226
-
227
- it("ScenarioOutcomes が failed の場合 failed バッジが表示される", () => {
228
- const outcomes: ScenarioOutcome[] = [{
229
- scenarioId: "s1",
230
- scenarioTitle: "Purchase flow",
231
- agentId: "a1",
232
- agentName: "Bob",
233
- achieved: false,
234
- reason: "Could not find the button",
235
- }];
236
- generateReport(makeRunLog(), [], emptyTriage, makeProductSpec(), [], new Map(), outcomes);
237
- expect(getSavedHtml()).toContain("failed");
238
- });
239
-
240
- it("finding が issued → unprocessed → skipped の順に並ぶ", () => {
241
- const f1 = makeFinding({ id: "f1", title: "Issued Finding" });
242
- const f2 = makeFinding({ id: "f2", title: "Skipped Finding" });
243
- const f3 = makeFinding({ id: "f3", title: "Unprocessed Finding" });
244
- const triage: TriageResult = { issued: ["f1"], skipped: ["f2"], unprocessed: ["f3"], issuesCreated: 1 };
245
- generateReport(makeRunLog(), [f2, f3, f1], triage, makeProductSpec(), [], new Map());
246
- const html = getSavedHtml();
247
- const issuedPos = html.indexOf("Issued Finding");
248
- const unprocessedPos = html.indexOf("Unprocessed Finding");
249
- const skippedPos = html.indexOf("Skipped Finding");
250
- expect(issuedPos).toBeLessThan(unprocessedPos);
251
- expect(unprocessedPos).toBeLessThan(skippedPos);
252
- });
253
- });