@hasna/testers 0.0.12 → 0.0.13
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/dashboard/dist/assets/{index-DyXKnBM8.css → index-PT-52SEY.css} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/index.js +2110 -1469
- package/dist/db/agents.d.ts +2 -0
- package/dist/db/agents.d.ts.map +1 -1
- package/dist/index.js +215 -117
- package/dist/lib/affected.d.ts +23 -0
- package/dist/lib/affected.d.ts.map +1 -0
- package/dist/lib/failure-pipeline.d.ts +20 -0
- package/dist/lib/failure-pipeline.d.ts.map +1 -0
- package/dist/lib/git-watch.d.ts +16 -0
- package/dist/lib/git-watch.d.ts.map +1 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/mcp/index.js +314 -94
- package/dist/server/index.js +138 -13
- package/package.json +1 -1
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/mcp/index.d.ts +0 -3
- package/dist/mcp/index.d.ts.map +0 -1
- /package/dashboard/dist/assets/{index-jNG_Nd_Q.js → index-FZ9gzLaz.js} +0 -0
package/dist/mcp/index.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
3
|
var __defProp = Object.defineProperty;
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
4
8
|
var __export = (target, all) => {
|
|
5
9
|
for (var name in all)
|
|
6
10
|
__defProp(target, name, {
|
|
7
11
|
get: all[name],
|
|
8
12
|
enumerable: true,
|
|
9
13
|
configurable: true,
|
|
10
|
-
set: (
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
11
15
|
});
|
|
12
16
|
};
|
|
13
17
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -5212,6 +5216,22 @@ function listAgents() {
|
|
|
5212
5216
|
const rows = db2.query("SELECT * FROM agents ORDER BY created_at DESC").all();
|
|
5213
5217
|
return rows.map(agentFromRow);
|
|
5214
5218
|
}
|
|
5219
|
+
function heartbeatAgent(id) {
|
|
5220
|
+
const db2 = getDatabase();
|
|
5221
|
+
const affected = db2.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(now(), id);
|
|
5222
|
+
if (affected.changes === 0)
|
|
5223
|
+
return null;
|
|
5224
|
+
return getAgent(id);
|
|
5225
|
+
}
|
|
5226
|
+
function setAgentFocus(id, scenarioId) {
|
|
5227
|
+
const db2 = getDatabase();
|
|
5228
|
+
const agent = getAgent(id);
|
|
5229
|
+
if (!agent)
|
|
5230
|
+
return null;
|
|
5231
|
+
const metadata = { ...agent.metadata ?? {}, focus: scenarioId };
|
|
5232
|
+
db2.query("UPDATE agents SET metadata = ?, last_seen_at = ? WHERE id = ?").run(JSON.stringify(metadata), now(), id);
|
|
5233
|
+
return getAgent(id);
|
|
5234
|
+
}
|
|
5215
5235
|
|
|
5216
5236
|
// src/lib/browser.ts
|
|
5217
5237
|
import { chromium as chromium2 } from "playwright";
|
|
@@ -6329,6 +6349,199 @@ async function pushFailedRunToLogs(run, failedResults, scenarios) {
|
|
|
6329
6349
|
} catch {}
|
|
6330
6350
|
}
|
|
6331
6351
|
|
|
6352
|
+
// src/lib/todos-connector.ts
|
|
6353
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
6354
|
+
import { existsSync as existsSync4 } from "fs";
|
|
6355
|
+
import { join as join4 } from "path";
|
|
6356
|
+
import { homedir as homedir4 } from "os";
|
|
6357
|
+
init_types();
|
|
6358
|
+
function resolveTodosDbPath() {
|
|
6359
|
+
const envPath = process.env["TODOS_DB_PATH"];
|
|
6360
|
+
if (envPath)
|
|
6361
|
+
return envPath;
|
|
6362
|
+
return join4(homedir4(), ".todos", "todos.db");
|
|
6363
|
+
}
|
|
6364
|
+
function connectToTodos() {
|
|
6365
|
+
const dbPath = resolveTodosDbPath();
|
|
6366
|
+
if (!existsSync4(dbPath)) {
|
|
6367
|
+
throw new TodosConnectionError(`Todos database not found at ${dbPath}. Install @hasna/todos or set TODOS_DB_PATH.`);
|
|
6368
|
+
}
|
|
6369
|
+
const db2 = new Database2(dbPath, { readonly: true });
|
|
6370
|
+
db2.exec("PRAGMA foreign_keys = ON");
|
|
6371
|
+
return db2;
|
|
6372
|
+
}
|
|
6373
|
+
function pullTasks(options = {}) {
|
|
6374
|
+
const db2 = connectToTodos();
|
|
6375
|
+
try {
|
|
6376
|
+
let query = "SELECT id, short_id, title, description, status, priority, tags, project_id FROM tasks WHERE 1=1";
|
|
6377
|
+
const params = [];
|
|
6378
|
+
if (options.status) {
|
|
6379
|
+
query += " AND status = ?";
|
|
6380
|
+
params.push(options.status);
|
|
6381
|
+
} else {
|
|
6382
|
+
query += " AND status IN ('pending', 'in_progress')";
|
|
6383
|
+
}
|
|
6384
|
+
if (options.priority) {
|
|
6385
|
+
query += " AND priority = ?";
|
|
6386
|
+
params.push(options.priority);
|
|
6387
|
+
}
|
|
6388
|
+
if (options.projectName) {
|
|
6389
|
+
const project = db2.query("SELECT id FROM projects WHERE name = ?").get(options.projectName);
|
|
6390
|
+
if (project) {
|
|
6391
|
+
query += " AND project_id = ?";
|
|
6392
|
+
params.push(project.id);
|
|
6393
|
+
}
|
|
6394
|
+
}
|
|
6395
|
+
query += " ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END";
|
|
6396
|
+
const tasks = db2.query(query).all(...params);
|
|
6397
|
+
if (options.tags && options.tags.length > 0) {
|
|
6398
|
+
return tasks.filter((task) => {
|
|
6399
|
+
const taskTags = JSON.parse(task.tags || "[]");
|
|
6400
|
+
return options.tags.some((tag) => taskTags.includes(tag));
|
|
6401
|
+
});
|
|
6402
|
+
}
|
|
6403
|
+
return tasks;
|
|
6404
|
+
} finally {
|
|
6405
|
+
db2.close();
|
|
6406
|
+
}
|
|
6407
|
+
}
|
|
6408
|
+
function taskToScenarioInput(task, projectId) {
|
|
6409
|
+
const tags = JSON.parse(task.tags || "[]");
|
|
6410
|
+
const priority = ["low", "medium", "high", "critical"].includes(task.priority) ? task.priority : "medium";
|
|
6411
|
+
const steps = [];
|
|
6412
|
+
if (task.description) {
|
|
6413
|
+
const lines = task.description.split(`
|
|
6414
|
+
`);
|
|
6415
|
+
for (const line of lines) {
|
|
6416
|
+
const match = line.match(/^\s*\d+[\.\)]\s*(.+)/);
|
|
6417
|
+
if (match?.[1]) {
|
|
6418
|
+
steps.push(match[1].trim());
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
6421
|
+
}
|
|
6422
|
+
return {
|
|
6423
|
+
name: task.title.replace(/^(OPE\d+-\d+|[A-Z]+-\d+):\s*/, ""),
|
|
6424
|
+
description: task.description || task.title,
|
|
6425
|
+
steps,
|
|
6426
|
+
tags,
|
|
6427
|
+
priority,
|
|
6428
|
+
projectId,
|
|
6429
|
+
metadata: { todosTaskId: task.id, todosShortId: task.short_id }
|
|
6430
|
+
};
|
|
6431
|
+
}
|
|
6432
|
+
function importFromTodos(options = {}) {
|
|
6433
|
+
const tasks = pullTasks({
|
|
6434
|
+
projectName: options.projectName,
|
|
6435
|
+
tags: options.tags ?? ["qa", "test", "testing"],
|
|
6436
|
+
priority: options.priority
|
|
6437
|
+
});
|
|
6438
|
+
const existing = listScenarios({ projectId: options.projectId });
|
|
6439
|
+
const existingTodoIds = new Set(existing.filter((s) => s.metadata?.todosTaskId).map((s) => s.metadata.todosTaskId));
|
|
6440
|
+
let imported = 0;
|
|
6441
|
+
let skipped = 0;
|
|
6442
|
+
for (const task of tasks) {
|
|
6443
|
+
if (existingTodoIds.has(task.id)) {
|
|
6444
|
+
skipped++;
|
|
6445
|
+
continue;
|
|
6446
|
+
}
|
|
6447
|
+
const input = taskToScenarioInput(task, options.projectId);
|
|
6448
|
+
createScenario(input);
|
|
6449
|
+
imported++;
|
|
6450
|
+
}
|
|
6451
|
+
return { imported, skipped };
|
|
6452
|
+
}
|
|
6453
|
+
|
|
6454
|
+
// src/lib/failure-pipeline.ts
|
|
6455
|
+
async function createFailureTasks(run, failedResults, scenarios) {
|
|
6456
|
+
if (failedResults.length === 0)
|
|
6457
|
+
return { created: 0, skipped: 0 };
|
|
6458
|
+
const projectId = process.env["TESTERS_TODOS_PROJECT_ID"];
|
|
6459
|
+
if (!projectId)
|
|
6460
|
+
return { created: 0, skipped: 0 };
|
|
6461
|
+
let db2 = null;
|
|
6462
|
+
try {
|
|
6463
|
+
db2 = connectToTodos();
|
|
6464
|
+
} catch {
|
|
6465
|
+
return { created: 0, skipped: 0 };
|
|
6466
|
+
}
|
|
6467
|
+
const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
|
|
6468
|
+
let created = 0;
|
|
6469
|
+
let skipped = 0;
|
|
6470
|
+
try {
|
|
6471
|
+
for (const result of failedResults) {
|
|
6472
|
+
const scenario = scenarioMap.get(result.scenarioId);
|
|
6473
|
+
const title = `BUG: [testers] ${scenario?.name ?? result.scenarioId} failed`;
|
|
6474
|
+
const existing = db2.query("SELECT id FROM tasks WHERE title = ? AND status NOT IN ('completed', 'cancelled') LIMIT 1").get(title);
|
|
6475
|
+
if (existing) {
|
|
6476
|
+
skipped++;
|
|
6477
|
+
continue;
|
|
6478
|
+
}
|
|
6479
|
+
const id = crypto.randomUUID();
|
|
6480
|
+
const now2 = new Date().toISOString();
|
|
6481
|
+
const description = [
|
|
6482
|
+
`Test failure detected by open-testers.`,
|
|
6483
|
+
``,
|
|
6484
|
+
`**Run:** ${run.id}`,
|
|
6485
|
+
`**URL:** ${run.url}`,
|
|
6486
|
+
`**Scenario:** ${scenario?.name ?? result.scenarioId}`,
|
|
6487
|
+
`**Status:** ${result.status}`,
|
|
6488
|
+
result.error ? `**Error:** ${result.error}` : null,
|
|
6489
|
+
result.reasoning ? `**Reasoning:** ${result.reasoning.slice(0, 500)}` : null,
|
|
6490
|
+
`**Duration:** ${result.durationMs ? `${(result.durationMs / 1000).toFixed(1)}s` : "N/A"}`,
|
|
6491
|
+
`**Tokens:** ${result.tokensUsed ?? 0}`
|
|
6492
|
+
].filter(Boolean).join(`
|
|
6493
|
+
`);
|
|
6494
|
+
try {
|
|
6495
|
+
db2.query(`
|
|
6496
|
+
INSERT INTO tasks (id, short_id, title, description, status, priority, tags, project_id, version, created_at, updated_at)
|
|
6497
|
+
VALUES (?, ?, ?, ?, 'pending', 'high', ?, ?, 1, ?, ?)
|
|
6498
|
+
`).run(id, `BUG-${id.slice(0, 6)}`, title, description, JSON.stringify(["bug", "testers", "auto-created"]), projectId, now2, now2);
|
|
6499
|
+
created++;
|
|
6500
|
+
} catch {
|
|
6501
|
+
skipped++;
|
|
6502
|
+
}
|
|
6503
|
+
}
|
|
6504
|
+
} finally {
|
|
6505
|
+
db2.close();
|
|
6506
|
+
}
|
|
6507
|
+
return { created, skipped };
|
|
6508
|
+
}
|
|
6509
|
+
async function notifyFailureToConversations(run, failedResults, scenarios) {
|
|
6510
|
+
const baseUrl = process.env["TESTERS_CONVERSATIONS_URL"];
|
|
6511
|
+
const space = process.env["TESTERS_CONVERSATIONS_SPACE"];
|
|
6512
|
+
if (!baseUrl || !space)
|
|
6513
|
+
return;
|
|
6514
|
+
const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
|
|
6515
|
+
const total = run.total;
|
|
6516
|
+
const failedCount = failedResults.length;
|
|
6517
|
+
const passedCount = run.passed;
|
|
6518
|
+
const failureLines = failedResults.slice(0, 5).map((r) => {
|
|
6519
|
+
const name = scenarioMap.get(r.scenarioId)?.name ?? r.scenarioId;
|
|
6520
|
+
const err = r.error ? ` \u2014 ${r.error.slice(0, 120)}` : "";
|
|
6521
|
+
return ` \u274C ${name}${err}`;
|
|
6522
|
+
});
|
|
6523
|
+
const extra = failedResults.length > 5 ? ` \u2026 and ${failedResults.length - 5} more` : "";
|
|
6524
|
+
const message = [
|
|
6525
|
+
`\uD83D\uDEA8 **Testers run failed** \u2014 ${failedCount}/${total} scenarios failed`,
|
|
6526
|
+
``,
|
|
6527
|
+
`**URL:** ${run.url}`,
|
|
6528
|
+
`**Run ID:** \`${run.id}\``,
|
|
6529
|
+
`**Pass rate:** ${passedCount}/${total}`,
|
|
6530
|
+
``,
|
|
6531
|
+
`**Failures:**`,
|
|
6532
|
+
...failureLines,
|
|
6533
|
+
extra
|
|
6534
|
+
].filter((l) => l !== "").join(`
|
|
6535
|
+
`);
|
|
6536
|
+
try {
|
|
6537
|
+
await fetch(`${baseUrl.replace(/\/$/, "")}/api/spaces/${encodeURIComponent(space)}/messages`, {
|
|
6538
|
+
method: "POST",
|
|
6539
|
+
headers: { "Content-Type": "application/json" },
|
|
6540
|
+
body: JSON.stringify({ content: message, from: "testers" })
|
|
6541
|
+
});
|
|
6542
|
+
} catch {}
|
|
6543
|
+
}
|
|
6544
|
+
|
|
6332
6545
|
// src/lib/runner.ts
|
|
6333
6546
|
var eventHandler = null;
|
|
6334
6547
|
function emit(event) {
|
|
@@ -6562,6 +6775,8 @@ async function runBatch(scenarios, options) {
|
|
|
6562
6775
|
if (finalRun.status === "failed") {
|
|
6563
6776
|
const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
|
|
6564
6777
|
pushFailedRunToLogs(finalRun, failedResults, scenarios).catch(() => {});
|
|
6778
|
+
createFailureTasks(finalRun, failedResults, scenarios).catch(() => {});
|
|
6779
|
+
notifyFailureToConversations(finalRun, failedResults, scenarios).catch(() => {});
|
|
6565
6780
|
}
|
|
6566
6781
|
return { run: finalRun, results };
|
|
6567
6782
|
}
|
|
@@ -6675,106 +6890,54 @@ function estimateCost(model, tokens) {
|
|
|
6675
6890
|
return tokens / 1e6 * costPer1M * 100;
|
|
6676
6891
|
}
|
|
6677
6892
|
|
|
6678
|
-
// src/lib/
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
}
|
|
6690
|
-
|
|
6691
|
-
const
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
const db2 = connectToTodos();
|
|
6701
|
-
try {
|
|
6702
|
-
let query = "SELECT id, short_id, title, description, status, priority, tags, project_id FROM tasks WHERE 1=1";
|
|
6703
|
-
const params = [];
|
|
6704
|
-
if (options.status) {
|
|
6705
|
-
query += " AND status = ?";
|
|
6706
|
-
params.push(options.status);
|
|
6707
|
-
} else {
|
|
6708
|
-
query += " AND status IN ('pending', 'in_progress')";
|
|
6709
|
-
}
|
|
6710
|
-
if (options.priority) {
|
|
6711
|
-
query += " AND priority = ?";
|
|
6712
|
-
params.push(options.priority);
|
|
6713
|
-
}
|
|
6714
|
-
if (options.projectName) {
|
|
6715
|
-
const project = db2.query("SELECT id FROM projects WHERE name = ?").get(options.projectName);
|
|
6716
|
-
if (project) {
|
|
6717
|
-
query += " AND project_id = ?";
|
|
6718
|
-
params.push(project.id);
|
|
6893
|
+
// src/lib/affected.ts
|
|
6894
|
+
function globToRegex(glob) {
|
|
6895
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\x00DS\x00").replace(/\*/g, "[^/]*").replace(/\x00DS\x00/g, ".*");
|
|
6896
|
+
return new RegExp(`^${escaped}$`, "i");
|
|
6897
|
+
}
|
|
6898
|
+
function matchFilesToScenarios(filePaths, scenarios, mappings = []) {
|
|
6899
|
+
if (filePaths.length === 0)
|
|
6900
|
+
return scenarios;
|
|
6901
|
+
const compiledMappings = mappings.map((m) => ({
|
|
6902
|
+
regex: globToRegex(m.glob),
|
|
6903
|
+
tags: m.tags
|
|
6904
|
+
}));
|
|
6905
|
+
const normPaths = filePaths.map((p) => p.replace(/\\/g, "/").toLowerCase());
|
|
6906
|
+
const matchedIds = new Set;
|
|
6907
|
+
for (const scenario of scenarios) {
|
|
6908
|
+
let matched = false;
|
|
6909
|
+
if (!matched) {
|
|
6910
|
+
for (const { regex, tags } of compiledMappings) {
|
|
6911
|
+
if (normPaths.some((fp) => regex.test(fp)) && tags.some((tag) => scenario.tags.includes(tag))) {
|
|
6912
|
+
matched = true;
|
|
6913
|
+
break;
|
|
6914
|
+
}
|
|
6719
6915
|
}
|
|
6720
6916
|
}
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
return options.tags.some((tag) => taskTags.includes(tag));
|
|
6727
|
-
});
|
|
6917
|
+
if (!matched && scenario.targetPath) {
|
|
6918
|
+
const segments = scenario.targetPath.replace(/^\//, "").split("/").filter((s) => s.length > 2);
|
|
6919
|
+
if (segments.some((seg) => normPaths.some((fp) => fp.includes(seg.toLowerCase())))) {
|
|
6920
|
+
matched = true;
|
|
6921
|
+
}
|
|
6728
6922
|
}
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
6735
|
-
const tags = JSON.parse(task.tags || "[]");
|
|
6736
|
-
const priority = ["low", "medium", "high", "critical"].includes(task.priority) ? task.priority : "medium";
|
|
6737
|
-
const steps = [];
|
|
6738
|
-
if (task.description) {
|
|
6739
|
-
const lines = task.description.split(`
|
|
6740
|
-
`);
|
|
6741
|
-
for (const line of lines) {
|
|
6742
|
-
const match = line.match(/^\s*\d+[\.\)]\s*(.+)/);
|
|
6743
|
-
if (match?.[1]) {
|
|
6744
|
-
steps.push(match[1].trim());
|
|
6923
|
+
if (!matched) {
|
|
6924
|
+
for (const tag of scenario.tags) {
|
|
6925
|
+
if (tag.length > 2 && normPaths.some((fp) => fp.includes(tag.toLowerCase()))) {
|
|
6926
|
+
matched = true;
|
|
6927
|
+
break;
|
|
6928
|
+
}
|
|
6745
6929
|
}
|
|
6746
6930
|
}
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
tags,
|
|
6753
|
-
priority,
|
|
6754
|
-
projectId,
|
|
6755
|
-
metadata: { todosTaskId: task.id, todosShortId: task.short_id }
|
|
6756
|
-
};
|
|
6757
|
-
}
|
|
6758
|
-
function importFromTodos(options = {}) {
|
|
6759
|
-
const tasks = pullTasks({
|
|
6760
|
-
projectName: options.projectName,
|
|
6761
|
-
tags: options.tags ?? ["qa", "test", "testing"],
|
|
6762
|
-
priority: options.priority
|
|
6763
|
-
});
|
|
6764
|
-
const existing = listScenarios({ projectId: options.projectId });
|
|
6765
|
-
const existingTodoIds = new Set(existing.filter((s) => s.metadata?.todosTaskId).map((s) => s.metadata.todosTaskId));
|
|
6766
|
-
let imported = 0;
|
|
6767
|
-
let skipped = 0;
|
|
6768
|
-
for (const task of tasks) {
|
|
6769
|
-
if (existingTodoIds.has(task.id)) {
|
|
6770
|
-
skipped++;
|
|
6771
|
-
continue;
|
|
6931
|
+
if (!matched) {
|
|
6932
|
+
const nameWords = scenario.name.toLowerCase().split(/[\s\-_/]+/).filter((w) => w.length > 3);
|
|
6933
|
+
if (nameWords.some((word) => normPaths.some((fp) => fp.includes(word)))) {
|
|
6934
|
+
matched = true;
|
|
6935
|
+
}
|
|
6772
6936
|
}
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
imported++;
|
|
6937
|
+
if (matched)
|
|
6938
|
+
matchedIds.add(scenario.id);
|
|
6776
6939
|
}
|
|
6777
|
-
return
|
|
6940
|
+
return scenarios.filter((s) => matchedIds.has(s.id));
|
|
6778
6941
|
}
|
|
6779
6942
|
|
|
6780
6943
|
// src/db/schedules.ts
|
|
@@ -7640,6 +7803,63 @@ server.tool("get_stale_scenarios", "List scenarios that have not been run recent
|
|
|
7640
7803
|
return errorResponse(error);
|
|
7641
7804
|
}
|
|
7642
7805
|
});
|
|
7806
|
+
server.tool("run_affected_scenarios", "Run only the scenarios relevant to a set of changed files. Matches files to scenarios via explicit glob\u2192tag rules, targetPath keywords, and name/tag inference. Returns immediately \u2014 poll with get_run.", {
|
|
7807
|
+
url: exports_external.string().describe("Target URL to test against"),
|
|
7808
|
+
filePaths: exports_external.array(exports_external.string()).describe("Changed file paths (relative or absolute)"),
|
|
7809
|
+
mappings: exports_external.array(exports_external.object({
|
|
7810
|
+
glob: exports_external.string().describe("File glob pattern (supports * and **)"),
|
|
7811
|
+
tags: exports_external.array(exports_external.string()).describe("Run scenarios tagged with these if glob matches")
|
|
7812
|
+
})).optional().describe("Explicit file glob \u2192 scenario tag mappings"),
|
|
7813
|
+
projectId: exports_external.string().optional().describe("Restrict to scenarios in this project"),
|
|
7814
|
+
model: exports_external.string().optional().describe(MODEL_DESC),
|
|
7815
|
+
headed: exports_external.boolean().optional().describe("Run browser in headed mode"),
|
|
7816
|
+
parallel: exports_external.number().optional().describe("Number of parallel workers")
|
|
7817
|
+
}, async ({ url, filePaths, mappings, projectId, model, headed, parallel }) => {
|
|
7818
|
+
try {
|
|
7819
|
+
const allScenarios = listScenarios({ projectId });
|
|
7820
|
+
const matched = matchFilesToScenarios(filePaths, allScenarios, mappings ?? []);
|
|
7821
|
+
if (matched.length === 0) {
|
|
7822
|
+
return json({ runId: null, scenarioCount: 0, matchedScenarios: [], message: "No scenarios matched the provided file paths." });
|
|
7823
|
+
}
|
|
7824
|
+
const scenarioIds = matched.map((s) => s.id);
|
|
7825
|
+
const { runId, scenarioCount } = startRunAsync({ url, scenarioIds, model, headed, parallel, projectId });
|
|
7826
|
+
return json({
|
|
7827
|
+
runId,
|
|
7828
|
+
scenarioCount,
|
|
7829
|
+
url,
|
|
7830
|
+
status: "running",
|
|
7831
|
+
matchedScenarios: matched.map((s) => ({ id: s.id, shortId: s.shortId, name: s.name, tags: s.tags })),
|
|
7832
|
+
message: "Poll with get_run to check progress."
|
|
7833
|
+
});
|
|
7834
|
+
} catch (error) {
|
|
7835
|
+
return errorResponse(error);
|
|
7836
|
+
}
|
|
7837
|
+
});
|
|
7838
|
+
server.tool("heartbeat", "Update an agent's last_seen_at timestamp. Call regularly to signal the agent is alive.", {
|
|
7839
|
+
agentId: exports_external.string().describe("Agent ID to heartbeat")
|
|
7840
|
+
}, async ({ agentId }) => {
|
|
7841
|
+
try {
|
|
7842
|
+
const agent = heartbeatAgent(agentId);
|
|
7843
|
+
if (!agent)
|
|
7844
|
+
return errorResponse(notFoundErr(agentId, "Agent"));
|
|
7845
|
+
return json({ ok: true, agentId: agent.id, lastSeenAt: agent.lastSeenAt });
|
|
7846
|
+
} catch (error) {
|
|
7847
|
+
return errorResponse(error);
|
|
7848
|
+
}
|
|
7849
|
+
});
|
|
7850
|
+
server.tool("set_focus", "Set (or clear) an agent's current focus scenario. Stored in agent metadata.", {
|
|
7851
|
+
agentId: exports_external.string().describe("Agent ID"),
|
|
7852
|
+
scenarioId: exports_external.string().nullable().describe("Scenario ID the agent is working on, or null to clear")
|
|
7853
|
+
}, async ({ agentId, scenarioId }) => {
|
|
7854
|
+
try {
|
|
7855
|
+
const agent = setAgentFocus(agentId, scenarioId);
|
|
7856
|
+
if (!agent)
|
|
7857
|
+
return errorResponse(notFoundErr(agentId, "Agent"));
|
|
7858
|
+
return json({ ok: true, agentId: agent.id, focus: agent.metadata?.focus ?? null });
|
|
7859
|
+
} catch (error) {
|
|
7860
|
+
return errorResponse(error);
|
|
7861
|
+
}
|
|
7862
|
+
});
|
|
7643
7863
|
async function main() {
|
|
7644
7864
|
const transport = new StdioServerTransport;
|
|
7645
7865
|
await server.connect(transport);
|