@skyramp/mcp 0.4.3 → 0.4.4-rc.1

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.
Files changed (69) hide show
  1. package/build/playwright/traceRecordingPrompt.js +2 -1
  2. package/build/prompts/orderAssertionRule.d.ts +9 -0
  3. package/build/prompts/orderAssertionRule.js +9 -0
  4. package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
  5. package/build/prompts/testbot/testbot-prompts.js +10 -7
  6. package/build/recommendation/answers.d.ts +54 -19
  7. package/build/recommendation/answers.js +102 -31
  8. package/build/recommendation/blockerProbes.d.ts +13 -0
  9. package/build/recommendation/blockerProbes.js +91 -0
  10. package/build/recommendation/blockers.d.ts +65 -0
  11. package/build/recommendation/blockers.js +135 -0
  12. package/build/recommendation/pullRequestText.d.ts +2 -0
  13. package/build/recommendation/pullRequestText.js +16 -0
  14. package/build/recommendation/registerPlan.d.ts +7 -4
  15. package/build/recommendation/registerPlan.js +37 -11
  16. package/build/recommendation/types.d.ts +21 -4
  17. package/build/recommendation/verifierContracts.d.ts +6 -6
  18. package/build/recommendation/verifierContracts.js +8 -7
  19. package/build/recommendation/verifiers/citedPath.d.ts +2 -2
  20. package/build/recommendation/verifiers/citedPath.js +2 -2
  21. package/build/recommendation/verifiers/coverage.js +25 -25
  22. package/build/recommendation/verifiers/deliveredMatchesPlan.js +3 -3
  23. package/build/recommendation/verifiers/issueTraceability.d.ts +1 -5
  24. package/build/recommendation/verifiers/issueTraceability.js +4 -35
  25. package/build/tools/executeSkyrampTestTool.js +8 -0
  26. package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -0
  27. package/build/tools/generate-tests/generateE2ERestTool.js +9 -1
  28. package/build/tools/generate-tests/generateUIRestTool.d.ts +2 -0
  29. package/build/tools/generate-tests/generateUIRestTool.js +9 -1
  30. package/build/tools/submitReportTool.d.ts +9 -9
  31. package/build/tools/submitReportTool.js +178 -26
  32. package/build/tools/test-management/registerTestPlanTool.d.ts +20 -14
  33. package/build/tools/test-management/registerTestPlanTool.js +30 -14
  34. package/build/types/TestTypes.d.ts +7 -0
  35. package/build/types/TestTypes.js +22 -0
  36. package/build/types/TestbotReport.d.ts +4 -1
  37. package/build/utils/assertion-verify/metrics.js +12 -0
  38. package/build/utils/findUp.d.ts +20 -0
  39. package/build/utils/findUp.js +59 -0
  40. package/build/utils/language-helper.js +1 -1
  41. package/build/utils/repoSdkVersion.d.ts +16 -0
  42. package/build/utils/repoSdkVersion.js +88 -0
  43. package/build/utils/testDependencyPolicy.d.ts +20 -0
  44. package/build/utils/testDependencyPolicy.js +64 -4
  45. package/build/utils/utils-verify/typecheck.js +3 -18
  46. package/node_modules/playwright/lib/mcp/skyramp/assertOrder.js +66 -0
  47. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +12 -3
  48. package/node_modules/playwright/lib/mcp/skyramp/common/liveReadError.js +40 -0
  49. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +21 -0
  50. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -1
  51. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +96 -16
  52. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +10 -0
  53. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  54. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +22 -0
  55. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/elementOrder.js +57 -0
  56. package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
  57. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.uiPTFncY.js → index.0MJW0uG7.js} +1 -1
  58. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  59. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  60. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  61. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +26 -1
  62. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/elementOrder.ts +83 -0
  63. package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
  64. package/node_modules/playwright/package.json +1 -1
  65. package/package.json +4 -2
  66. package/plugin/prompts/plan-tests.md +9 -17
  67. package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -1
  68. package/plugin/skills/enhance-assertions/reference/ui.md +2 -2
  69. package/plugin/skills/fix-test-import-errors/SKILL.md +1 -1
@@ -45,6 +45,8 @@ var import_log = require("../log");
45
45
  var import_skyRampExport = require("../test/skyRampExport");
46
46
  var import_exportTool = require("./exportTool");
47
47
  var import_assertTool = require("./assertTool");
48
+ var import_assertOrder = require("./assertOrder");
49
+ var import_liveReadError = require("./common/liveReadError");
48
50
  var import_assertHiddenTool = require("./assertHiddenTool");
49
51
  var import_assertApiRequestTool = require("./assertApiRequestTool");
50
52
  var import_loadTraceTool = require("./loadTraceTool");
@@ -246,7 +248,9 @@ class TraceRecordingBackend {
246
248
  }
247
249
  if (name === import_assertTool.assertToolSchema.name) {
248
250
  const parsed = import_assertTool.assertToolSchema.inputSchema.parse(args || {});
249
- return this._handleAssert(parsed);
251
+ if (parsed.type === "order")
252
+ return this._handleAssertOrder(parsed);
253
+ return this._handleAssert({ ...parsed, type: parsed.type });
250
254
  }
251
255
  if (name === import_assertHiddenTool.assertHiddenToolSchema.name) {
252
256
  const parsed = import_assertHiddenTool.assertHiddenToolSchema.inputSchema.parse(args || {});
@@ -933,6 +937,14 @@ Continue recording with browser_* tools, then call skyramp_export_zip to write t
933
937
  return null;
934
938
  return { toolName: "browser_assert", code: `assertAttribute:${a.selector}:${a.attrName.trim()}:${a.attrValue}`, args: { type: "attribute", selector: a.selector, property: a.attrName.trim(), expected: a.attrValue } };
935
939
  }
940
+ // SKYR-4544: an order assertion needs both ends and its relation; a line
941
+ // missing one is dropped rather than seeded with a hole.
942
+ case "assertOrder": {
943
+ const fields = (0, import_utils.parseOrderFields)(a);
944
+ if (!fields)
945
+ return null;
946
+ return { toolName: "browser_assert", code: `assertOrder:${fields.selector}:${fields.relation}:${fields.otherSelector}`, args: { type: "order", ...fields } };
947
+ }
936
948
  case "assertCount":
937
949
  if (typeof a.count !== "number" || !Number.isInteger(a.count) || a.count < 0)
938
950
  return null;
@@ -1340,8 +1352,9 @@ Linked GoJS nodes ${params.fromKey} \u2192 ${params.toKey} (${params.fromPort}\u
1340
1352
  * proper Playwright selector (testid > role > text), then verify
1341
1353
  * text/value/checked/disabled/visible/count against the snapshot data, and
1342
1354
  * css/class/attribute against the live element (getComputedStyle / classList
1343
- * / getAttribute, read with the pointer moved off the element). Only passing
1344
- * assertions are recorded.
1355
+ * / getAttribute, read with the pointer moved off the element). "order" is
1356
+ * dispatched to _handleAssertOrder instead: two refs, a live boundingBox()
1357
+ * comparison, no pointer-off read. Only passing assertions are recorded.
1345
1358
  */
