@m8i-51/shoal 0.1.12 → 0.1.14

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.
@@ -259,7 +259,7 @@ describe("computeWeightedSummary", () => {
259
259
 
260
260
  // updateCoverage が 31件目を追加してトリムすることを確認
261
261
  vi.mocked(fs.existsSync).mockReturnValue(true);
262
- vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ entries }) as unknown as Buffer);
262
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ entries }));
263
263
 
264
264
  // computeWeightedSummary は entries をそのまま使うだけなので、
265
265
  // 35件あってもエラーにならないことを確認
@@ -47,12 +47,14 @@ function makeAgentLog(overrides: Partial<AgentLog> = {}): AgentLog {
47
47
  return {
48
48
  agentId: "a1",
49
49
  agentName: "Alice",
50
- agentType: "browser",
50
+ agentType: "explorer",
51
51
  role: "tester",
52
52
  status: "completed",
53
53
  iterations: 3,
54
+ actions: [],
54
55
  issuesPosted: [],
55
56
  regressionChecks: [],
57
+ error: null,
56
58
  startedAt: "2026-04-27T00:01:00.000Z",
57
59
  completedAt: "2026-04-27T00:03:00.000Z",
58
60
  ...overrides,
@@ -172,7 +174,7 @@ describe("generateReport", () => {
172
174
 
173
175
  it("regression checks がある場合 Progress セクションが表示される", () => {
174
176
  const checks: RegressionCheck[] = [
175
- { issueNumber: 42, issueTitle: "Login button broken", status: "fixed" },
177
+ { issueNumber: 42, issueTitle: "Login button broken", status: "fixed", note: "", regressionUrl: null },
176
178
  ];
177
179
  const agent = makeAgentLog({ agentType: "regression", regressionChecks: checks });
178
180
  generateReport(makeRunLog({ agents: [agent] }), [], emptyTriage, makeProductSpec(), [], new Map());
@@ -185,7 +187,7 @@ describe("generateReport", () => {
185
187
 
186
188
  it("regression が再発した場合 regressed バッジが表示される", () => {
187
189
  const checks: RegressionCheck[] = [
188
- { issueNumber: 7, issueTitle: "Crash on submit", status: "regressed" },
190
+ { issueNumber: 7, issueTitle: "Crash on submit", status: "regressed", note: "", regressionUrl: null },
189
191
  ];
190
192
  const agent = makeAgentLog({ agentType: "regression", regressionChecks: checks });
191
193
  generateReport(makeRunLog({ agents: [agent] }), [], emptyTriage, makeProductSpec(), [], new Map());
@@ -287,7 +287,8 @@ If user management is not accessible from this account, or the app has no role s
287
287
  }
288
288
 
289
289
  case "navigate": {
290
- const { path: navPath } = toolUse.input as { path: string };
290
+ const { path: navPath } = toolUse.input as { path?: string };
291
+ if (!navPath) { resultText = "navigate: missing path"; break; }
291
292
  await saveSnapshotBeforeAction(page, observation);
292
293
  await page.goto(`${baseUrl}${navPath}`, { waitUntil: "networkidle" });
293
294
  await page.waitForTimeout(500);
@@ -297,7 +298,8 @@ If user management is not accessible from this account, or the app has no role s
297
298
  }
298
299
 
299
300
  case "click": {
300
- const { description } = toolUse.input as { description: string };
301
+ const { description } = toolUse.input as { description?: string };
302
+ if (!description) { resultText = "click: missing description"; break; }
301
303
  await saveSnapshotBeforeAction(page, observation);
302
304
  const escaped = description.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
303
305
  let clicked = false;
@@ -316,7 +318,8 @@ If user management is not accessible from this account, or the app has no role s
316
318
  }
317
319
 
318
320
  case "fill": {
319
- const { label, value } = toolUse.input as { label: string; value: string };
321
+ const { label, value } = toolUse.input as { label?: string; value?: string };
322
+ if (!label || value === undefined) { resultText = "fill: missing label or value"; break; }
320
323
  await saveSnapshotBeforeAction(page, observation);
321
324
  const byLabel = page.getByLabel(new RegExp(label, "i"));
322
325
  const byPlaceholder = page.getByPlaceholder(new RegExp(label, "i"));
@@ -340,7 +343,8 @@ If user management is not accessible from this account, or the app has no role s
340
343
  }
341
344
 
342
345
  case "save_account": {
343
- const { email, password, role } = toolUse.input as { email: string; password: string; role: string };
346
+ const { email, password, role } = toolUse.input as { email?: string; password?: string; role?: string };
347
+ if (!email || !password || !role) { resultText = "save_account: missing required fields"; break; }
344
348
  savedAccounts.push({ email, password, role });
345
349
  console.log(` [account-manager] saved account: ${email} (role: ${role})`);
346
350
  resultText = `Account saved: ${email} (${role})`;
@@ -348,7 +352,8 @@ If user management is not accessible from this account, or the app has no role s
348
352
  }
349
353
 
350
354
  case "post_finding": {
351
- const { title, body } = toolUse.input as { title: string; body: string };
355
+ const { title, body } = toolUse.input as { title?: string; body?: string };
356
+ if (!title || !body) { resultText = "post_finding: missing title or body"; break; }
352
357
  saveFinding({
353
358
  id: `acct_${Date.now()}`,
354
359
  runId,
@@ -157,11 +157,11 @@ function fromOpenAIResponse(response: OpenAI.ChatCompletion): Message {
157
157
  const content: ContentBlock[] = [];
158
158
 
159
159
  if (choice.message.content) {
160
- content.push({ type: "text", text: choice.message.content });
160
+ content.push({ type: "text", text: choice.message.content } as ContentBlock);
161
161
  }
162
162
 
163
163
  if (choice.message.tool_calls) {
164
- for (const tc of choice.message.tool_calls) {
164
+ for (const tc of choice.message.tool_calls.filter((t) => t.type === "function")) {
165
165
  let input: Record<string, unknown> = {};
166
166
  try {
167
167
  input = JSON.parse(tc.function.arguments);
@@ -172,7 +172,7 @@ function fromOpenAIResponse(response: OpenAI.ChatCompletion): Message {
172
172
  id: tc.id,
173
173
  name: tc.function.name,
174
174
  input,
175
- });
175
+ } as ContentBlock);
176
176
  }
177
177
  }
178
178
 
@@ -392,7 +392,7 @@ function fromCodexResponse(response: Record<string, unknown>): Message {
392
392
  if (i.type === "message") {
393
393
  for (const block of (i.content as unknown[]) ?? []) {
394
394
  const b = block as Record<string, unknown>;
395
- if (b.type === "output_text") content.push({ type: "text", text: b.text as string });
395
+ if (b.type === "output_text") content.push({ type: "text", text: b.text as string } as ContentBlock);
396
396
  }
397
397
  } else if (i.type === "function_call") {
398
398
  hasToolUse = true;
@@ -403,7 +403,7 @@ function fromCodexResponse(response: Record<string, unknown>): Message {
403
403
  id: (i.call_id ?? i.id) as string,
404
404
  name: i.name as string,
405
405
  input,
406
- });
406
+ } as ContentBlock);
407
407
  }
408
408
  }
409
409
 
@@ -253,7 +253,8 @@ Guidelines for output_spec:
253
253
  let result: string;
254
254
 
255
255
  if (toolUse.name === "navigate_and_read") {
256
- const { path } = toolUse.input as { path: string };
256
+ const { path } = toolUse.input as { path?: string };
257
+ if (!path) { result = "navigate_and_read: missing path"; toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result }); continue; }
257
258
  try {
258
259
  await page.goto(`${baseUrl}${path}`, { waitUntil: "networkidle", timeout: 10000 });
259
260
  await page.waitForTimeout(500);
@@ -268,7 +269,8 @@ Guidelines for output_spec:
268
269
  }
269
270
 
270
271
  } else if (toolUse.name === "fetch_url") {
271
- const { url } = toolUse.input as { url: string };
272
+ const { url } = toolUse.input as { url?: string };
273
+ if (!url) { result = "fetch_url: missing url"; toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: result }); continue; }
272
274
  try {
273
275
  const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
274
276
  const text = await res.text();
@@ -143,11 +143,16 @@ Organize feedback collected by multiple agents and post it as issue tickets.
143
143
 
144
144
  } else if (toolUse.name === "create_issue") {
145
145
  const { title, body, category, merged_finding_ids } = toolUse.input as {
146
- title: string;
147
- body: string;
148
- category: string;
149
- merged_finding_ids: string[] | undefined;
146
+ title?: string;
147
+ body?: string;
148
+ category?: string;
149
+ merged_finding_ids?: string[];
150
150
  };
151
+ if (!title || !body || !category) {
152
+ result = { error: "create_issue: missing required fields" };
153
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: JSON.stringify(result) });
154
+ continue;
155
+ }
151
156
  const mergedIds = merged_finding_ids ?? [];
152
157
  if (mergedIds.length === 0) {
153
158
  result = { error: "merged_finding_ids must contain at least one ID" };
@@ -175,7 +180,12 @@ Organize feedback collected by multiple agents and post it as issue tickets.
175
180
  }
176
181
 
177
182
  } else if (toolUse.name === "skip_finding") {
178
- const { finding_id, reason } = toolUse.input as { finding_id: string; reason: string };
183
+ const { finding_id, reason } = toolUse.input as { finding_id?: string; reason?: string };
184
+ if (!finding_id) {
185
+ result = { error: "skip_finding: missing finding_id" };
186
+ toolResults.push({ type: "tool_result", tool_use_id: toolUse.id, content: JSON.stringify(result) });
187
+ continue;
188
+ }
179
189
  pendingIds.delete(finding_id);
180
190
  skippedIds.push(finding_id);
181
191
  skipped++;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m8i-51/shoal",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "type": "module",
5
5
  "description": "Multi-agent web exploration framework — finds bugs, UX issues, and missing features by running AI agents against your app",
6
6
  "repository": {