@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.
- package/.env.example +11 -0
- package/README.md +3 -0
- package/framework/coverage.ts +3 -3
- package/framework/diary.ts +3 -3
- package/framework/llm-client.ts +4 -1
- package/framework/types.ts +11 -0
- package/package.json +5 -3
- package/server/index.ts +3 -3
- package/server/runs.ts +3 -3
- package/web/dist/assets/index-Cx9-vLgF.js +85 -0
- package/web/dist/index.html +1 -1
- package/framework/__tests__/cost.test.ts +0 -130
- package/framework/__tests__/coverage.test.ts +0 -448
- package/framework/__tests__/cross-run-dedup.test.ts +0 -83
- package/framework/__tests__/diary.test.ts +0 -155
- package/framework/__tests__/page-cache.test.ts +0 -106
- package/framework/__tests__/report.test.ts +0 -253
- package/server/__tests__/api.test.ts +0 -582
- package/server/__tests__/runs.test.ts +0 -186
- package/server/__tests__/scheduler.test.ts +0 -137
- package/web/dist/assets/index-PhasbbTM.js +0 -85
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
|
|
3
|
-
vi.mock("fs");
|
|
4
|
-
|
|
5
|
-
import * as fs from "fs";
|
|
6
|
-
import { listRuns, getReportPath } from "../runs";
|
|
7
|
-
import type { RunLog, Finding } from "../../framework/types";
|
|
8
|
-
|
|
9
|
-
function mockRunLog(overrides: Partial<RunLog> = {}): RunLog {
|
|
10
|
-
return {
|
|
11
|
-
runId: "run_1",
|
|
12
|
-
startedAt: "2026-01-01T00:00:00.000Z",
|
|
13
|
-
completedAt: "2026-01-01T00:05:00.000Z",
|
|
14
|
-
repo: "test",
|
|
15
|
-
agents: [],
|
|
16
|
-
summary: {
|
|
17
|
-
totalAgents: 0,
|
|
18
|
-
completed: 0,
|
|
19
|
-
errors: 0,
|
|
20
|
-
iterationLimitReached: 0,
|
|
21
|
-
totalActions: 0,
|
|
22
|
-
totalIssuesPosted: 0,
|
|
23
|
-
regressionChecked: 0,
|
|
24
|
-
regressionFailed: 0,
|
|
25
|
-
rateLimitRetries: 0,
|
|
26
|
-
cost: { inputTokens: 0, outputTokens: 0, estimatedUSD: null },
|
|
27
|
-
},
|
|
28
|
-
...overrides,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function mockFinding(overrides: Partial<Finding> = {}): Finding {
|
|
33
|
-
return {
|
|
34
|
-
id: "f1",
|
|
35
|
-
runId: "run_1",
|
|
36
|
-
agentId: "a1",
|
|
37
|
-
agentName: "Alice",
|
|
38
|
-
role: "tester",
|
|
39
|
-
title: "title",
|
|
40
|
-
body: "body",
|
|
41
|
-
category: "bug",
|
|
42
|
-
timestamp: "2026-01-01T00:00:00.000Z",
|
|
43
|
-
...overrides,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
beforeEach(() => {
|
|
48
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
49
|
-
vi.mocked(fs.readdirSync).mockReturnValue([] as unknown as ReturnType<typeof fs.readdirSync>);
|
|
50
|
-
vi.mocked(fs.readFileSync).mockReturnValue("{}" as unknown as ReturnType<typeof fs.readFileSync>);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("listRuns", () => {
|
|
54
|
-
it("logs ディレクトリがない → 空配列", () => {
|
|
55
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
56
|
-
expect(listRuns()).toEqual([]);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("running_*.json から実行中の run を isLive:true で返す", () => {
|
|
60
|
-
vi.mocked(fs.existsSync).mockImplementation((p: unknown) => String(p).endsWith("logs"));
|
|
61
|
-
vi.mocked(fs.readdirSync).mockReturnValue(["running_run_live.json"] as unknown as ReturnType<typeof fs.readdirSync>);
|
|
62
|
-
vi.mocked(fs.readFileSync).mockReturnValue(
|
|
63
|
-
JSON.stringify({ runId: "run_live", startedAt: "2026-06-01T00:00:00.000Z" }) as unknown as ReturnType<typeof fs.readFileSync>
|
|
64
|
-
);
|
|
65
|
-
const result = listRuns();
|
|
66
|
-
expect(result).toHaveLength(1);
|
|
67
|
-
expect(result[0]).toMatchObject({ runId: "run_live", status: "running", isLive: true });
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("report_ プレフィックスのファイルは run ログとして扱わない", () => {
|
|
71
|
-
vi.mocked(fs.existsSync).mockImplementation((p: unknown) => String(p).endsWith("logs"));
|
|
72
|
-
vi.mocked(fs.readdirSync).mockReturnValue(["report_run_1.html"] as unknown as ReturnType<typeof fs.readdirSync>);
|
|
73
|
-
expect(listRuns()).toEqual([]);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("通常の run ログを完了状態のサマリーに変換する", () => {
|
|
77
|
-
const log = mockRunLog({
|
|
78
|
-
runId: "run_done",
|
|
79
|
-
agents: [
|
|
80
|
-
{ agentType: "explorer", agentId: "a1", agentName: "A", role: "r", startedAt: "", completedAt: null, status: "completed", iterations: 1, actions: [], visitedPaths: [], issuesPosted: [], regressionChecks: [], error: null },
|
|
81
|
-
{ agentType: "explorer", agentId: "a2", agentName: "B", role: "r", startedAt: "", completedAt: null, status: "error", iterations: 1, actions: [], visitedPaths: [], issuesPosted: [], regressionChecks: [], error: "x" },
|
|
82
|
-
],
|
|
83
|
-
});
|
|
84
|
-
vi.mocked(fs.existsSync).mockImplementation((p: unknown) => String(p).endsWith("logs"));
|
|
85
|
-
vi.mocked(fs.readdirSync).mockReturnValue(["2026-01-01T00-00-00_run_done.json"] as unknown as ReturnType<typeof fs.readdirSync>);
|
|
86
|
-
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(log) as unknown as ReturnType<typeof fs.readFileSync>);
|
|
87
|
-
|
|
88
|
-
const result = listRuns();
|
|
89
|
-
expect(result).toHaveLength(1);
|
|
90
|
-
expect(result[0]).toMatchObject({
|
|
91
|
-
runId: "run_done",
|
|
92
|
-
status: "completed",
|
|
93
|
-
agentCount: 2,
|
|
94
|
-
completedAgents: 1,
|
|
95
|
-
errorAgents: 1,
|
|
96
|
-
hasReport: false,
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it("findings/run_id/ 内の finding を category 別に集計する(triage_result.json は除外)", () => {
|
|
101
|
-
const log = mockRunLog({ runId: "run_with_findings" });
|
|
102
|
-
vi.mocked(fs.existsSync).mockImplementation((p: unknown) => {
|
|
103
|
-
const s = String(p);
|
|
104
|
-
return s.endsWith("logs") || s.endsWith("findings/run_with_findings") || s.includes("findings") && s.endsWith("run_with_findings");
|
|
105
|
-
});
|
|
106
|
-
vi.mocked(fs.readdirSync).mockImplementation((p: unknown) => {
|
|
107
|
-
const s = String(p);
|
|
108
|
-
if (s.endsWith("logs")) return ["2026-01-01T00-00-00_run_with_findings.json"] as unknown as ReturnType<typeof fs.readdirSync>;
|
|
109
|
-
return ["f0.json", "f1.json", "triage_result.json"] as unknown as ReturnType<typeof fs.readdirSync>;
|
|
110
|
-
});
|
|
111
|
-
vi.mocked(fs.readFileSync).mockImplementation((p: unknown) => {
|
|
112
|
-
const s = String(p);
|
|
113
|
-
if (s.endsWith("f0.json")) return JSON.stringify(mockFinding({ id: "f0", category: "bug" })) as unknown as ReturnType<typeof fs.readFileSync>;
|
|
114
|
-
if (s.endsWith("f1.json")) return JSON.stringify(mockFinding({ id: "f1", category: "ux" })) as unknown as ReturnType<typeof fs.readFileSync>;
|
|
115
|
-
if (s.endsWith("triage_result.json")) {
|
|
116
|
-
return JSON.stringify({ runId: "run_with_findings", completedAt: "x", issued: [], skipped: [], unprocessed: [] }) as unknown as ReturnType<typeof fs.readFileSync>;
|
|
117
|
-
}
|
|
118
|
-
return JSON.stringify(log) as unknown as ReturnType<typeof fs.readFileSync>;
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
const result = listRuns();
|
|
122
|
-
expect(result).toHaveLength(1);
|
|
123
|
-
expect(result[0].findingCount).toBe(2);
|
|
124
|
-
expect(result[0].findingsByCategory).toEqual({ bug: 1, ux: 1 });
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("同じ runId が複数ファイルに登場しても重複しない", () => {
|
|
128
|
-
vi.mocked(fs.existsSync).mockImplementation((p: unknown) => String(p).endsWith("logs"));
|
|
129
|
-
vi.mocked(fs.readdirSync).mockReturnValue([
|
|
130
|
-
"running_run_dup.json",
|
|
131
|
-
"2026-01-01T00-00-00_run_dup.json",
|
|
132
|
-
] as unknown as ReturnType<typeof fs.readdirSync>);
|
|
133
|
-
vi.mocked(fs.readFileSync).mockImplementation((p: unknown) => {
|
|
134
|
-
const s = String(p);
|
|
135
|
-
if (s.includes("running_run_dup")) {
|
|
136
|
-
return JSON.stringify({ runId: "run_dup", startedAt: "2026-01-01T00:00:00.000Z" }) as unknown as ReturnType<typeof fs.readFileSync>;
|
|
137
|
-
}
|
|
138
|
-
return JSON.stringify(mockRunLog({ runId: "run_dup" })) as unknown as ReturnType<typeof fs.readFileSync>;
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
const result = listRuns();
|
|
142
|
-
expect(result).toHaveLength(1);
|
|
143
|
-
expect(result[0].isLive).toBe(true);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
it("startedAt の降順でソートされる", () => {
|
|
147
|
-
const older = mockRunLog({ runId: "run_old", startedAt: "2026-01-01T00:00:00.000Z" });
|
|
148
|
-
const newer = mockRunLog({ runId: "run_new", startedAt: "2026-06-01T00:00:00.000Z" });
|
|
149
|
-
vi.mocked(fs.existsSync).mockImplementation((p: unknown) => String(p).endsWith("logs"));
|
|
150
|
-
vi.mocked(fs.readdirSync).mockReturnValue([
|
|
151
|
-
"2026-01-01T00-00-00_run_old.json",
|
|
152
|
-
"2026-06-01T00-00-00_run_new.json",
|
|
153
|
-
] as unknown as ReturnType<typeof fs.readdirSync>);
|
|
154
|
-
vi.mocked(fs.readFileSync).mockImplementation((p: unknown) => {
|
|
155
|
-
const s = String(p);
|
|
156
|
-
if (s.includes("run_old")) return JSON.stringify(older) as unknown as ReturnType<typeof fs.readFileSync>;
|
|
157
|
-
return JSON.stringify(newer) as unknown as ReturnType<typeof fs.readFileSync>;
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
const result = listRuns();
|
|
161
|
-
expect(result.map((r) => r.runId)).toEqual(["run_new", "run_old"]);
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it("壊れた JSON の run ログはスキップする", () => {
|
|
165
|
-
vi.mocked(fs.existsSync).mockImplementation((p: unknown) => String(p).endsWith("logs"));
|
|
166
|
-
vi.mocked(fs.readdirSync).mockReturnValue(["2026-01-01T00-00-00_run_broken.json"] as unknown as ReturnType<typeof fs.readdirSync>);
|
|
167
|
-
vi.mocked(fs.readFileSync).mockReturnValue("not json" as unknown as ReturnType<typeof fs.readFileSync>);
|
|
168
|
-
expect(listRuns()).toEqual([]);
|
|
169
|
-
});
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
describe("getReportPath", () => {
|
|
173
|
-
it("不正な runId → null", () => {
|
|
174
|
-
expect(getReportPath("bad-id")).toBeNull();
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("report ファイルが存在しない → null", () => {
|
|
178
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
179
|
-
expect(getReportPath("run_123")).toBeNull();
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("report ファイルが存在する → パスを返す", () => {
|
|
183
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
184
|
-
expect(getReportPath("run_123")).toMatch(/report_run_123\.html$/);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } 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
|
-
vi.mock("../runner.js", () => ({ spawnRun: vi.fn() }));
|
|
10
|
-
|
|
11
|
-
import { loadSchedule, saveSchedule, type ScheduleConfig } from "../scheduler";
|
|
12
|
-
import { spawnRun } from "../runner.js";
|
|
13
|
-
|
|
14
|
-
const DEFAULT: ScheduleConfig = {
|
|
15
|
-
enabled: false,
|
|
16
|
-
dayOfWeek: 1,
|
|
17
|
-
hour: 9,
|
|
18
|
-
minute: 0,
|
|
19
|
-
lastRunDate: null,
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
24
|
-
vi.mocked(fs.readFileSync).mockReturnValue("{}");
|
|
25
|
-
vi.mocked(fs.writeFileSync).mockReturnValue(undefined);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
vi.clearAllMocks();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe("loadSchedule", () => {
|
|
33
|
-
it("ファイルがない場合はデフォルト設定を返す", () => {
|
|
34
|
-
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
35
|
-
expect(loadSchedule()).toEqual(DEFAULT);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("ファイルがある場合は設定を読み込む", () => {
|
|
39
|
-
const saved: ScheduleConfig = { enabled: true, dayOfWeek: 3, hour: 14, minute: 30, lastRunDate: "2026-05-12" };
|
|
40
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
41
|
-
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(saved));
|
|
42
|
-
expect(loadSchedule()).toEqual(saved);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("ファイルが壊れている場合はデフォルトを返す", () => {
|
|
46
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
47
|
-
vi.mocked(fs.readFileSync).mockReturnValue("invalid json{{{");
|
|
48
|
-
expect(loadSchedule()).toEqual(DEFAULT);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it("部分的な設定はデフォルトとマージされる", () => {
|
|
52
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
53
|
-
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ enabled: true }));
|
|
54
|
-
expect(loadSchedule()).toEqual({ ...DEFAULT, enabled: true });
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
describe("saveSchedule", () => {
|
|
59
|
-
it("設定を JSON ファイルに書き出す", () => {
|
|
60
|
-
const config: ScheduleConfig = { enabled: true, dayOfWeek: 1, hour: 9, minute: 0, lastRunDate: null };
|
|
61
|
-
saveSchedule(config);
|
|
62
|
-
const [, content] = vi.mocked(fs.writeFileSync).mock.calls[0];
|
|
63
|
-
expect(JSON.parse(content as string)).toEqual(config);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe("scheduler — 時刻判定ロジック", () => {
|
|
68
|
-
it("enabled=false のときは spawnRun を呼ばない", async () => {
|
|
69
|
-
vi.useFakeTimers();
|
|
70
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
71
|
-
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ ...DEFAULT, enabled: false }));
|
|
72
|
-
|
|
73
|
-
vi.resetModules();
|
|
74
|
-
const { startScheduler } = await import("../scheduler");
|
|
75
|
-
startScheduler();
|
|
76
|
-
|
|
77
|
-
// 最初の setTimeout(次の分の頭)+ check が走る分だけ進める
|
|
78
|
-
await vi.advanceTimersByTimeAsync(61_000);
|
|
79
|
-
expect(spawnRun).not.toHaveBeenCalled();
|
|
80
|
-
vi.useRealTimers();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("スケジュール時刻に一致したとき spawnRun を呼ぶ", async () => {
|
|
84
|
-
vi.useFakeTimers();
|
|
85
|
-
|
|
86
|
-
// 月曜 09:00 に固定。scheduler は new Date().getDay() などローカル時刻を使うため、
|
|
87
|
-
// テスト設定も同じメソッドで一致させる(環境のタイムゾーンに依存するが両者が整合する)
|
|
88
|
-
const monday9am = new Date("2026-05-11T09:00:00.000Z");
|
|
89
|
-
vi.setSystemTime(monday9am);
|
|
90
|
-
const now = new Date();
|
|
91
|
-
|
|
92
|
-
const config: ScheduleConfig = {
|
|
93
|
-
enabled: true,
|
|
94
|
-
dayOfWeek: now.getDay(),
|
|
95
|
-
hour: now.getHours(),
|
|
96
|
-
minute: now.getMinutes(),
|
|
97
|
-
lastRunDate: null,
|
|
98
|
-
};
|
|
99
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
100
|
-
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(config));
|
|
101
|
-
|
|
102
|
-
vi.resetModules();
|
|
103
|
-
const { startScheduler: start } = await import("../scheduler");
|
|
104
|
-
start();
|
|
105
|
-
|
|
106
|
-
await vi.advanceTimersByTimeAsync(61_000);
|
|
107
|
-
expect(spawnRun).toHaveBeenCalledOnce();
|
|
108
|
-
vi.useRealTimers();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("同日に既に実行済みなら spawnRun を呼ばない", async () => {
|
|
112
|
-
vi.useFakeTimers();
|
|
113
|
-
|
|
114
|
-
const monday9am = new Date("2026-05-11T09:00:00.000Z");
|
|
115
|
-
vi.setSystemTime(monday9am);
|
|
116
|
-
const now = new Date();
|
|
117
|
-
const today = now.toISOString().slice(0, 10); // scheduler と同じ UTC 日付を使う
|
|
118
|
-
|
|
119
|
-
const config: ScheduleConfig = {
|
|
120
|
-
enabled: true,
|
|
121
|
-
dayOfWeek: now.getDay(),
|
|
122
|
-
hour: now.getHours(),
|
|
123
|
-
minute: now.getMinutes(),
|
|
124
|
-
lastRunDate: today,
|
|
125
|
-
};
|
|
126
|
-
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
127
|
-
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(config));
|
|
128
|
-
|
|
129
|
-
vi.resetModules();
|
|
130
|
-
const { startScheduler: start } = await import("../scheduler");
|
|
131
|
-
start();
|
|
132
|
-
|
|
133
|
-
await vi.advanceTimersByTimeAsync(61_000);
|
|
134
|
-
expect(spawnRun).not.toHaveBeenCalled();
|
|
135
|
-
vi.useRealTimers();
|
|
136
|
-
});
|
|
137
|
-
});
|