1346
1359
  async _handleAssert(params) {
1347
1360
  const timestamp = Date.now();
@@ -1471,15 +1484,7 @@ Could not read the un-hovered state of "${params.element}": the pointer could no
1471
1484
  details = passed ? `Attribute assertion passed: "${params.element}" has ${name}="${actual}".` : `Attribute assertion FAILED: "${params.element}" has ${name}=${actual === null ? "(absent)" : `"${actual}"`}, expected "${params.expected}".`;
1472
1485
  }
1473
1486
  } catch (e) {
1474
- const msg = String(e?.message ?? e);
1475
- if (/strict mode violation/i.test(msg))
1476
- return { content: [{ type: "text", text: `### Error
1477
- Target ${parsed.selector} matches multiple elements, so the ${kind} read is ambiguous${unique.pinFailure ? `, and ${unique.pinFailure}` : ""}. Use a more specific target (a data-testid, or role + exact name). ${msg.split("\n")[0]}` }], isError: true };
1478
- if (e?.name === "TimeoutError")
1479
- return { content: [{ type: "text", text: `### Error
1480
- "${params.element}" (${parsed.selector}) did not resolve within ${LIVE_READ_TIMEOUT_MS} ms. Re-snapshot and retry with a fresh ref.` }], isError: true };
1481
- return { content: [{ type: "text", text: `### Error
1482
- Could not read ${kind} of "${params.element}": ${msg}` }], isError: true };
1487
+ return (0, import_liveReadError.liveReadError)(e, { selector: parsed.selector, element: params.element, ambiguity: `the ${kind} read is ambiguous`, failedTo: `read ${kind} of`, pinFailure: unique.pinFailure, timeoutMs: LIVE_READ_TIMEOUT_MS });
1483
1488
  }
1484
1489
  if (passed)
1485
1490
  details += unique.note;
@@ -1624,6 +1629,82 @@ Could not count "${refRole}" elements: ${e.message}` }], isError: true };
1624
1629
  ${details}` }]
1625
1630
  };
1626
1631
  }
1632
+ /**
1633
+ * browser_assert type "order" (SKYR-4544): the ref's element sits left of /
1634
+ * right of / above / below the other ref's element. A swap or a moved block
1635
+ * is a relation between two elements, which no single-element kind can
1636
+ * express, so both ends are resolved through the same durable-locator path
1637
+ * as the other kinds and their live boxes compared on the relation's axis.
1638
+ * The comparison is the one the generated test runs: each boundingBox()'s
1639
+ * origin — the left edge (x) for left-of/right-of, the top edge (y) for
1640
+ * above/below — compared strictly. It says where each element STARTS, not
1641
+ * that one lies wholly beyond the other.
1642
+ */
1643
+ async _handleAssertOrder(params) {
1644
+ const timestamp = Date.now();
1645
+ const error = (text) => ({ content: [{ type: "text", text: `### Error
1646
+ ${text}` }], isError: true });
1647
+ const failed = (text) => ({ content: [{ type: "text", text: `### Assertion Failed
1648
+ ${text}` }], isError: true });
1649
+ const checked = (0, import_assertOrder.checkOrderArgs)(params);
1650
+ if ("error" in checked)
1651
+ return error(checked.error);
1652
+ const { relation, otherElement } = checked;
1653
+ const tab = this._browserBackend.context?.currentTab();
1654
+ if (!tab)
1655
+ return failed("No active page.");
1656
+ const notReady = await this._requireLoadedPage(tab, "domcontentloaded");
1657
+ if (notReady)
1658
+ return notReady;
1659
+ const ends = [];
1660
+ for (const [ref, element] of [[params.ref, params.element], [params.otherRef, otherElement]]) {
1661
+ const resolved = await this._resolveRef(ref, element);
1662
+ if ("error" in resolved)
1663
+ return failed(resolved.error);
1664
+ const lineText = (0, import_snapshotLocators.snapshotLineText)(resolved.refLine) ?? "";
1665
+ const unique = await this._makeUnique(resolved.parsed, { snapLines: resolved.snapLines, ref, element, lineText, exact: false });
1666
+ if (unique.refusal)
1667
+ return unique.refusal;
1668
+ ends.push({ parsed: unique.parsed, note: unique.note, pinFailure: unique.pinFailure, element, selector: unique.parsed.selector });
1669
+ }
1670
+ const [a, b] = ends;
1671
+ if (a.selector === b.selector)
1672
+ return error(`"${a.element}" and "${b.element}" resolve to the same locator (${a.selector}), so their order cannot be asserted. Use refs of two different elements.`);
1673
+ const boxes = [];
1674
+ for (const end of ends) {
1675
+ let box;
1676
+ try {
1677
+ box = await tab.page.locator(end.selector).boundingBox({ timeout: LIVE_READ_TIMEOUT_MS });
1678
+ } catch (e) {
1679
+ return (0, import_liveReadError.liveReadError)(e, { selector: end.selector, element: end.element, ambiguity: "its position is ambiguous", failedTo: "read the position of", pinFailure: end.pinFailure, timeoutMs: LIVE_READ_TIMEOUT_MS });
1680
+ }
1681
+ if (!box)
1682
+ return failed(`"${end.element}" (${end.selector}) has no box on the page (it is not visible), so its position cannot be compared.`);
1683
+ boxes.push(box);
1684
+ }
1685
+ const [boxA, boxB] = boxes;
1686
+ const nested = (0, import_assertOrder.containmentError)(a, b, boxA, boxB);
1687
+ if (nested)
1688
+ return error(nested);
1689
+ const { result, details } = (0, import_assertOrder.describeOrderResult)(relation, a, b, boxA, boxB);
1690
+ if (!result.ok)
1691
+ return { content: [{ type: "text", text: `### Assertion Failed
1692
+ ${details}` }] };
1693
+ const args = {
1694
+ type: "order",
1695
+ ref: params.ref,
1696
+ selector: a.selector,
1697
+ locator: a.parsed.locator,
1698
+ otherRef: params.otherRef,
1699
+ otherSelector: b.selector,
1700
+ otherLocator: b.parsed.locator,
1701
+ relation
1702
+ };
1703
+ this._trackedActions.push({ toolName: "browser_assert", args, code: `assertOrder:${a.selector}:${relation}:${b.selector}`, timestamp, pageAlias: this._currentPageAlias });
1704
+ traceDebug(`Assert: assertOrder ${a.selector} ${relation} ${b.selector}`);
1705
+ return { content: [{ type: "text", text: `### Assertion Passed
1706
+ ${details}${a.note}${b.note}` }] };
1707
+ }
1627
1708
  /**
1628
1709
  * Handle browser_assert_table_cell by resolving the cell from (row, column),
1629
1710
  * reading its value directly from the page, and recording an assertTableCell
@@ -1838,10 +1919,9 @@ ${text}` }], isError: true });
1838
1919
  try {
1839
1920
  await tab.page.locator(selector).waitFor({ state: "hidden", timeout: TraceRecordingBackend.HIDDEN_WAIT_TIMEOUT_MS });
1840
1921
  } catch (e) {
1841
- if (/strict mode violation/i.test(String(e?.message ?? "")))
1842
- return err(`Target ${selector} matches multiple elements, so toBeHidden() would fail strict mode. Use a more specific target (a data-testid, or role + exact name). ${String(e.message).split("\n")[0]}`);
1843
- if (e?.name !== "TimeoutError")
1844
- return err(`Could not verify absence of "${params.element}": ${e?.message ?? e}`);
1922
+ const error = (0, import_liveReadError.liveReadError)(e, { selector, element: params.element, ambiguity: "toBeHidden() would fail strict mode", failedTo: "verify absence of" });
1923
+ if (error)
1924
+ return error;
1845
1925
  passed = false;
1846
1926
  }
1847
1927
  let details;
@@ -509,6 +509,16 @@ function assertActionToJsonl(action, pageGuid, timestamp) {
509
509
  return null;
510
510
  return JSON.stringify({ name: "assertAttribute", selector, attrName, attrValue: expected, locator, ...base });
511
511
  }
512
+ // SKYR-4544: where the element sits relative to another one — a
513
+ // boundingBox() comparison downstream. Both ends and the relation are
514
+ // needed; a line missing any of them is dropped (reported as skipped).
515
+ case "order": {
516
+ const fields = (0, import_utils.parseOrderFields)({ ...args, selector });
517
+ if (!fields)
518
+ return null;
519
+ const otherLocator = fields.otherLocator ?? selectorToLocator(fields.otherSelector);
520
+ return JSON.stringify({ name: "assertOrder", selector, otherSelector: fields.otherSelector, relation: fields.relation, locator, otherLocator, ...base });
521
+ }
512
522
  default:
513
523
  return null;
514
524
  }
@@ -137,6 +137,7 @@ const _ACTION_NAMES = /* @__PURE__ */ new Set([
137
137
  "assertCSS",
138
138
  "assertClass",
139
139
  "assertAttribute",
140
+ "assertOrder",
140
141
  "dragTo",
141
142
  "diagramNodeAdd",
142
143
  "diagramLinkAdd",
@@ -196,6 +196,28 @@ async function performActionImpl(progress, mainFrame, actionInContext) {
196
196
  });
197
197
  return;
198
198
  }
