@flowriki/agent 0.1.0-beta.41 → 0.1.0-beta.43
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/dist/{chunk-ALLCHKXG.js → chunk-ZMHE47A5.js} +461 -443
- package/dist/cli.js +118 -518
- package/dist/engine.js +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,16 +14,16 @@ import {
|
|
|
14
14
|
helperRunner,
|
|
15
15
|
mirrorCheckTargets,
|
|
16
16
|
openRunSession,
|
|
17
|
+
parseGherkin,
|
|
17
18
|
prepareBrowsers,
|
|
18
19
|
replayCase,
|
|
19
20
|
replayFlow,
|
|
20
21
|
resolveCheckouts,
|
|
21
22
|
resolveService,
|
|
22
23
|
runtimeValue,
|
|
23
|
-
similarity,
|
|
24
24
|
start,
|
|
25
25
|
status
|
|
26
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-ZMHE47A5.js";
|
|
27
27
|
|
|
28
28
|
// src/cli.ts
|
|
29
29
|
import { parseArgs } from "util";
|
|
@@ -219,6 +219,20 @@ var ServiceClient = class {
|
|
|
219
219
|
return this.call(`/api/agent/library?${query}`);
|
|
220
220
|
}
|
|
221
221
|
/** Test cases written, edited or linked in one commit; duplicates skipped. */
|
|
222
|
+
readCases(projectId, refs) {
|
|
223
|
+
const ids = refs.filter((r) => /^tc_/.test(r.trim()));
|
|
224
|
+
const names = refs.filter((r) => !/^tc_/.test(r.trim()));
|
|
225
|
+
const q = new URLSearchParams({ projectId, ...ids.length ? { ids: ids.join(",") } : {}, ...names.length ? { names: names.join("|") } : {} });
|
|
226
|
+
return this.call(`/api/agent/cases/read?${q}`);
|
|
227
|
+
}
|
|
228
|
+
/** Test cases written down — business steps, no flows yet — on Claude's
|
|
229
|
+
* working branch. */
|
|
230
|
+
writeCases(projectId, groups, changeId) {
|
|
231
|
+
return this.call(
|
|
232
|
+
`/api/agent/cases/batch?projectId=${encodeURIComponent(projectId)}`,
|
|
233
|
+
{ method: "POST", body: JSON.stringify({ groups, ...changeId ? { changeId } : {} }) }
|
|
234
|
+
);
|
|
235
|
+
}
|
|
222
236
|
saveLibrary(projectId, input) {
|
|
223
237
|
return this.call(`/api/agent/library?projectId=${encodeURIComponent(projectId)}`, { method: "POST", body: JSON.stringify(input) });
|
|
224
238
|
}
|
|
@@ -754,7 +768,7 @@ ${lines.join("\n")}`;
|
|
|
754
768
|
const note = others.length ? `
|
|
755
769
|
Claude has ${others.length} other open branch${others.length === 1 ? "" : "es"} on this project \u2014 gather_branches puts them into this one, for one review.` : "";
|
|
756
770
|
const after = draft.after?.length ? ` It starts after ${draft.after.map((a) => `"${a.name}"`).join(", ")}.` : "";
|
|
757
|
-
return `${what} ${where_}.${after}${vault} Its ${checkCount} check${checkCount === 1 ? " is" : "s are"} saved switched off \u2014
|
|
771
|
+
return `${what} ${where_}.${after}${vault} Its ${checkCount} check${checkCount === 1 ? " is" : "s are"} saved switched off \u2014 a test case switches on the ones it is about. Nothing is published yet.
|
|
758
772
|
Review and publish it here: ${review}${note}`;
|
|
759
773
|
}
|
|
760
774
|
/** A test case built from flows, without saving: its rows and every check,
|
|
@@ -837,9 +851,14 @@ THE MODEL \u2014 get this right before recording anything:
|
|
|
837
851
|
something different.
|
|
838
852
|
- A flow records checks (add_check) that prove each thing happened, but they
|
|
839
853
|
are SAVED SWITCHED OFF: which ones matter depends on the test.
|
|
840
|
-
-
|
|
854
|
+
- A TEST CASE is the test. It strings flows together as rows (in the same
|
|
841
855
|
browser, or a new one for a second person) and switches on the checks it is
|
|
842
856
|
about. Build it with preview_case, then save_case \u2014 which runs it first.
|
|
857
|
+
- A test case can be WRITTEN BEFORE IT IS AUTOMATED: business steps (Given /
|
|
858
|
+
When / Then) and no flows \u2014 imported from a spreadsheet, or written with
|
|
859
|
+
write_test_cases. To automate one, record the flows its steps need, then
|
|
860
|
+
save_case with the SAME name in the SAME folder: that fills it, and its
|
|
861
|
+
business steps stay. Its Then lines are the checks to switch on.
|
|
843
862
|
- SIGN-IN is shared setup: a flow or case that needs somebody signed in uses a
|
|
844
863
|
sign-in profile from the catalog (start_flow's signIn), never typed login steps.
|
|
845
864
|
None yet? Make it: record the login as a flow that ends with a check proving
|
|
@@ -862,19 +881,9 @@ THE MODEL \u2014 get this right before recording anything:
|
|
|
862
881
|
"Ordering \u2013 Pizza builder", "Checkout \u2013 Payment". Flows (features) and the
|
|
863
882
|
cases built from them use the same names. Keep one area per folder.
|
|
864
883
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
that check it. Read it with library_cases before deciding what to automate \u2014
|
|
869
|
-
the important cases nothing checks yet come first. Write cases with
|
|
870
|
-
save_test_cases (a case the library already has is skipped, so writing the
|
|
871
|
-
same thing twice is harmless), and after save_case, link the automation to the
|
|
872
|
-
test cases it proves with link_test_case.
|
|
873
|
-
|
|
874
|
-
KNOWLEDGE: project_brief is the project's memory \u2014 read it first. Search
|
|
875
|
-
everything (knowledge pages, documents, requirements, test cases) with
|
|
876
|
-
knowledge_search; read a page with knowledge_read; read documents with
|
|
877
|
-
sources. product.md is "About the product": the person's own description of
|
|
884
|
+
KNOWLEDGE: project_brief is the project's memory \u2014 read it first. Search the
|
|
885
|
+
knowledge pages with knowledge_search; read one with knowledge_read.
|
|
886
|
+
product.md is "About the product": the person's own description of
|
|
878
887
|
the product, in their words, as long as they like \u2014 read it first. They
|
|
879
888
|
write it, often with you in a conversation beside it; propose text there and
|
|
880
889
|
let them decide, rather than writing product.md unless they ask you to.
|
|
@@ -885,32 +894,11 @@ as learned. Keep what you learn with write_knowledge (features/<name>.md,
|
|
|
885
894
|
memory/decisions.md, a session summary), on Claude's branch for review \u2014
|
|
886
895
|
never a password, key or token.
|
|
887
896
|
|
|
888
|
-
FROM DOCUMENTS TO TESTS: documents (sources) are PRDs, CRs, feature docs.
|
|
889
|
-
requirements lists what was found in them and what the latest version changed
|
|
890
|
-
(new / changed / removed). For a new requirement: write test cases that cover
|
|
891
|
-
it (save_test_cases with covers: [requirement id]) \u2014 but first check
|
|
892
|
-
find_similar_cases, and link an existing case instead of writing a duplicate.
|
|
893
|
-
For a changed one: update the cases that cover it. For a removed one: mark
|
|
894
|
-
cases that only covered it deprecated. Never invent requirements.
|
|
895
|
-
|
|
896
|
-
AUTOMATING A TEST CASE: first write the plan with save_automation_plan \u2014 flows
|
|
897
|
-
to reuse, flows to record, which checks prove each expected result, accounts
|
|
898
|
-
and helpers it needs, and anything that cannot be automated and why. Show it
|
|
899
|
-
and wait for the person to approve before recording. Then record, save_case,
|
|
900
|
-
and link_test_case.
|
|
901
|
-
|
|
902
|
-
MANUAL TESTING: a test cycle is a set of library cases a person runs by hand
|
|
903
|
-
("Release 4.2 regression"); each case is marked passed, failed, blocked or
|
|
904
|
-
skipped, step by step, with a bug raised from the step that broke. Read them
|
|
905
|
-
with test_cycles and bugs; make and change cycles with manage_cycle; record
|
|
906
|
-
what happened with record_result; raise or update bugs with save_bug. Never
|
|
907
|
-
mark a case passed on a person's behalf without being told it passed.
|
|
908
|
-
|
|
909
897
|
HOW TO WORK:
|
|
910
898
|
1. Call project_catalog. Reuse what exists \u2014 published, and on Claude's working
|
|
911
899
|
branch \u2014 rather than writing it again.
|
|
912
900
|
2. Before recording, say the plan to the person: the folders, the building-block
|
|
913
|
-
flows (and what each starts after), and the
|
|
901
|
+
flows (and what each starts after), and the test cases (rows + what each
|
|
914
902
|
checks). Then record the blocks, earliest first.
|
|
915
903
|
3. Record with start_flow (pass "after" for the flows it continues from), read
|
|
916
904
|
the page with read_page, and act using the role and name EXACTLY as the page
|
|
@@ -921,7 +909,7 @@ HOW TO WORK:
|
|
|
921
909
|
catalog, then type {{its saveAs name}} into the field that needs it.
|
|
922
910
|
5. Add checks for what each step proves (a heading, a message, a value, the
|
|
923
911
|
URL). replay_flow, then save_flow. A flow must record at least one check.
|
|
924
|
-
6. Build each
|
|
912
|
+
6. Build each test case: preview_case lists its rows and every check, numbered;
|
|
925
913
|
save_case with the numbers to switch on runs the whole case and saves it only
|
|
926
914
|
if it passed. Give the person the review link.
|
|
927
915
|
7. Saving the same name again replaces it on the working branch. Old branches
|
|
@@ -936,7 +924,7 @@ A test that types a fixed new email passes once and fails every run after.`;
|
|
|
936
924
|
var CASE_INPUT = {
|
|
937
925
|
type: "object",
|
|
938
926
|
properties: {
|
|
939
|
-
name: { type: "string", description: 'The
|
|
927
|
+
name: { type: "string", description: 'The test case: the scenario it proves, e.g. "Wrong password is refused".' },
|
|
940
928
|
folder: { type: "string", description: 'The case folder \u2014 area and topic, e.g. "Account \u2013 Sign in". Made if new.' },
|
|
941
929
|
description: { type: "string" },
|
|
942
930
|
rows: {
|
|
@@ -1303,8 +1291,8 @@ ${page}`);
|
|
|
1303
1291
|
},
|
|
1304
1292
|
{
|
|
1305
1293
|
name: "preview_case",
|
|
1306
|
-
title: "Preview
|
|
1307
|
-
description: "Build
|
|
1294
|
+
title: "Preview a test case",
|
|
1295
|
+
description: "Build a test case from flows without saving or running it: its rows (each flow with what it starts after folded in, and which browser it runs in) and every check, numbered, with which are on. Use the numbers in save_case.",
|
|
1308
1296
|
inputSchema: CASE_INPUT,
|
|
1309
1297
|
annotations: { readOnlyHint: true },
|
|
1310
1298
|
run: async (args) => {
|
|
@@ -1314,8 +1302,8 @@ ${page}`);
|
|
|
1314
1302
|
},
|
|
1315
1303
|
{
|
|
1316
1304
|
name: "save_case",
|
|
1317
|
-
title: "Run and save
|
|
1318
|
-
description: "Build the
|
|
1305
|
+
title: "Run and save a test case",
|
|
1306
|
+
description: "Build the test case, run it from the start in a fresh browser exactly as a test run will, and \u2014 only if every row passed \u2014 save it onto Claude's working branch. Saving the same name in the same folder again replaces it. Refused if it switches on no checks.",
|
|
1319
1307
|
inputSchema: CASE_INPUT,
|
|
1320
1308
|
annotations: { openWorldHint: true },
|
|
1321
1309
|
run: async (args) => {
|
|
@@ -1325,10 +1313,90 @@ ${page}`);
|
|
|
1325
1313
|
return text(out);
|
|
1326
1314
|
}
|
|
1327
1315
|
},
|
|
1316
|
+
{
|
|
1317
|
+
name: "read_test_cases",
|
|
1318
|
+
title: "Read test cases",
|
|
1319
|
+
description: "Read test cases by id (tc_\u2026) or exact name: their feature, business steps (Given / When / Then), whether they are automated, and the flows they run. Read a case's steps before automating it.",
|
|
1320
|
+
inputSchema: {
|
|
1321
|
+
type: "object",
|
|
1322
|
+
properties: { projectId: { type: "string" }, cases: { type: "array", items: { type: "string" }, description: "Ids or names." } },
|
|
1323
|
+
required: ["cases"],
|
|
1324
|
+
additionalProperties: false
|
|
1325
|
+
},
|
|
1326
|
+
annotations: { readOnlyHint: true },
|
|
1327
|
+
run: async (args) => {
|
|
1328
|
+
const projectId = await projectFor(args["projectId"]);
|
|
1329
|
+
const refs = Array.isArray(args["cases"]) ? args["cases"].map(String) : [];
|
|
1330
|
+
const { cases, missing } = await service.readCases(projectId, refs);
|
|
1331
|
+
const parts = cases.map((c) => [
|
|
1332
|
+
`${c.folder} \u203A ${c.name} (${c.id}) \u2014 ${c.automated ? `automated, rows: ${c.rows.join(" \u2192 ")}` : "not automated yet"}`,
|
|
1333
|
+
...c.description ? [` ${c.description}`] : [],
|
|
1334
|
+
...c.businessSteps.length ? c.businessSteps.map((s) => ` ${s.keyword} ${s.text}`) : [" (no business steps written)"]
|
|
1335
|
+
].join("\n"));
|
|
1336
|
+
if (missing.length) parts.push(`Not found: ${missing.join(", ")} \u2014 project_catalog lists the test cases.`);
|
|
1337
|
+
return text(parts.join("\n\n") || "No test cases found.");
|
|
1338
|
+
}
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
name: "write_test_cases",
|
|
1342
|
+
title: "Write test cases down, before automating them",
|
|
1343
|
+
description: "Write test cases as business steps \u2014 Given / When / Then \u2014 with no flows yet, grouped by feature (the case folder), on Claude's working branch for review. They show as not automated until a test case with the same name is saved into the same folder with save_case, which keeps these steps. A case its folder already has is skipped. Use it when asked to write, plan or list test cases; record and save_case only when asked to automate.",
|
|
1344
|
+
inputSchema: {
|
|
1345
|
+
type: "object",
|
|
1346
|
+
properties: {
|
|
1347
|
+
projectId: { type: "string" },
|
|
1348
|
+
features: {
|
|
1349
|
+
type: "array",
|
|
1350
|
+
description: 'One entry per feature (case folder), e.g. "Account \u2013 Sign up".',
|
|
1351
|
+
items: {
|
|
1352
|
+
type: "object",
|
|
1353
|
+
properties: {
|
|
1354
|
+
feature: { type: "string" },
|
|
1355
|
+
cases: {
|
|
1356
|
+
type: "array",
|
|
1357
|
+
items: {
|
|
1358
|
+
type: "object",
|
|
1359
|
+
properties: {
|
|
1360
|
+
name: { type: "string", description: 'The scenario it proves, e.g. "A taken email is refused".' },
|
|
1361
|
+
description: { type: "string" },
|
|
1362
|
+
steps: { type: "array", description: `Given / When / Then lines, e.g. "Given the sign-up form is open", "When I sign up with a taken email", "Then I see 'That email is taken'". And / But continue the line above.`, items: { type: "string" } }
|
|
1363
|
+
},
|
|
1364
|
+
required: ["name", "steps"],
|
|
1365
|
+
additionalProperties: false
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
},
|
|
1369
|
+
required: ["feature", "cases"],
|
|
1370
|
+
additionalProperties: false
|
|
1371
|
+
}
|
|
1372
|
+
},
|
|
1373
|
+
branch: { type: "string", description: "Optional: the id of one of Claude's open branches to write to (list_my_branches). Default: the working branch." }
|
|
1374
|
+
},
|
|
1375
|
+
required: ["features"],
|
|
1376
|
+
additionalProperties: false
|
|
1377
|
+
},
|
|
1378
|
+
run: async (args) => {
|
|
1379
|
+
const projectId = await projectFor(args["projectId"]);
|
|
1380
|
+
const features = Array.isArray(args["features"]) ? args["features"] : [];
|
|
1381
|
+
const groups = features.map((f) => ({
|
|
1382
|
+
folder: String(f.feature ?? "").trim(),
|
|
1383
|
+
cases: (Array.isArray(f.cases) ? f.cases : []).map((c) => {
|
|
1384
|
+
const lines = Array.isArray(c.steps) ? c.steps.map(String) : [];
|
|
1385
|
+
const steps = parseGherkin(lines.join("\n"));
|
|
1386
|
+
if (!steps) throw new Error(`the steps of "${String(c.name ?? "")}" must each start with Given, When, Then, And or But`);
|
|
1387
|
+
return { name: String(c.name ?? "").trim(), ...typeof c.description === "string" ? { description: c.description } : {}, businessSteps: steps };
|
|
1388
|
+
})
|
|
1389
|
+
}));
|
|
1390
|
+
const done = await service.writeCases(projectId, groups, str(args["branch"]));
|
|
1391
|
+
catalogs.delete(projectId);
|
|
1392
|
+
const skipped = done.skipped.length ? ` Skipped ${done.skipped.length} the folder already had: ${done.skipped.map((s) => `"${s.name}"`).join(", ")}.` : "";
|
|
1393
|
+
return text(`Wrote ${done.created.length} test case${done.created.length === 1 ? "" : "s"}, not automated yet, on Claude's working branch ${done.branch}.${skipped}`);
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1328
1396
|
{
|
|
1329
1397
|
name: "create_sign_in",
|
|
1330
1398
|
title: "Make a sign-in profile",
|
|
1331
|
-
description: "Turn a saved login flow into a sign-in profile that flows and
|
|
1399
|
+
description: "Turn a saved login flow into a sign-in profile that flows and test cases can start signed in with. The flow must not itself sign in or start after other flows, and must end with a check proving the login worked \u2014 its last check is switched on here. Returns the fields an account fills in. Making the same name again replaces Claude's earlier one.",
|
|
1332
1400
|
inputSchema: {
|
|
1333
1401
|
type: "object",
|
|
1334
1402
|
properties: {
|
|
@@ -1360,7 +1428,7 @@ Next: create_account for each person who signs in this way.`);
|
|
|
1360
1428
|
{
|
|
1361
1429
|
name: "create_account",
|
|
1362
1430
|
title: "Make a sign-in account",
|
|
1363
|
-
description: "An account for a sign-in profile: who signs in, and the values its login flow types, by field label (create_sign_in and project_catalog list them). Secret fields are sealed in the project's vault and never shown again. Then start_flow /
|
|
1431
|
+
description: "An account for a sign-in profile: who signs in, and the values its login flow types, by field label (create_sign_in and project_catalog list them). Secret fields are sealed in the project's vault and never shown again. Then start_flow / test cases can use signIn {profileId, accountId}. Making the same name again updates Claude's earlier one.",
|
|
1364
1432
|
inputSchema: {
|
|
1365
1433
|
type: "object",
|
|
1366
1434
|
properties: {
|
|
@@ -1419,134 +1487,15 @@ Use it with start_flow signIn {"profileId": "\u2026", "accountId": "${made.accou
|
|
|
1419
1487
|
return text(`${made.replaced ? "Updated" : "Made"} the ${made.kind} helper "${made.name}" (id ${made.helperId}) on Claude's working branch ${made.branch}.${made.inputs.length ? ` A step supplies ${made.inputs.map((n) => `{{${n}}}`).join(", ")}.` : ""} Use it with use_helper.`);
|
|
1420
1488
|
}
|
|
1421
1489
|
},
|
|
1422
|
-
{
|
|
1423
|
-
name: "library_cases",
|
|
1424
|
-
title: "Read the test library",
|
|
1425
|
-
description: "The project's test cases \u2014 what the team checks \u2014 with their key (TC-n), priority, status, steps and expected results, and which automations check each one. Optional q (title, key or tag) and featureId narrow it. Read before planning automations: cases with priority P0/P1 and no automation are the gaps.",
|
|
1426
|
-
inputSchema: {
|
|
1427
|
-
type: "object",
|
|
1428
|
-
properties: {
|
|
1429
|
-
projectId: { type: "string" },
|
|
1430
|
-
q: { type: "string", description: "Words in the title, a key like TC-12, or a tag." },
|
|
1431
|
-
featureId: { type: "string", description: "Only this feature's cases, e.g. f_checkout." }
|
|
1432
|
-
},
|
|
1433
|
-
additionalProperties: false
|
|
1434
|
-
},
|
|
1435
|
-
annotations: { readOnlyHint: true },
|
|
1436
|
-
run: async (args) => {
|
|
1437
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1438
|
-
const { cases, total } = await service.library(projectId, {
|
|
1439
|
-
...str(args["q"]) ? { q: str(args["q"]) } : {},
|
|
1440
|
-
...str(args["featureId"]) ? { featureId: str(args["featureId"]) } : {}
|
|
1441
|
-
});
|
|
1442
|
-
if (cases.length === 0) return text(total ? "No test case matches." : "The test library is empty. Write cases with save_test_cases.");
|
|
1443
|
-
const lines = cases.map((c) => {
|
|
1444
|
-
const automated = c.automation.links.length ? `automated by ${c.automation.links.map((l) => l.automationId).join(", ")}${c.automation.links.some((l) => l.caseRevision < c.revision) ? " (case changed since \u2014 check it still matches)" : ""}` : c.automation.state === "not-automatable" ? `manual only: ${c.automation.reason ?? ""}` : c.automation.state === "planned" ? "automation planned" : "NOT AUTOMATED";
|
|
1445
|
-
const steps = c.steps.map((st, i) => ` ${i + 1}. ${st.action}${st.data ? ` [${st.data}]` : ""}${st.expected ? ` \u2192 ${st.expected}` : ""}`).join("\n");
|
|
1446
|
-
return `${c.key} (id ${c.id}, revision ${c.revision}) ${c.title} \u2014 ${c.featureId}, ${c.priority}, ${c.status}, ${automated}${c.preconditions.length ? `
|
|
1447
|
-
Preconditions: ${c.preconditions.join("; ")}` : ""}${steps ? `
|
|
1448
|
-
${steps}` : ""}`;
|
|
1449
|
-
});
|
|
1450
|
-
return text(`${cases.length} of ${total} test cases:
|
|
1451
|
-
${lines.join("\n")}`);
|
|
1452
|
-
}
|
|
1453
|
-
},
|
|
1454
|
-
{
|
|
1455
|
-
name: "save_test_cases",
|
|
1456
|
-
title: "Write test cases",
|
|
1457
|
-
description: `Add test cases to the library, or edit existing ones, in one commit on Claude's working branch. create: [{feature: "Checkout" (or featureId), title, priority?: P0|P1|P2|P3, type?: functional|negative|edge|regression|smoke|accessibility|other, status?: draft|ready|deprecated, preconditions?: [..], steps: [{action, data?, expected?}], tags?: [..], description?}]. update: [{id, revision (from library_cases), ...only the fields to change}]. A new case the library already has is skipped and named in the answer \u2014 never duplicated.`,
|
|
1458
|
-
inputSchema: {
|
|
1459
|
-
type: "object",
|
|
1460
|
-
properties: {
|
|
1461
|
-
projectId: { type: "string" },
|
|
1462
|
-
create: { type: "array", items: { type: "object" } },
|
|
1463
|
-
update: { type: "array", items: { type: "object" } },
|
|
1464
|
-
branch: { type: "string", description: "Optional: the id of one of Claude's open branches to write to (list_my_branches). Default: the working branch." }
|
|
1465
|
-
},
|
|
1466
|
-
additionalProperties: false
|
|
1467
|
-
},
|
|
1468
|
-
run: async (args) => {
|
|
1469
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1470
|
-
const featureOf = (item) => {
|
|
1471
|
-
const id = str(item["featureId"]);
|
|
1472
|
-
if (id) return id;
|
|
1473
|
-
const name = str(item["feature"]) ?? "General";
|
|
1474
|
-
return `f_${name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "general"}`;
|
|
1475
|
-
};
|
|
1476
|
-
const create = (Array.isArray(args["create"]) ? args["create"] : []).map((raw) => {
|
|
1477
|
-
const { feature: _feature, ...item } = raw;
|
|
1478
|
-
return { ...item, featureId: featureOf(raw) };
|
|
1479
|
-
});
|
|
1480
|
-
const update = (Array.isArray(args["update"]) ? args["update"] : []).map((raw) => {
|
|
1481
|
-
const item = raw;
|
|
1482
|
-
if (item["feature"] === void 0) return item;
|
|
1483
|
-
const { feature: _feature, ...rest } = item;
|
|
1484
|
-
return { ...rest, featureId: featureOf(item) };
|
|
1485
|
-
});
|
|
1486
|
-
const saved = await service.saveLibrary(projectId, {
|
|
1487
|
-
create,
|
|
1488
|
-
update,
|
|
1489
|
-
message: "Test cases written with Claude",
|
|
1490
|
-
...str(args["branch"]) ? { changeId: str(args["branch"]) } : {}
|
|
1491
|
-
});
|
|
1492
|
-
const made = saved.created.map((c) => `${c.key} ${c.title} (id ${c.id})`);
|
|
1493
|
-
const edited = saved.updated.map((c) => `${c.key} ${c.title}`);
|
|
1494
|
-
return text([
|
|
1495
|
-
made.length ? `Added ${made.length}: ${made.join("; ")}.` : "",
|
|
1496
|
-
edited.length ? `Updated ${edited.length}: ${edited.join("; ")}.` : "",
|
|
1497
|
-
saved.duplicates.length ? `Skipped ${saved.duplicates.length} already in the library: ${saved.duplicates.map((d) => `"${d.title}" is ${d.matches}`).join("; ")}.` : "",
|
|
1498
|
-
`On Claude's working branch ${saved.branch}.`
|
|
1499
|
-
].filter(Boolean).join(" "));
|
|
1500
|
-
}
|
|
1501
|
-
},
|
|
1502
|
-
{
|
|
1503
|
-
name: "link_test_case",
|
|
1504
|
-
title: "Link an automation to a test case",
|
|
1505
|
-
description: "Record that an automation (a case id from save_case or project_catalog) checks a library test case (its key like TC-12, or its id). unlink: true removes the link. Do this after save_case for every test case the automation proves.",
|
|
1506
|
-
inputSchema: {
|
|
1507
|
-
type: "object",
|
|
1508
|
-
properties: {
|
|
1509
|
-
projectId: { type: "string" },
|
|
1510
|
-
testCase: { type: "string", description: "TC-12, or the test case id." },
|
|
1511
|
-
automationId: { type: "string" },
|
|
1512
|
-
unlink: { type: "boolean" },
|
|
1513
|
-
branch: { type: "string" }
|
|
1514
|
-
},
|
|
1515
|
-
required: ["testCase", "automationId"],
|
|
1516
|
-
additionalProperties: false
|
|
1517
|
-
},
|
|
1518
|
-
run: async (args) => {
|
|
1519
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1520
|
-
const wanted = str(args["testCase"]) ?? "";
|
|
1521
|
-
const automationId = str(args["automationId"]) ?? "";
|
|
1522
|
-
const { cases } = await service.library(projectId);
|
|
1523
|
-
const target = cases.find((c) => c.id === wanted || c.key.toLowerCase() === wanted.toLowerCase());
|
|
1524
|
-
if (!target) return text(`No test case ${wanted} in the library \u2014 library_cases lists them.`);
|
|
1525
|
-
const has = target.automation.links.some((l) => l.automationId === automationId);
|
|
1526
|
-
const unlink = args["unlink"] === true;
|
|
1527
|
-
if (has !== unlink) return text(`${target.key} is ${unlink ? "not" : "already"} linked to ${automationId}.`);
|
|
1528
|
-
const links = unlink ? target.automation.links.filter((l) => l.automationId !== automationId) : [...target.automation.links, { automationId, caseRevision: target.revision + 1, at: (/* @__PURE__ */ new Date()).toISOString(), by: "Claude" }];
|
|
1529
|
-
const state = links.length ? "automated" : target.automation.state === "automated" ? "none" : target.automation.state;
|
|
1530
|
-
const saved = await service.saveLibrary(projectId, {
|
|
1531
|
-
update: [{ id: target.id, revision: target.revision, automation: { ...target.automation, state, links } }],
|
|
1532
|
-
message: `${unlink ? "Unlink" : "Link"} ${target.key} ${unlink ? "from" : "to"} ${automationId}`,
|
|
1533
|
-
...str(args["branch"]) ? { changeId: str(args["branch"]) } : {}
|
|
1534
|
-
});
|
|
1535
|
-
return text(`${unlink ? "Unlinked" : "Linked"} ${target.key} "${target.title}" ${unlink ? "from" : "to"} automation ${automationId}, on Claude's working branch ${saved.branch}.`);
|
|
1536
|
-
}
|
|
1537
|
-
},
|
|
1538
1490
|
{
|
|
1539
1491
|
name: "project_brief",
|
|
1540
1492
|
title: "Read the project's memory",
|
|
1541
|
-
description: "The project's own knowledge, in one call: About the product (product.md), memory/decisions.md, what each linked codebase folder was learned as, the list of feature pages and session summaries
|
|
1493
|
+
description: "The project's own knowledge, in one call: About the product (product.md), memory/decisions.md, what each linked codebase folder was learned as, and the list of feature pages and session summaries. Call it first in every conversation.",
|
|
1542
1494
|
inputSchema: { type: "object", properties: { projectId: { type: "string" } }, additionalProperties: false },
|
|
1543
1495
|
annotations: { readOnlyHint: true },
|
|
1544
1496
|
run: async (args) => {
|
|
1545
1497
|
const projectId = await projectFor(args["projectId"]);
|
|
1546
|
-
const
|
|
1547
|
-
service.knowledgePages(projectId),
|
|
1548
|
-
service.sources(projectId).catch(() => ({ sources: [] }))
|
|
1549
|
-
]);
|
|
1498
|
+
const { pages } = await service.knowledgePages(projectId);
|
|
1550
1499
|
const read = async (path) => pages.some((p) => p.path === path) ? (await service.knowledgePage(projectId, path)).content : null;
|
|
1551
1500
|
const [product, glossary, decisions] = await Promise.all(["product.md", "glossary.md", "memory/decisions.md"].map(read));
|
|
1552
1501
|
const codebases = pages.filter((p) => p.path.startsWith("codebase/"));
|
|
@@ -1562,9 +1511,7 @@ ${decisions}` : "",
|
|
|
1562
1511
|
codebases.length ? `## Codebase, as learned
|
|
1563
1512
|
${headers.join("\n")}` : "No codebase learned yet.",
|
|
1564
1513
|
`## Other pages
|
|
1565
|
-
${pages.filter((p) => !shown.has(p.path)).map((p) => `- ${p.path}`).join("\n") || "none"}
|
|
1566
|
-
`## Documents
|
|
1567
|
-
${sources.map((s) => `- ${s.title} (id ${s.id}, v${s.version}) \u2014 requirements: ${s.requirements["new"] ?? 0} new, ${s.requirements["changed"] ?? 0} changed, ${s.requirements["active"] ?? 0} unchanged, ${s.requirements["removed"] ?? 0} removed`).join("\n") || "none"}`
|
|
1514
|
+
${pages.filter((p) => !shown.has(p.path)).map((p) => `- ${p.path}`).join("\n") || "none"}`
|
|
1568
1515
|
];
|
|
1569
1516
|
return text(parts.filter(Boolean).join("\n\n"));
|
|
1570
1517
|
}
|
|
@@ -1616,353 +1563,6 @@ ${sources.map((s) => `- ${s.title} (id ${s.id}, v${s.version}) \u2014 requiremen
|
|
|
1616
1563
|
return text(`Wrote ${saved.path} (${saved.length} characters) on Claude's working branch ${saved.branch}.`);
|
|
1617
1564
|
}
|
|
1618
1565
|
},
|
|
1619
|
-
{
|
|
1620
|
-
name: "sources",
|
|
1621
|
-
title: "Read source documents",
|
|
1622
|
-
description: "Without sourceId: every source (PRDs, CRs, feature docs, Jira stories, Figma files\u2026) with its version and how many requirements are new, changed, unchanged or removed. With sourceId: the latest version's text.",
|
|
1623
|
-
inputSchema: { type: "object", properties: { projectId: { type: "string" }, sourceId: { type: "string" } }, additionalProperties: false },
|
|
1624
|
-
annotations: { readOnlyHint: true },
|
|
1625
|
-
run: async (args) => {
|
|
1626
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1627
|
-
const id = str(args["sourceId"]);
|
|
1628
|
-
if (id) {
|
|
1629
|
-
const source = await service.source(projectId, id);
|
|
1630
|
-
return text(`# ${source.title} (v${source.version})
|
|
1631
|
-
|
|
1632
|
-
${source.text.slice(0, 6e4)}`);
|
|
1633
|
-
}
|
|
1634
|
-
const { sources } = await service.sources(projectId);
|
|
1635
|
-
if (!sources.length) return text("No sources yet. add_source adds a document by text or link; the Knowledge screen takes files.");
|
|
1636
|
-
return text(sources.map((s) => `${s.title} (id ${s.id}, v${s.version}, ${s.kind}${s.externalUrl ? `, ${s.externalUrl}` : ""}) \u2014 ${s.requirements["new"] ?? 0} new, ${s.requirements["changed"] ?? 0} changed, ${s.requirements["active"] ?? 0} unchanged, ${s.requirements["removed"] ?? 0} removed`).join("\n"));
|
|
1637
|
-
}
|
|
1638
|
-
},
|
|
1639
|
-
{
|
|
1640
|
-
name: "add_source",
|
|
1641
|
-
title: "Add a source document",
|
|
1642
|
-
description: "Add a document tests are written from \u2014 as text (Markdown is best: headings and lists are how requirements are found) or as a public link \u2014 or a new version of one (sourceId). The same text again is not a new version. Requirements are found and compared at once.",
|
|
1643
|
-
inputSchema: {
|
|
1644
|
-
type: "object",
|
|
1645
|
-
properties: { projectId: { type: "string" }, title: { type: "string" }, text: { type: "string" }, url: { type: "string" }, sourceId: { type: "string" } },
|
|
1646
|
-
additionalProperties: false
|
|
1647
|
-
},
|
|
1648
|
-
run: async (args) => {
|
|
1649
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1650
|
-
const made = await service.addSource(projectId, {
|
|
1651
|
-
...str(args["title"]) ? { title: str(args["title"]) } : {},
|
|
1652
|
-
...typeof args["text"] === "string" ? { text: args["text"] } : {},
|
|
1653
|
-
...str(args["url"]) ? { url: str(args["url"]) } : {}
|
|
1654
|
-
}, str(args["sourceId"]));
|
|
1655
|
-
const r = made.requirements;
|
|
1656
|
-
return text(made.changed ? `${made.source.title} is now v${made.source.version} (id ${made.source.id}). Requirements: ${r["added"] ?? 0} new, ${r["changed"] ?? 0} changed, ${r["unchanged"] ?? 0} unchanged, ${r["removed"] ?? 0} removed.` : `${made.source.title} has not changed since v${made.source.version}; nothing new.`);
|
|
1657
|
-
}
|
|
1658
|
-
},
|
|
1659
|
-
{
|
|
1660
|
-
name: "delete_source",
|
|
1661
|
-
title: "Delete a source document",
|
|
1662
|
-
description: "Delete a source and everything found in it, for good. Only after the person has said to.",
|
|
1663
|
-
inputSchema: { type: "object", properties: { projectId: { type: "string" }, sourceId: { type: "string" } }, required: ["sourceId"], additionalProperties: false },
|
|
1664
|
-
annotations: { destructiveHint: true },
|
|
1665
|
-
run: async (args) => {
|
|
1666
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1667
|
-
await service.deleteSource(projectId, str(args["sourceId"]) ?? "");
|
|
1668
|
-
return text(`Deleted the source ${str(args["sourceId"])} and its requirements.`);
|
|
1669
|
-
}
|
|
1670
|
-
},
|
|
1671
|
-
{
|
|
1672
|
-
name: "update_requirement",
|
|
1673
|
-
title: "File or correct a requirement",
|
|
1674
|
-
description: "Set which feature a requirement belongs to (featureId like f_checkout), or correct its wording where extraction got it wrong.",
|
|
1675
|
-
inputSchema: { type: "object", properties: { projectId: { type: "string" }, requirementId: { type: "string" }, featureId: { type: "string" }, text: { type: "string" } }, required: ["requirementId"], additionalProperties: false },
|
|
1676
|
-
run: async (args) => {
|
|
1677
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1678
|
-
const saved = await service.updateRequirement(projectId, str(args["requirementId"]) ?? "", {
|
|
1679
|
-
...typeof args["featureId"] === "string" ? { featureId: args["featureId"] } : {},
|
|
1680
|
-
...str(args["text"]) ? { text: str(args["text"]) } : {}
|
|
1681
|
-
});
|
|
1682
|
-
return text(`${saved.id}${saved.featureId ? ` is filed under ${saved.featureId}` : ""}: ${saved.text}`);
|
|
1683
|
-
}
|
|
1684
|
-
},
|
|
1685
|
-
{
|
|
1686
|
-
name: "requirements",
|
|
1687
|
-
title: "Read requirements",
|
|
1688
|
-
description: "Requirements found in sources, each with where it is (heading path), its state as of the source's latest version \u2014 new, changed (with what it said before), active (unchanged) or removed \u2014 and its id for covers. Optional sourceId and state narrow it.",
|
|
1689
|
-
inputSchema: { type: "object", properties: { projectId: { type: "string" }, sourceId: { type: "string" }, state: { type: "string", enum: ["new", "changed", "active", "removed"] } }, additionalProperties: false },
|
|
1690
|
-
annotations: { readOnlyHint: true },
|
|
1691
|
-
run: async (args) => {
|
|
1692
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1693
|
-
const [{ requirements }, { cases }] = await Promise.all([
|
|
1694
|
-
service.requirements(projectId, { ...str(args["sourceId"]) ? { sourceId: str(args["sourceId"]) } : {}, ...str(args["state"]) ? { state: str(args["state"]) } : {} }),
|
|
1695
|
-
service.library(projectId)
|
|
1696
|
-
]);
|
|
1697
|
-
if (!requirements.length) return text("No requirements match.");
|
|
1698
|
-
return text(requirements.map((r) => {
|
|
1699
|
-
const covering = cases.filter((c) => c.covers.includes(r.id)).map((c) => c.key);
|
|
1700
|
-
return `${r.id} [${r.state}] ${r.sourceTitle} \u203A ${r.anchor}: ${r.text}${r.state === "changed" && r.history.length ? `
|
|
1701
|
-
was: ${r.history.at(-1).text}` : ""}
|
|
1702
|
-
covered by: ${covering.join(", ") || "nothing yet"}`;
|
|
1703
|
-
}).join("\n"));
|
|
1704
|
-
}
|
|
1705
|
-
},
|
|
1706
|
-
{
|
|
1707
|
-
name: "find_similar_cases",
|
|
1708
|
-
title: "Find test cases like this one",
|
|
1709
|
-
description: "Before writing a test case, find existing ones that may already check the same thing \u2014 by title and steps. You judge whether one is really the same; if it is, link the requirement to it (covers) instead of writing a duplicate.",
|
|
1710
|
-
inputSchema: { type: "object", properties: { projectId: { type: "string" }, text: { type: "string", description: "The case you are about to write: its title and steps." } }, required: ["text"], additionalProperties: false },
|
|
1711
|
-
annotations: { readOnlyHint: true },
|
|
1712
|
-
run: async (args) => {
|
|
1713
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1714
|
-
const wanted = String(args["text"] ?? "");
|
|
1715
|
-
const { cases } = await service.library(projectId);
|
|
1716
|
-
const scored = cases.map((c) => ({ c, score: similarity(wanted, [c.title, ...c.steps.flatMap((st) => [st.action, st.expected ?? ""])].join(" ")) })).filter((x) => x.score >= 0.2).sort((a, b) => b.score - a.score).slice(0, 8);
|
|
1717
|
-
if (!scored.length) return text("Nothing similar in the library.");
|
|
1718
|
-
return text(scored.map(({ c, score }) => `${c.key} (id ${c.id}, revision ${c.revision}, ${Math.round(score * 100)}% alike) ${c.title}
|
|
1719
|
-
${c.steps.map((st, i) => ` ${i + 1}. ${st.action}${st.expected ? ` \u2192 ${st.expected}` : ""}`).join("\n")}`).join("\n"));
|
|
1720
|
-
}
|
|
1721
|
-
},
|
|
1722
|
-
{
|
|
1723
|
-
name: "save_automation_plan",
|
|
1724
|
-
title: "Write the plan to automate a test case",
|
|
1725
|
-
description: "Save the plan for automating a test case (TC key or id) before recording anything: flows to reuse, flows to record, which check proves each expected result, accounts and helpers it needs, and what cannot be automated and why. Marks it planned. approved: true only when the person has said the plan is right.",
|
|
1726
|
-
inputSchema: {
|
|
1727
|
-
type: "object",
|
|
1728
|
-
properties: { projectId: { type: "string" }, testCase: { type: "string" }, plan: { type: "string" }, approved: { type: "boolean" }, branch: { type: "string" } },
|
|
1729
|
-
required: ["testCase", "plan"],
|
|
1730
|
-
additionalProperties: false
|
|
1731
|
-
},
|
|
1732
|
-
run: async (args) => {
|
|
1733
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1734
|
-
const ref = str(args["testCase"]) ?? "";
|
|
1735
|
-
const { cases } = await service.library(projectId);
|
|
1736
|
-
const target = cases.find((c) => c.id === ref || c.key.toLowerCase() === ref.toLowerCase());
|
|
1737
|
-
if (!target) return text(`No test case ${ref} in the library.`);
|
|
1738
|
-
const saved = await service.saveLibrary(projectId, {
|
|
1739
|
-
update: [{
|
|
1740
|
-
id: target.id,
|
|
1741
|
-
revision: target.revision,
|
|
1742
|
-
automation: {
|
|
1743
|
-
...target.automation,
|
|
1744
|
-
plan: String(args["plan"] ?? ""),
|
|
1745
|
-
planApproved: args["approved"] === true,
|
|
1746
|
-
state: target.automation.links.length ? target.automation.state : "planned"
|
|
1747
|
-
}
|
|
1748
|
-
}],
|
|
1749
|
-
message: `Automation plan for ${target.key}`,
|
|
1750
|
-
...str(args["branch"]) ? { changeId: str(args["branch"]) } : {}
|
|
1751
|
-
});
|
|
1752
|
-
return text(`Saved the ${args["approved"] === true ? "approved " : ""}automation plan for ${target.key} "${target.title}" on Claude's working branch ${saved.branch}.`);
|
|
1753
|
-
}
|
|
1754
|
-
},
|
|
1755
|
-
{
|
|
1756
|
-
name: "test_cycles",
|
|
1757
|
-
title: "Read manual test cycles",
|
|
1758
|
-
description: "Manual test cycles and how they are going. Without cycleId: every cycle with its counts (passed, failed, blocked, untested). With cycleId: each case in it \u2014 key, title, steps, the outcome of each step, notes, evidence and bugs.",
|
|
1759
|
-
inputSchema: {
|
|
1760
|
-
type: "object",
|
|
1761
|
-
properties: { projectId: { type: "string" }, cycleId: { type: "string" } },
|
|
1762
|
-
additionalProperties: false
|
|
1763
|
-
},
|
|
1764
|
-
annotations: { readOnlyHint: true },
|
|
1765
|
-
run: async (args) => {
|
|
1766
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1767
|
-
const counts = (s) => `${s.passed} passed, ${s.failed} failed, ${s.blocked} blocked, ${s.skipped} skipped, ${s.untested} untested of ${s.total}`;
|
|
1768
|
-
const id = str(args["cycleId"]);
|
|
1769
|
-
if (!id) {
|
|
1770
|
-
const { cycles } = await service.cycles(projectId);
|
|
1771
|
-
if (!cycles.length) return text("No test cycles yet. manage_cycle with action create makes one from library cases.");
|
|
1772
|
-
return text(cycles.map((c) => `${c.name} (id ${c.id}, ${c.status}) \u2014 ${counts(c.summary)}`).join("\n"));
|
|
1773
|
-
}
|
|
1774
|
-
const cycle = await service.cycle(projectId, id);
|
|
1775
|
-
const lines = cycle.entries.map((e) => {
|
|
1776
|
-
const r = e.result;
|
|
1777
|
-
const steps = e.steps.map((st, i) => {
|
|
1778
|
-
const got = r?.steps.find((x) => x.stepId === st.id);
|
|
1779
|
-
return ` ${i + 1}. ${st.action}${st.expected ? ` \u2192 ${st.expected}` : ""}: ${got?.outcome ?? "untested"}${got?.actual ? ` (actual: ${got.actual})` : ""}`;
|
|
1780
|
-
}).join("\n");
|
|
1781
|
-
return `${e.key} ${e.title} [${e.priority}] \u2014 ${r?.outcome ?? "untested"}${e.assignee ? `, assigned to ${e.assignee}` : ""}${r?.note ? `
|
|
1782
|
-
Note: ${r.note}` : ""}${r?.bugIds.length ? `
|
|
1783
|
-
Bugs: ${r.bugIds.join(", ")}` : ""}${r?.evidence.length ? `
|
|
1784
|
-
Evidence: ${r.evidence.map((ev) => ev.url ? `${ev.name} <${ev.url}>` : ev.name).join(", ")}` : ""}${steps ? `
|
|
1785
|
-
${steps}` : ""}`;
|
|
1786
|
-
});
|
|
1787
|
-
return text(`${cycle.name} (${cycle.status}) \u2014 ${counts(cycle.summary)}
|
|
1788
|
-
${lines.join("\n")}`);
|
|
1789
|
-
}
|
|
1790
|
-
},
|
|
1791
|
-
{
|
|
1792
|
-
name: "manage_cycle",
|
|
1793
|
-
title: "Make or change a test cycle",
|
|
1794
|
-
description: "action create: {name, description?, testCases: [TC keys or ids]} \u2014 cases are taken from the library as they are now (on Claude's working branch when it has one). action update: {cycleId, name?, description?, addTestCases?, removeTestCases?, assign?: {testCase, assignee}}. action close / reopen: {cycleId}. action delete: {cycleId} \u2014 deletes the cycle and its results for good; only after the person has said to.",
|
|
1795
|
-
inputSchema: {
|
|
1796
|
-
type: "object",
|
|
1797
|
-
properties: {
|
|
1798
|
-
projectId: { type: "string" },
|
|
1799
|
-
action: { type: "string", enum: ["create", "update", "close", "reopen", "delete"] },
|
|
1800
|
-
cycleId: { type: "string" },
|
|
1801
|
-
name: { type: "string" },
|
|
1802
|
-
description: { type: "string" },
|
|
1803
|
-
testCases: { type: "array", items: { type: "string" } },
|
|
1804
|
-
addTestCases: { type: "array", items: { type: "string" } },
|
|
1805
|
-
removeTestCases: { type: "array", items: { type: "string" } },
|
|
1806
|
-
assign: { type: "object", properties: { testCase: { type: "string" }, assignee: { type: "string" } }, additionalProperties: false }
|
|
1807
|
-
},
|
|
1808
|
-
required: ["action"],
|
|
1809
|
-
additionalProperties: false
|
|
1810
|
-
},
|
|
1811
|
-
run: async (args) => {
|
|
1812
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1813
|
-
const action = str(args["action"]);
|
|
1814
|
-
const cycleId = str(args["cycleId"]);
|
|
1815
|
-
const resolve = async (refs) => {
|
|
1816
|
-
const wanted = Array.isArray(refs) ? refs.map(String) : [];
|
|
1817
|
-
if (!wanted.length) return { ids: [], changeId: void 0 };
|
|
1818
|
-
const lib = await service.library(projectId);
|
|
1819
|
-
const ids = wanted.map((ref) => lib.cases.find((c) => c.id === ref || c.key.toLowerCase() === ref.toLowerCase())?.id);
|
|
1820
|
-
const missing = wanted.filter((_, i) => !ids[i]);
|
|
1821
|
-
if (missing.length) throw new Error(`no test case ${missing.join(", ")} in the library \u2014 library_cases lists them`);
|
|
1822
|
-
return { ids, changeId: lib.changeId };
|
|
1823
|
-
};
|
|
1824
|
-
if (action === "create") {
|
|
1825
|
-
const name = str(args["name"]);
|
|
1826
|
-
if (!name) return text("A cycle needs a name.");
|
|
1827
|
-
const { ids, changeId } = await resolve(args["testCases"]);
|
|
1828
|
-
const made = await service.createCycle(projectId, { name, ...str(args["description"]) ? { description: str(args["description"]) } : {}, testCaseIds: ids }, changeId);
|
|
1829
|
-
return text(`Made the cycle "${made.name}" (id ${made.id}) with ${made.entries.length} case${made.entries.length === 1 ? "" : "s"}: ${made.entries.map((e) => e.key).join(", ")}.`);
|
|
1830
|
-
}
|
|
1831
|
-
if (!cycleId) return text("Say which cycle (cycleId \u2014 test_cycles lists them).");
|
|
1832
|
-
if (action === "delete") {
|
|
1833
|
-
await service.deleteCycle(projectId, cycleId);
|
|
1834
|
-
return text(`Deleted the cycle ${cycleId} and its results.`);
|
|
1835
|
-
}
|
|
1836
|
-
if (action === "close" || action === "reopen") {
|
|
1837
|
-
const saved2 = await service.updateCycle(projectId, cycleId, { status: action === "close" ? "closed" : "open" });
|
|
1838
|
-
return text(`${saved2.name} is ${saved2.status}.`);
|
|
1839
|
-
}
|
|
1840
|
-
const current = await service.cycle(projectId, cycleId);
|
|
1841
|
-
const inCycle = (ref) => current.entries.find((e) => e.testCaseId === ref || e.key.toLowerCase() === ref.toLowerCase())?.testCaseId;
|
|
1842
|
-
const added = await resolve(args["addTestCases"]);
|
|
1843
|
-
const body = {
|
|
1844
|
-
...str(args["name"]) ? { name: str(args["name"]) } : {},
|
|
1845
|
-
...typeof args["description"] === "string" ? { description: args["description"] } : {},
|
|
1846
|
-
...added.ids.length ? { addCaseIds: added.ids } : {},
|
|
1847
|
-
...Array.isArray(args["removeTestCases"]) ? { removeCaseIds: args["removeTestCases"].map(String).map((r) => inCycle(r) ?? r) } : {}
|
|
1848
|
-
};
|
|
1849
|
-
const assign = args["assign"];
|
|
1850
|
-
if (assign?.testCase) body["assign"] = { testCaseId: inCycle(assign.testCase) ?? assign.testCase, assignee: assign.assignee ?? "" };
|
|
1851
|
-
const saved = await service.updateCycle(projectId, cycleId, body, added.changeId);
|
|
1852
|
-
return text(`Updated "${saved.name}": ${saved.entries.length} case${saved.entries.length === 1 ? "" : "s"} (${saved.entries.map((e) => e.key).join(", ")}).`);
|
|
1853
|
-
}
|
|
1854
|
-
},
|
|
1855
|
-
{
|
|
1856
|
-
name: "record_result",
|
|
1857
|
-
title: "Record how a case went",
|
|
1858
|
-
description: "Record a manual result for one case in a cycle: steps [{step: its number (1, 2\u2026), outcome: passed|failed|blocked|skipped|untested, actual?: what really happened}], or a whole outcome, a note, links as evidence ([{name, url}]). Any failed step fails the case. retest: true keeps the earlier attempt in the case's history. Only record what the person tells you happened.",
|
|
1859
|
-
inputSchema: {
|
|
1860
|
-
type: "object",
|
|
1861
|
-
properties: {
|
|
1862
|
-
projectId: { type: "string" },
|
|
1863
|
-
cycleId: { type: "string" },
|
|
1864
|
-
testCase: { type: "string", description: "TC-12, or the test case id." },
|
|
1865
|
-
steps: { type: "array", items: { type: "object" } },
|
|
1866
|
-
outcome: { type: "string", enum: ["passed", "failed", "blocked", "skipped", "untested"] },
|
|
1867
|
-
note: { type: "string" },
|
|
1868
|
-
retest: { type: "boolean" },
|
|
1869
|
-
evidenceLinks: { type: "array", items: { type: "object", properties: { name: { type: "string" }, url: { type: "string" } }, additionalProperties: false } }
|
|
1870
|
-
},
|
|
1871
|
-
required: ["cycleId", "testCase"],
|
|
1872
|
-
additionalProperties: false
|
|
1873
|
-
},
|
|
1874
|
-
run: async (args) => {
|
|
1875
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1876
|
-
const cycleId = str(args["cycleId"]) ?? "";
|
|
1877
|
-
const cycle = await service.cycle(projectId, cycleId);
|
|
1878
|
-
const ref = str(args["testCase"]) ?? "";
|
|
1879
|
-
const entry = cycle.entries.find((e) => e.testCaseId === ref || e.key.toLowerCase() === ref.toLowerCase());
|
|
1880
|
-
if (!entry) return text(`${ref} is not in "${cycle.name}" \u2014 test_cycles lists its cases.`);
|
|
1881
|
-
const steps = (Array.isArray(args["steps"]) ? args["steps"] : []).map((raw) => {
|
|
1882
|
-
const item = raw;
|
|
1883
|
-
const at = typeof item.step === "number" ? entry.steps[item.step - 1]?.id : void 0;
|
|
1884
|
-
return { stepId: String(item.stepId ?? at ?? ""), outcome: String(item.outcome ?? ""), ...typeof item.actual === "string" ? { actual: item.actual } : {} };
|
|
1885
|
-
});
|
|
1886
|
-
const saved = await service.recordResult(projectId, cycleId, entry.testCaseId, {
|
|
1887
|
-
steps,
|
|
1888
|
-
...str(args["outcome"]) ? { outcome: str(args["outcome"]) } : {},
|
|
1889
|
-
...typeof args["note"] === "string" ? { note: args["note"] } : {},
|
|
1890
|
-
...args["retest"] === true ? { retest: true } : {}
|
|
1891
|
-
});
|
|
1892
|
-
for (const link of Array.isArray(args["evidenceLinks"]) ? args["evidenceLinks"] : []) {
|
|
1893
|
-
const { name, url } = link;
|
|
1894
|
-
if (url) await service.addEvidenceLink(projectId, cycleId, entry.testCaseId, { name: name || "Evidence", url });
|
|
1895
|
-
}
|
|
1896
|
-
const now = saved.entries.find((e) => e.testCaseId === entry.testCaseId);
|
|
1897
|
-
return text(`${entry.key} is ${now.result?.outcome ?? "untested"} in "${saved.name}". The cycle: ${saved.summary.passed} passed, ${saved.summary.failed} failed, ${saved.summary.blocked} blocked, ${saved.summary.untested} untested.`);
|
|
1898
|
-
}
|
|
1899
|
-
},
|
|
1900
|
-
{
|
|
1901
|
-
name: "bugs",
|
|
1902
|
-
title: "Read bugs",
|
|
1903
|
-
description: "The project's bugs: key (BUG-n), title, severity, status, and the test cases and cycle step they were found at. Optional status narrows it (open, in-progress, fixed, verified, closed, wont-fix).",
|
|
1904
|
-
inputSchema: { type: "object", properties: { projectId: { type: "string" }, status: { type: "string" } }, additionalProperties: false },
|
|
1905
|
-
annotations: { readOnlyHint: true },
|
|
1906
|
-
run: async (args) => {
|
|
1907
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1908
|
-
const { bugs } = await service.bugs(projectId);
|
|
1909
|
-
const shown = str(args["status"]) ? bugs.filter((b) => b.status === str(args["status"])) : bugs;
|
|
1910
|
-
if (!shown.length) return text("No bugs.");
|
|
1911
|
-
return text(shown.map((b) => `${b.key} (id ${b.id}) ${b.title} \u2014 ${b.severity}, ${b.status}${b.testCaseIds.length ? `, test cases ${b.testCaseIds.join(", ")}` : ""}${b.description ? `
|
|
1912
|
-
${b.description.slice(0, 300)}` : ""}`).join("\n"));
|
|
1913
|
-
}
|
|
1914
|
-
},
|
|
1915
|
-
{
|
|
1916
|
-
name: "save_bug",
|
|
1917
|
-
title: "Raise or update a bug",
|
|
1918
|
-
description: "New: {title, description?, severity?: critical|major|minor|trivial, cycleId?, testCase?, step? (its number)} \u2014 found in a cycle, it is attached to that case's result there. Update: {bugId, title?, description?, severity?, status?: open|in-progress|fixed|verified|closed|wont-fix}.",
|
|
1919
|
-
inputSchema: {
|
|
1920
|
-
type: "object",
|
|
1921
|
-
properties: {
|
|
1922
|
-
projectId: { type: "string" },
|
|
1923
|
-
bugId: { type: "string" },
|
|
1924
|
-
title: { type: "string" },
|
|
1925
|
-
description: { type: "string" },
|
|
1926
|
-
severity: { type: "string" },
|
|
1927
|
-
status: { type: "string" },
|
|
1928
|
-
cycleId: { type: "string" },
|
|
1929
|
-
testCase: { type: "string" },
|
|
1930
|
-
step: { type: "number" }
|
|
1931
|
-
},
|
|
1932
|
-
additionalProperties: false
|
|
1933
|
-
},
|
|
1934
|
-
run: async (args) => {
|
|
1935
|
-
const projectId = await projectFor(args["projectId"]);
|
|
1936
|
-
const bugId = str(args["bugId"]);
|
|
1937
|
-
if (bugId) {
|
|
1938
|
-
const body2 = Object.fromEntries(["title", "description", "severity", "status"].filter((k) => typeof args[k] === "string").map((k) => [k, args[k]]));
|
|
1939
|
-
const saved = await service.updateBug(projectId, bugId, body2);
|
|
1940
|
-
return text(`${saved.key} "${saved.title}" is ${saved.status} (${saved.severity}).`);
|
|
1941
|
-
}
|
|
1942
|
-
const title = str(args["title"]);
|
|
1943
|
-
if (!title) return text("A bug needs a title.");
|
|
1944
|
-
const body = {
|
|
1945
|
-
title,
|
|
1946
|
-
...typeof args["description"] === "string" ? { description: args["description"] } : {},
|
|
1947
|
-
...str(args["severity"]) ? { severity: str(args["severity"]) } : {}
|
|
1948
|
-
};
|
|
1949
|
-
const cycleId = str(args["cycleId"]);
|
|
1950
|
-
const ref = str(args["testCase"]);
|
|
1951
|
-
if (cycleId && ref) {
|
|
1952
|
-
const cycle = await service.cycle(projectId, cycleId);
|
|
1953
|
-
const entry = cycle.entries.find((e) => e.testCaseId === ref || e.key.toLowerCase() === ref.toLowerCase());
|
|
1954
|
-
if (!entry) return text(`${ref} is not in that cycle.`);
|
|
1955
|
-
const step = typeof args["step"] === "number" ? entry.steps[args["step"] - 1]?.id : void 0;
|
|
1956
|
-
body["foundAt"] = { cycleId, testCaseId: entry.testCaseId, ...step ? { stepId: step } : {} };
|
|
1957
|
-
} else if (ref) {
|
|
1958
|
-
const lib = await service.library(projectId);
|
|
1959
|
-
const found = lib.cases.find((c) => c.id === ref || c.key.toLowerCase() === ref.toLowerCase());
|
|
1960
|
-
if (found) body["testCaseIds"] = [found.id];
|
|
1961
|
-
}
|
|
1962
|
-
const made = await service.createBug(projectId, body);
|
|
1963
|
-
return text(`Raised ${made.key} "${made.title}" (${made.severity}).`);
|
|
1964
|
-
}
|
|
1965
|
-
},
|
|
1966
1566
|
{
|
|
1967
1567
|
name: "close_browser",
|
|
1968
1568
|
title: "Close the browser",
|