199
+ if (raw.name === "assertOrder") {
200
+ const fields = (0, import_utils.parseOrderFields)(raw);
201
+ if (!fields)
202
+ throw new Error(`assertOrder failed: trace line needs "otherSelector" and a relation of ${import_utils.ORDER_RELATIONS.join("/")}, got ${JSON.stringify(raw.relation)}`);
203
+ const otherSelector = (0, import_recorderUtils.buildFullSelector)(actionInContext.frame.framePath, fields.otherSelector);
204
+ const box = async (s) => {
205
+ const handle = await mainFrame.waitForSelector(progress, s, false, { strict: true, state: "visible" });
206
+ try {
207
+ const rect = await handle?.boundingBox();
208
+ if (!rect)
209
+ throw new Error(`assertOrder: ${s} has no box on the page`);
210
+ return rect;
211
+ } finally {
212
+ handle?.dispose();
213
+ }
214
+ };
215
+ const [a, b] = [await box(selector), await box(otherSelector)];
216
+ const { ok, axis, edgeA, edgeB } = (0, import_utils.orderHolds)(fields.relation, a, b);
217
+ if (!ok)
218
+ throw new Error(`assertOrder failed: expected ${selector} ${fields.relation} ${otherSelector}, got ${axis} ${edgeA} vs ${edgeB}`);
219
+ return;
220
+ }
199
221
  throw new Error("Internal error: unexpected action " + action.name);
200
222
  }
201
223
  function toClickOptions(action) {
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var elementOrder_exports = {};
20
+ __export(elementOrder_exports, {
21
+ ORDER_RELATIONS: () => ORDER_RELATIONS,
22
+ isOrderRelation: () => isOrderRelation,
23
+ orderHolds: () => orderHolds,
24
+ parseOrderFields: () => parseOrderFields
25
+ });
26
+ module.exports = __toCommonJS(elementOrder_exports);
27
+ const ORDER_RELATIONS = ["left-of", "right-of", "above", "below"];
28
+ function isOrderRelation(value) {
29
+ return typeof value === "string" && ORDER_RELATIONS.includes(value);
30
+ }
31
+ function orderHolds(relation, a, b) {
32
+ const axis = relation === "left-of" || relation === "right-of" ? "x" : "y";
33
+ const edgeA = a[axis];
34
+ const edgeB = b[axis];
35
+ const ok = relation === "left-of" || relation === "above" ? edgeA < edgeB : edgeA > edgeB;
36
+ return { ok, axis, edgeA, edgeB };
37
+ }
38
+ function parseOrderFields(raw) {
39
+ if (!raw || typeof raw !== "object")
40
+ return null;
41
+ const { selector, otherSelector, relation, locator, otherLocator } = raw;
42
+ if (typeof selector !== "string" || !selector || typeof otherSelector !== "string" || !otherSelector || !isOrderRelation(relation))
43
+ return null;
44
+ const fields = { selector, otherSelector, relation };
45
+ if (locator && typeof locator === "object")
46
+ fields.locator = locator;
47
+ if (otherLocator && typeof otherLocator === "object")
48
+ fields.otherLocator = otherLocator;
49
+ return fields;
50
+ }
51
+ // Annotate the CommonJS export names for ESM import in node:
52
+ 0 && (module.exports = {
53
+ ORDER_RELATIONS,
54
+ isOrderRelation,
55
+ orderHolds,
56
+ parseOrderFields
57
+ });
@@ -25,6 +25,7 @@ module.exports = __toCommonJS(utils_exports);
25
25
  __reExport(utils_exports, require("./utils/isomorphic/ariaSnapshot"), module.exports);
26
26
  __reExport(utils_exports, require("./utils/isomorphic/assert"), module.exports);
27
27
  __reExport(utils_exports, require("./utils/isomorphic/colors"), module.exports);
28
+ __reExport(utils_exports, require("./utils/isomorphic/elementOrder"), module.exports);
28
29
  __reExport(utils_exports, require("./utils/isomorphic/headers"), module.exports);
29
30
  __reExport(utils_exports, require("./utils/isomorphic/locatorGenerators"), module.exports);
30
31
  __reExport(utils_exports, require("./utils/isomorphic/locatorParser"), module.exports);
@@ -72,6 +73,7 @@ var import_utilsBundle = require("./utilsBundle");
72
73
  ...require("./utils/isomorphic/ariaSnapshot"),
73
74
  ...require("./utils/isomorphic/assert"),
74
75
  ...require("./utils/isomorphic/colors"),
76
+ ...require("./utils/isomorphic/elementOrder"),
75
77
  ...require("./utils/isomorphic/headers"),
76
78
  ...require("./utils/isomorphic/locatorGenerators"),
77
79
  ...require("./utils/isomorphic/locatorParser"),
@@ -1,2 +1,2 @@
1
- import{T as x,r,a as C,W as F,j as e,D as z,b as I,c as P,d as A,e as O,f as V}from"./assets/defaultSettingsView-DTp2-8Si.js";const $=()=>{const[o,c]=r.useState(!1),[n,l]=r.useState(),[h,u]=r.useState(),[p,b]=r.useState(N),[f,j]=r.useState({done:0,total:0}),[k,v]=r.useState(!1),[y,m]=r.useState(null),[T,R]=r.useState(null),[U,E]=r.useState(!1),g=r.useCallback(t=>{const s=new URL(window.location.href);if(!t.length)return;const i=t.item(0),a=URL.createObjectURL(i);s.searchParams.append("trace",a);const d=s.toString();window.history.pushState({},"",d),l(a),u(i.name),v(!1),m(null)},[]);r.useEffect(()=>{const t=async s=>{var a;if(!((a=s.clipboardData)!=null&&a.files.length))return;const i=["application/zip","application/x-zip-compressed"];for(const d of s.clipboardData.files)if(!i.includes(d.type))return;s.preventDefault(),g(s.clipboardData.files)};return document.addEventListener("paste",t),()=>document.removeEventListener("paste",t)}),r.useEffect(()=>{const t=s=>{const{method:i,params:a}=s.data;if(i!=="load"||!((a==null?void 0:a.trace)instanceof Blob))return;const d=new File([a.trace],"trace.zip",{type:"application/zip"}),w=new DataTransfer;w.items.add(d),g(w.files)};return window.addEventListener("message",t),()=>window.removeEventListener("message",t)});const W=r.useCallback(t=>{t.preventDefault(),g(t.dataTransfer.files)},[g]),M=r.useCallback(t=>{t.preventDefault(),t.target.files&&g(t.target.files)},[g]);r.useEffect(()=>{const t=new URL(window.location.href).searchParams,s=t.get("trace");if(c(t.has("isServer")),s!=null&&s.startsWith("file:")){R(s||null);return}if(t.has("isServer")){const i=new URLSearchParams(window.location.search).get("ws"),a=new URL(`../${i}`,window.location.toString());a.protocol=window.location.protocol==="https:"?"wss:":"ws:";const d=new C(new F(a));d.onLoadTraceRequested(async w=>{l(w.traceUrl),v(!1),m(null)}),d.initialize({}).catch(()=>{})}else s&&!s.startsWith("blob:")&&l(s)},[]);const S=r.useCallback(async t=>{const s=new URLSearchParams;s.set("trace",t);const i=await fetch(`contexts?${s.toString()}`);if(!i.ok){const{error:w}=await i.json();return m(w),w}const a=await i.json(),d=new x(t,a);j({done:0,total:0}),m(null),b(d)},[]);r.useEffect(()=>{(async()=>{if(!n){b(N);return}const t=s=>{s.data.method==="progress"&&j(s.data.params)};try{navigator.serviceWorker.addEventListener("message",t),j({done:0,total:1});let s=await S(n);s!=null&&s.includes("please grant permission for Local Network Access")&&(await fetch(n,{method:"HEAD",headers:{"x-pw-serviceworker":"skip"}}),s=await S(n)),s&&(o||l(void 0))}finally{navigator.serviceWorker.removeEventListener("message",t)}})()},[o,n,h,S]);const D=f.done!==f.total&&f.total!==0&&!y;r.useEffect(()=>{if(D){const t=setTimeout(()=>{E(!0)},200);return()=>clearTimeout(t)}else E(!1)},[D]);const L=!!(!o&&!k&&!T&&(!n||y));return e.jsxs("div",{className:"vbox workbench-loader",onDragOver:t=>{t.preventDefault(),t.dataTransfer.types.includes("Files")&&v(!0)},children:[e.jsxs("div",{className:"hbox header",...L?{inert:!0}:{},children:[e.jsx("div",{className:"logo",children:e.jsx("img",{src:"playwright-logo.svg",alt:"Playwright logo"})}),e.jsx("div",{className:"product",children:"Playwright"}),p.title&&e.jsx("div",{className:"title",children:p.title}),e.jsx("div",{className:"spacer"}),e.jsx(z,{icon:"settings-gear",title:"Settings",dialogDataTestId:"settings-toolbar-dialog",children:e.jsx(I,{location:"trace-viewer"})})]}),e.jsx(P,{model:p,inert:L}),T&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{children:"Trace Viewer uses Service Workers to show traces. To view trace:"}),e.jsxs("div",{style:{paddingTop:20},children:[e.jsxs("div",{children:["1. Click ",e.jsx("a",{href:T,children:"here"})," to put your trace into the download shelf"]}),e.jsxs("div",{children:["2. Go to ",e.jsx("a",{href:"https://trace.playwright.dev",children:"trace.playwright.dev"})]}),e.jsx("div",{children:"3. Drop the trace from the download shelf into the page"})]})]}),e.jsx(A,{open:U,isModal:!0,className:"progress-dialog",children:e.jsxs("div",{className:"progress-content",children:[e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Loading Playwright Trace..."}),e.jsx("div",{className:"progress-wrapper",children:e.jsx("div",{className:"inner-progress",style:{width:f.total?100*f.done/f.total+"%":0}})})]})}),L&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{className:"processing-error",role:"alert",children:y}),e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Drop Playwright Trace to load"}),e.jsx("div",{children:"or"}),e.jsx("button",{onClick:()=>{const t=document.createElement("input");t.type="file",t.click(),t.addEventListener("change",s=>M(s))},type:"button",children:"Select file"}),e.jsx("div",{className:"info",children:"Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere, it opens it locally."}),e.jsxs("div",{className:"version",children:["Playwright v","1.58.2-skyramp.8.14.13"]})]}),o&&!n&&e.jsx("div",{className:"drop-target",children:e.jsx("div",{className:"title",children:"Select test to see the trace"})}),k&&e.jsx("div",{className:"drop-target",onDragLeave:()=>{v(!1)},onDrop:t=>W(t),children:e.jsx("div",{className:"title",children:"Release to analyse the Playwright Trace"})})]})},N=new x("",[]),q=({traceJson:o})=>{const[c,n]=r.useState(void 0),[l,h]=r.useState(0),u=r.useRef(null);return r.useEffect(()=>(u.current&&clearTimeout(u.current),u.current=setTimeout(async()=>{try{const p=await B(o);n(p)}catch{const p=new x("",[]);n(p)}finally{h(l+1)}},500),()=>{u.current&&clearTimeout(u.current)}),[o,l]),e.jsx(P,{isLive:!0,model:c})};async function B(o){const c=new URLSearchParams;c.set("trace",o);const l=await(await fetch(`contexts?${c.toString()}`)).json();return new x(o,l)}(async()=>{const o=new URLSearchParams(window.location.search);if(O(),window.location.protocol!=="file:"){if(o.get("isUnderTest")==="true"&&await new Promise(h=>setTimeout(h,1e3)),!navigator.serviceWorker)throw new Error(`Service workers are not supported.
1
+ import{T as x,r,a as C,W as F,j as e,D as z,b as I,c as P,d as A,e as O,f as V}from"./assets/defaultSettingsView-DTp2-8Si.js";const $=()=>{const[o,c]=r.useState(!1),[n,l]=r.useState(),[h,u]=r.useState(),[p,b]=r.useState(N),[f,j]=r.useState({done:0,total:0}),[k,v]=r.useState(!1),[y,m]=r.useState(null),[T,R]=r.useState(null),[U,E]=r.useState(!1),g=r.useCallback(t=>{const s=new URL(window.location.href);if(!t.length)return;const i=t.item(0),a=URL.createObjectURL(i);s.searchParams.append("trace",a);const d=s.toString();window.history.pushState({},"",d),l(a),u(i.name),v(!1),m(null)},[]);r.useEffect(()=>{const t=async s=>{var a;if(!((a=s.clipboardData)!=null&&a.files.length))return;const i=["application/zip","application/x-zip-compressed"];for(const d of s.clipboardData.files)if(!i.includes(d.type))return;s.preventDefault(),g(s.clipboardData.files)};return document.addEventListener("paste",t),()=>document.removeEventListener("paste",t)}),r.useEffect(()=>{const t=s=>{const{method:i,params:a}=s.data;if(i!=="load"||!((a==null?void 0:a.trace)instanceof Blob))return;const d=new File([a.trace],"trace.zip",{type:"application/zip"}),w=new DataTransfer;w.items.add(d),g(w.files)};return window.addEventListener("message",t),()=>window.removeEventListener("message",t)});const W=r.useCallback(t=>{t.preventDefault(),g(t.dataTransfer.files)},[g]),M=r.useCallback(t=>{t.preventDefault(),t.target.files&&g(t.target.files)},[g]);r.useEffect(()=>{const t=new URL(window.location.href).searchParams,s=t.get("trace");if(c(t.has("isServer")),s!=null&&s.startsWith("file:")){R(s||null);return}if(t.has("isServer")){const i=new URLSearchParams(window.location.search).get("ws"),a=new URL(`../${i}`,window.location.toString());a.protocol=window.location.protocol==="https:"?"wss:":"ws:";const d=new C(new F(a));d.onLoadTraceRequested(async w=>{l(w.traceUrl),v(!1),m(null)}),d.initialize({}).catch(()=>{})}else s&&!s.startsWith("blob:")&&l(s)},[]);const S=r.useCallback(async t=>{const s=new URLSearchParams;s.set("trace",t);const i=await fetch(`contexts?${s.toString()}`);if(!i.ok){const{error:w}=await i.json();return m(w),w}const a=await i.json(),d=new x(t,a);j({done:0,total:0}),m(null),b(d)},[]);r.useEffect(()=>{(async()=>{if(!n){b(N);return}const t=s=>{s.data.method==="progress"&&j(s.data.params)};try{navigator.serviceWorker.addEventListener("message",t),j({done:0,total:1});let s=await S(n);s!=null&&s.includes("please grant permission for Local Network Access")&&(await fetch(n,{method:"HEAD",headers:{"x-pw-serviceworker":"skip"}}),s=await S(n)),s&&(o||l(void 0))}finally{navigator.serviceWorker.removeEventListener("message",t)}})()},[o,n,h,S]);const D=f.done!==f.total&&f.total!==0&&!y;r.useEffect(()=>{if(D){const t=setTimeout(()=>{E(!0)},200);return()=>clearTimeout(t)}else E(!1)},[D]);const L=!!(!o&&!k&&!T&&(!n||y));return e.jsxs("div",{className:"vbox workbench-loader",onDragOver:t=>{t.preventDefault(),t.dataTransfer.types.includes("Files")&&v(!0)},children:[e.jsxs("div",{className:"hbox header",...L?{inert:!0}:{},children:[e.jsx("div",{className:"logo",children:e.jsx("img",{src:"playwright-logo.svg",alt:"Playwright logo"})}),e.jsx("div",{className:"product",children:"Playwright"}),p.title&&e.jsx("div",{className:"title",children:p.title}),e.jsx("div",{className:"spacer"}),e.jsx(z,{icon:"settings-gear",title:"Settings",dialogDataTestId:"settings-toolbar-dialog",children:e.jsx(I,{location:"trace-viewer"})})]}),e.jsx(P,{model:p,inert:L}),T&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{children:"Trace Viewer uses Service Workers to show traces. To view trace:"}),e.jsxs("div",{style:{paddingTop:20},children:[e.jsxs("div",{children:["1. Click ",e.jsx("a",{href:T,children:"here"})," to put your trace into the download shelf"]}),e.jsxs("div",{children:["2. Go to ",e.jsx("a",{href:"https://trace.playwright.dev",children:"trace.playwright.dev"})]}),e.jsx("div",{children:"3. Drop the trace from the download shelf into the page"})]})]}),e.jsx(A,{open:U,isModal:!0,className:"progress-dialog",children:e.jsxs("div",{className:"progress-content",children:[e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Loading Playwright Trace..."}),e.jsx("div",{className:"progress-wrapper",children:e.jsx("div",{className:"inner-progress",style:{width:f.total?100*f.done/f.total+"%":0}})})]})}),L&&e.jsxs("div",{className:"drop-target",children:[e.jsx("div",{className:"processing-error",role:"alert",children:y}),e.jsx("div",{className:"title",role:"heading","aria-level":1,children:"Drop Playwright Trace to load"}),e.jsx("div",{children:"or"}),e.jsx("button",{onClick:()=>{const t=document.createElement("input");t.type="file",t.click(),t.addEventListener("change",s=>M(s))},type:"button",children:"Select file"}),e.jsx("div",{className:"info",children:"Playwright Trace Viewer is a Progressive Web App, it does not send your trace anywhere, it opens it locally."}),e.jsxs("div",{className:"version",children:["Playwright v","1.58.2-skyramp.8.14.14"]})]}),o&&!n&&e.jsx("div",{className:"drop-target",children:e.jsx("div",{className:"title",children:"Select test to see the trace"})}),k&&e.jsx("div",{className:"drop-target",onDragLeave:()=>{v(!1)},onDrop:t=>W(t),children:e.jsx("div",{className:"title",children:"Release to analyse the Playwright Trace"})})]})},N=new x("",[]),q=({traceJson:o})=>{const[c,n]=r.useState(void 0),[l,h]=r.useState(0),u=r.useRef(null);return r.useEffect(()=>(u.current&&clearTimeout(u.current),u.current=setTimeout(async()=>{try{const p=await B(o);n(p)}catch{const p=new x("",[]);n(p)}finally{h(l+1)}},500),()=>{u.current&&clearTimeout(u.current)}),[o,l]),e.jsx(P,{isLive:!0,model:c})};async function B(o){const c=new URLSearchParams;c.set("trace",o);const l=await(await fetch(`contexts?${c.toString()}`)).json();return new x(o,l)}(async()=>{const o=new URLSearchParams(window.location.search);if(O(),window.location.protocol!=="file:"){if(o.get("isUnderTest")==="true"&&await new Promise(h=>setTimeout(h,1e3)),!navigator.serviceWorker)throw new Error(`Service workers are not supported.
2
2
  Make sure to serve the Trace Viewer (${window.location}) via HTTPS or localhost.`);navigator.serviceWorker.register("sw.bundle.js"),navigator.serviceWorker.controller||await new Promise(h=>{navigator.serviceWorker.oncontrollerchange=()=>h()}),setInterval(function(){fetch("ping")},1e4)}const c=o.get("trace"),l=(c==null?void 0:c.endsWith(".json"))?e.jsx(q,{traceJson:c}):e.jsx($,{});V.createRoot(document.querySelector("#root")).render(l)})();
@@ -7,7 +7,7 @@
7
7
  <link rel="icon" href="./playwright-logo.svg" type="image/svg+xml">
8
8
  <link rel="manifest" href="./manifest.webmanifest">
9
9
  <title>Playwright Trace Viewer</title>
10
- <script type="module" crossorigin src="./index.uiPTFncY.js"></script>
10
+ <script type="module" crossorigin src="./index.0MJW0uG7.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/defaultSettingsView-DTp2-8Si.js">
12
12
  <link rel="stylesheet" crossorigin href="./defaultSettingsView.7ch9cixO.css">
13
13
  <link rel="stylesheet" crossorigin href="./index.BVu7tZDe.css">
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "playwright-core",
3
- "version": "1.58.2-skyramp.8.14.13",
3
+ "version": "1.58.2-skyramp.8.14.14",
4
4
  "description": "A high-level API to automate web browsers",
5
5
  "repository": {
6
6
  "type": "git",
@@ -118,7 +118,7 @@ const _ACTION_NAMES = new Set<string>([
118
118
  'navigate', 'openPage', 'press', 'select', 'uncheck',
119
119
  'setInputFiles', 'assertText', 'assertValue', 'assertChecked',
120
120
  'assertVisible', 'assertDisabled', 'assertCount', 'assertHidden', 'assertSnapshot', 'visualSnapshot', 'assertTableCell',
121
- 'assertCSS', 'assertClass', 'assertAttribute',
121
+ 'assertCSS', 'assertClass', 'assertAttribute', 'assertOrder',
122
122
  'dragTo', 'diagramNodeAdd', 'diagramLinkAdd', 'selectArea', 'tableSnapshot', 'domSnapshot', 'fileChooser',
123
123
  'penTool', 'mouse.wheel', 'mouse.move', 'mouse.down', 'mouse.up',
124
124
  'modalOpen', 'modalClose', 'waitForTimeout', 'evaluate',
@@ -14,7 +14,7 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { escapeRegExp, serializeExpectedTextValues } from '../../utils';
17
+ import { escapeRegExp, ORDER_RELATIONS, orderHolds, parseOrderFields, serializeExpectedTextValues } from '../../utils';
18
18
  import { toKeyboardModifiers } from '../codegen/language';
19
19
  import { buildFullSelector, mainFrameForAction } from './recorderUtils';
20
20
  import { Progress, ProgressController } from '../progress';
@@ -240,6 +240,31 @@ async function performActionImpl(progress: Progress, mainFrame: Frame, actionInC
240
240
  return;
241
241
  }
242
242
 
243
+ if (raw.name === 'assertOrder') {
244
+ // SKYR-4544: the order contract (utils/isomorphic/elementOrder) the MCP
245
+ // recorder and replay use too; both ends must be visible first.
246
+ const fields = parseOrderFields(raw);
247
+ if (!fields)
248
+ throw new Error(`assertOrder failed: trace line needs "otherSelector" and a relation of ${ORDER_RELATIONS.join('/')}, got ${JSON.stringify(raw.relation)}`);
249
+ const otherSelector = buildFullSelector(actionInContext.frame.framePath, fields.otherSelector);
250
+ const box = async (s: string) => {
251
+ const handle = await mainFrame.waitForSelector(progress, s, false, { strict: true, state: 'visible' });
252
+ try {
253
+ const rect = await handle?.boundingBox();
254
+ if (!rect)
255
+ throw new Error(`assertOrder: ${s} has no box on the page`);
256
+ return rect;
257
+ } finally {
258
+ handle?.dispose();
259
+ }
260
+ };
261
+ const [a, b] = [await box(selector), await box(otherSelector)];
262
+ const { ok, axis, edgeA, edgeB } = orderHolds(fields.relation, a, b);
263
+ if (!ok)
264
+ throw new Error(`assertOrder failed: expected ${selector} ${fields.relation} ${otherSelector}, got ${axis} ${edgeA} vs ${edgeB}`);
265
+ return;
266
+ }
267
+
243
268
  throw new Error('Internal error: unexpected action ' + (action as any).name);
244
269
  }
245
270
 
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Copyright (c) Microsoft Corporation.
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ // SKYR-4544: the one definition of an order assertion ("A starts before B on an
18
+ // axis"). The MCP recorder, the MCP trace export and seeding, the MCP trace
19
+ // replay and the core recorder replay all use it, so record and replay cannot
20
+ // drift apart. (The generated test's comparison lives in the Go codegen, which
21
+ // cannot import this; its goldens and the live demoshop check pin it.)
22
+
23
+ /** Where the asserted element sits relative to the other one. */
24
+ export const ORDER_RELATIONS = ['left-of', 'right-of', 'above', 'below'] as const;
25
+ export type OrderRelation = typeof ORDER_RELATIONS[number];
26
+
27
+ export function isOrderRelation(value: unknown): value is OrderRelation {
28
+ return typeof value === 'string' && (ORDER_RELATIONS as readonly string[]).includes(value);
29
+ }
30
+
31
+ /** The part of a box the comparison reads: its origin. */
32
+ export type OrderBox = { x: number, y: number };
33
+
34
+ export type OrderResult = {
35
+ ok: boolean,
36
+ /** x for left-of/right-of, y for above/below. */
37
+ axis: 'x' | 'y',
38
+ edgeA: number,
39
+ edgeB: number,
40
+ };
41
+
42
+ /**
43
+ * Whether `a` is `relation` of `b`: each box's origin — the left edge (x) for
44
+ * left-of/right-of, the top edge (y) for above/below — compared strictly. It
45
+ * says where each element STARTS, not that one lies wholly beyond the other,
46
+ * and equal edges never hold.
47
+ */
48
+ export function orderHolds(relation: OrderRelation, a: OrderBox, b: OrderBox): OrderResult {
49
+ const axis = relation === 'left-of' || relation === 'right-of' ? 'x' : 'y';
50
+ const edgeA = a[axis];
51
+ const edgeB = b[axis];
52
+ const ok = relation === 'left-of' || relation === 'above' ? edgeA < edgeB : edgeA > edgeB;
53
+ return { ok, axis, edgeA, edgeB };
54
+ }
55
+
56
+ export type OrderFields = {
57
+ selector: string,
58
+ otherSelector: string,
59
+ relation: OrderRelation,
60
+ locator?: object,
61
+ otherLocator?: object,
62
+ };
63
+
64
+ /**
65
+ * The fields of an order assertion — a JSONL `assertOrder` line or a recorded
66
+ * browser_assert type "order" action's args — or null when it is malformed (no
67
+ * selector, no other end, or a relation outside ORDER_RELATIONS). Export, seeding
68
+ * and both replays parse through this, so they accept and drop exactly the same
69
+ * lines.
70
+ */
71
+ export function parseOrderFields(raw: any): OrderFields | null {
72
+ if (!raw || typeof raw !== 'object')
73
+ return null;
74
+ const { selector, otherSelector, relation, locator, otherLocator } = raw;
75
+ if (typeof selector !== 'string' || !selector || typeof otherSelector !== 'string' || !otherSelector || !isOrderRelation(relation))
76
+ return null;
77
+ const fields: OrderFields = { selector, otherSelector, relation };
78
+ if (locator && typeof locator === 'object')
79
+ fields.locator = locator;
80
+ if (otherLocator && typeof otherLocator === 'object')
81
+ fields.otherLocator = otherLocator;
82
+ return fields;
83
+ }
@@ -17,6 +17,7 @@
17
17
  export * from './utils/isomorphic/ariaSnapshot';
18
18
  export * from './utils/isomorphic/assert';
19
19
  export * from './utils/isomorphic/colors';
20
+ export * from './utils/isomorphic/elementOrder';
20
21
  export * from './utils/isomorphic/headers';
21
22
  export * from './utils/isomorphic/locatorGenerators';
22
23
  export * from './utils/isomorphic/locatorParser';
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/playwright",
3
- "version": "1.58.2-skyramp.8.14.13",
3
+ "version": "1.58.2-skyramp.8.14.14",
4
4
  "description": "Skyramp's fork of Playwright with trace recording for UI test generation",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyramp/mcp",
3
- "version": "0.4.3",
3
+ "version": "0.4.4-rc.1",
4
4
  "main": "build/index.js",
5
5
  "exports": {
6
6
  ".": "./build/index.js",
@@ -76,7 +76,8 @@
76
76
  "fast-glob": "^3.3.3",
77
77
  "js-yaml": "^4.1.1",
78
78
  "picomatch": "^4.0.5",
79
- "playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.14.13.tgz",
79
+ "playwright": "file:vendor/skyramp-playwright-1.58.2-skyramp.8.14.14.tgz",
80
+ "semver": "^7.8.5",
80
81
  "simple-git": "^3.30.0",
81
82
  "smol-toml": "^1.8.0",
82
83
  "ts-is-record": "^3.0.5",
@@ -90,6 +91,7 @@
90
91
  "@types/mocha": "^10.0.10",
91
92
  "@types/node": "^22.15.19",
92
93
  "@types/picomatch": "^4.0.3",
94
+ "@types/semver": "^7.8.0",
93
95
  "@typescript-eslint/eslint-plugin": "^8.0.0",
94
96
  "@typescript-eslint/parser": "^8.0.0",
95
97
  "eslint": "^9.0.0",
@@ -3,11 +3,11 @@
3
3
 
4
4
  ## Terms
5
5
 
6
- You decide what to test. The tools give you the diff and the existing tests; they do not choose tests for you.
6
+ You decide what to test.
7
7
 
8
8
  A **change** is one thing the pull request should make happen, or reject, for a user or a caller. Say on each change whether it is on the API, on a page, or both.
9
9
 
10
- A **behaviour** is one rule about one endpoint or one page. One change can carry two behaviours, one on the API and one on the page. A test may prove several behaviours. Every behaviour has at least one test that proves it. Rejections the application already handles share one test, one test case each; a test case a defect breaks gets its own test that expects to fail. Declare on each change the inputs that prove it: what the route must accept and what it must reject.
10
+ A **behaviour** is one rule about one endpoint or one page. One change can carry two behaviours, one on the API and one on the page. A test may prove several behaviours. Every behaviour has at least one test that proves it. Rejections the application already handles share one test, one test case each.
11
11
 
12
12
  ## The procedure
13
13
 
@@ -18,7 +18,7 @@ Do the steps in this order.
18
18
  3. **Write the changes.** List every change, one line each, with a short id and where you read it. Write the list in your reply before you go on.
19
19
  Keep each line through the rest of the procedure. Do not fold a change into another one or drop it because no test covers it.
20
20
  For each edit in the diff, write one line: what it makes different for a user or a caller.
21
- For each requirement the title, the description or a requirements file states, write one line, even when no edit in the diff serves it. If the requirement is an item of a numbered or bulleted list, put its number in the line's `source`: `spec:<path>#11` or `pr-description#3`.
21
+ For each requirement the title, the description or a requirements file states, write one line, even when no edit in the diff serves it.
22
22
  4. **Declare the test cases each request must refuse.**
23
23
  - For each input a request takes, declare one test case with a value the request must reject. Declare it even when the code does not reject that value today.
24
24
  - For each list a request takes, declare one test case that is valid except for one item in the list. Give the test case one `param` and one `value`: the invalid item. The step that sends the test case carries the rest of the list.
@@ -27,33 +27,25 @@ Do the steps in this order.
27
27
  - For each state a record can be in that the request must refuse, declare one test case that puts the record in that state first and then sends the request.
28
28
  - A request that names a record by id, in the path, in a query parameter, or in the body, must reject a record that does not exist or does not belong to the parent.
29
29
  - A record that belongs to another user is its own test case, even when the answer is the same.
30
- - For each rule the pull request states, declare the test cases that catch a wrong implementation: each bound, one value past each bound, one value far past a bound, and each value the rule forbids. Take their values from the rule, not from an example.
30
+ - For each rule the pull request states, declare the test cases that catch a wrong implementation: each bound, one value past each bound, one value far past a bound, and each value the rule forbids. Take their values from the rule, not from an example. If an input accepts only values from a list, declare an accept test case for every value in it, even when the values share code or make the test long.
31
31
  - A reject test case expects the request to be refused and the record to be unchanged. Declare an exact status code or error text only when the pull request or a requirements file states it.
32
32
  5. **Declare what each response value must be.**
33
33
  - For each value the response carries that this change decides, declare what that value must be and where you read it: the pull request title or description, a requirements file the description names, or a rule the application already follows in code this pull request did not change.
34
34
  - Quote a value only when it holds for every input the test case can send: a fixed constant, a bound, or one of the allowed names. A value the inputs decide is `derived`, even when the source shows it in an example.
35
- - If no source states the value, declare none and assert its shape instead.
36
35
  - Never take the value from the running application: that is the one source a defect cannot contradict.
37
36
  6. **Find where each change is served.** Read the code that declares routes and pages, and write each route's full path with its mount prefix. Follow the callers of a changed file that has no route of its own to the endpoint or page that runs it. Read the code that writes each stored value the new code reads; any rule on that value is a rejection to list.
38
- 7. **Review the code that serves each change.** Read the handler and the functions it calls to read or write data. For a changed screen, read the component and what it calls. Read these files even when the diff does not contain them; a defect often sits in the code the change depends on. Write each defect as one line with an id, the file, the line, and what is wrong. Write the list in your reply before you go on. An empty list is a statement: you read the code and found nothing. Common patterns to flag:
39
- - A computed field that is not recalculated after a mutation, such as `total_amount` unchanged after items are added or removed
40
- - Incomplete CRUD: a create with no cleanup, an update that adds new records and does not remove the old ones
41
- - Missing input validation on a new endpoint
42
- - A frontend rendering error visible in the code, such as an invalid prop, a missing required attribute, or a value shown without the formatting its neighbours use
43
- - Incorrect arithmetic in business logic, such as a discount calculation or a price aggregation
37
+ 7. **Review the code that serves each change.** Read the handler and the functions it calls to read or write data. For a changed screen, read the component and what it calls. Read these files even when the diff does not contain them; a defect often sits in the code the change depends on. Write each defect as one line with an id, the file, the line, and what is wrong. Write the list in your reply before you go on. An empty list is a statement: you read the code and found nothing.
44
38
  8. **Read the existing tests that reach a change.** The analysis lists the repository's test files. Read the ones that import a changed file, call a changed route, or open a changed page.
45
39
  9. **Group the changes into behaviours and decide each.** A planned test names every change it covers. A change gets no test only when a test already in the repository proves it; the size of the edit or the kind of file is never a reason. A change no behaviour needs gets one sentence that says why.
46
- When the test that proves a change is one you are editing, declare that test in `maintains` and name the changes it covers. Those changes are then covered and get no planned test of their own; a second test beside the one you edited is the mistake. A plan whose only coverage is maintenance is a complete plan.
47
- A requirement the title, the description or a named requirements file states and the code does not meet is a defect; its file is the code that contradicts it. A defect gets a planned test that names it in `defects` and expects to fail. A defect in the code the change touches is a defect whether or not this pull request introduced it, and it gets the same test. A defect gets no test only when something stopped the test: say what.
40
+ When the test that proves a change is one you are editing, or one you will run unchanged as a VERIFY, declare that test in `maintains` and name the changes it covers. Those changes are then covered and get no planned test of their own.
41
+ A requirement the title, the description or a named requirements file states and the code does not meet is a defect; its file is the code that contradicts it. A defect gets a planned test that names it in `defects` and expects to fail. If a test you ran unchanged as a VERIFY failed on the defect, name the defect in that test's `maintains` entry instead; the defect then needs no planned test. A defect gets no test only when something stopped the test: say what.
48
42
  If a state comes from time passing, create the record with the nearest allowed deadline and wait for it.
49
43
  10. **Walk each page behaviour once, as its test will run.** Start at login, do the steps of the behaviour, run `browser_blueprint` on each page you use, and export the trace when the flow ends. Name only elements from those captures. Do not try other values or read messages; the recording is the test.
50
- 11. **Declare each planned test.** The tool schema says what goes in each field, `defects` included.
44
+ 11. **Declare each planned test.**
51
45
  - A test that expects a rejection sends an accepted request first. That request is the test's setup.
52
- - Two planned tests on one endpoint each say how they differ.
53
46
  - A test that expects to fail proves one test case: its first assertion after setup is the one the defect breaks, and a red step stops every step after it. Declare one such test per test case the defect breaks, so each test case reports on its own.
54
47
  - Accepted test cases may share one test that expects to pass.
55
- - A test that expects to fail asserts a value the code does not return today. Before you declare that test, put that value on the test case it sends, as `expectedValue`, and name where you read it, as `expectedFrom`. If the source states the rule and never writes the number, put the rule in `derived` and leave `expectedValue` out.
56
- 12. **Register the whole plan in one call to `skyramp_register_test_plan`.** The call carries `changes`, `defects` and `plannedTests`.
48
+ 12. **Register the whole plan in one call to `skyramp_register_test_plan`.**
57
49
  13. **Answer each objection, or change the plan and register it again.**
58
50
  An answer that says a state cannot be produced says what you tried.
59
51
  14. **Stop when every objection has an answer.** Disagreeing in the answer closes it too.
@@ -26,7 +26,7 @@ Apply only to new test functions you are adding and existing test functions affe
26
26
 
27
27
  Before editing `$2`, you must output a `<thinking>` block. It states the behaviour each test evaluates, then works through the rules for the responses that behaviour depends on, so no rule that applies is overlooked. The JSON array must match the `rule_checklist` template that accompanies the `$0` rules. Use the template's keys verbatim; each key is the slug shown in that rule's heading. Every key must appear — `[]` where the rule does not apply.
28
28
 
29
- 1. For each test function in scope, name the behaviour it evaluates in one line, taken from the recommendation it came from. Generated test functions are usually named `test_integration` or `testUi`, which claim nothing — use the name only when it is specific enough to add something, and never treat a generic one as evidence the test has no single behaviour. Then scan `$2` and the expected responses for the code change tested.
29
+ 1. For each test function in scope, name the behaviour it evaluates in one line, taken from the recommendation it came from. A generated test function may carry a name that states its behaviour — trust that name when it does. A function still named `test_integration` or `testUi` claims nothing, so use the name only when it is specific enough to add something, and never treat a generic one as evidence the test has no single behaviour. Then scan `$2` and the expected responses for the code change tested.
30
30
  2. Classify each response first by its response status type and then assign the applicable assertion rules to the response.
31
31
  - Success with body (2xx with a response body): all assertion rules below may apply — echo-back of request fields, computed response fields, array / items validation, and chained values across steps.
32
32
  - Success with no body (200/202/204 with an empty body — e.g. logout/cancel/submit action endpoints): assert the status code only. Also apply chained-values rules if a follow-up step uses this response's ID.