@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.
- package/build/playwright/traceRecordingPrompt.js +2 -1
- package/build/prompts/orderAssertionRule.d.ts +9 -0
- package/build/prompts/orderAssertionRule.js +9 -0
- package/build/prompts/test-maintenance/driftAnalysisShared.js +2 -0
- package/build/prompts/testbot/testbot-prompts.js +10 -7
- package/build/recommendation/answers.d.ts +54 -19
- package/build/recommendation/answers.js +102 -31
- package/build/recommendation/blockerProbes.d.ts +13 -0
- package/build/recommendation/blockerProbes.js +91 -0
- package/build/recommendation/blockers.d.ts +65 -0
- package/build/recommendation/blockers.js +135 -0
- package/build/recommendation/pullRequestText.d.ts +2 -0
- package/build/recommendation/pullRequestText.js +16 -0
- package/build/recommendation/registerPlan.d.ts +7 -4
- package/build/recommendation/registerPlan.js +37 -11
- package/build/recommendation/types.d.ts +21 -4
- package/build/recommendation/verifierContracts.d.ts +6 -6
- package/build/recommendation/verifierContracts.js +8 -7
- package/build/recommendation/verifiers/citedPath.d.ts +2 -2
- package/build/recommendation/verifiers/citedPath.js +2 -2
- package/build/recommendation/verifiers/coverage.js +25 -25
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +3 -3
- package/build/recommendation/verifiers/issueTraceability.d.ts +1 -5
- package/build/recommendation/verifiers/issueTraceability.js +4 -35
- package/build/tools/executeSkyrampTestTool.js +8 -0
- package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -0
- package/build/tools/generate-tests/generateE2ERestTool.js +9 -1
- package/build/tools/generate-tests/generateUIRestTool.d.ts +2 -0
- package/build/tools/generate-tests/generateUIRestTool.js +9 -1
- package/build/tools/submitReportTool.d.ts +9 -9
- package/build/tools/submitReportTool.js +178 -26
- package/build/tools/test-management/registerTestPlanTool.d.ts +20 -14
- package/build/tools/test-management/registerTestPlanTool.js +30 -14
- package/build/types/TestTypes.d.ts +7 -0
- package/build/types/TestTypes.js +22 -0
- package/build/types/TestbotReport.d.ts +4 -1
- package/build/utils/assertion-verify/metrics.js +12 -0
- package/build/utils/findUp.d.ts +20 -0
- package/build/utils/findUp.js +59 -0
- package/build/utils/language-helper.js +1 -1
- package/build/utils/repoSdkVersion.d.ts +16 -0
- package/build/utils/repoSdkVersion.js +88 -0
- package/build/utils/testDependencyPolicy.d.ts +20 -0
- package/build/utils/testDependencyPolicy.js +64 -4
- package/build/utils/utils-verify/typecheck.js +3 -18
- package/node_modules/playwright/lib/mcp/skyramp/assertOrder.js +66 -0
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +12 -3
- package/node_modules/playwright/lib/mcp/skyramp/common/liveReadError.js +40 -0
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +21 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -1
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +96 -16
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +10 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +22 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/elementOrder.js +57 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.uiPTFncY.js → index.0MJW0uG7.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +26 -1
- package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/elementOrder.ts +83 -0
- package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
- package/node_modules/playwright/package.json +1 -1
- package/package.json +4 -2
- package/plugin/prompts/plan-tests.md +9 -17
- package/plugin/skills/enhance-assertions/reference/shared-rules.md +1 -1
- package/plugin/skills/enhance-assertions/reference/ui.md +2 -2
- package/plugin/skills/fix-test-import-errors/SKILL.md +1 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
7
|
import { getPersonaPrefix } from "../prompts/personas.js";
|
|
8
|
+
import { ORDER_ASSERTION_RULE } from "../prompts/orderAssertionRule.js";
|
|
8
9
|
export function getTraceRecordingPromptText(opts) {
|
|
9
10
|
const outputDir = opts?.outputDir;
|
|
10
11
|
const modularize = opts?.modularize ?? true;
|
|
@@ -65,7 +66,7 @@ Call \`browser_assert\` when assertions are needed. Provide \`expected\` for eve
|
|
|
65
66
|
|
|
66
67
|
**Visual / screenshot assertions.** When the instruction asks to take a screenshot, capture a baseline, or verify how a page/element/region *looks* (as opposed to its text or value), call \`browser_visual_snapshot\` — it records a \`toHaveScreenshot()\` assertion so the generated test pixel-compares against a baseline image on every run.
|
|
67
68
|
|
|
68
|
-
**When the change under test is how something looks** — a colour, class or attribute the diff changes — assert it directly with \`browser_assert\` \`type: css/class/attribute\` on the element that OWNS the property (the flex container for \`flex-direction\`, not its children); if it has no stable ref, note a recording gap rather than assert the wrong element.
|
|
69
|
+
**When the change under test is how something looks** — a colour, class or attribute the diff changes — assert it directly with \`browser_assert\` \`type: css/class/attribute\` on the element that OWNS the property (the flex container for \`flex-direction\`, not its children); if it has no stable ref, note a recording gap rather than assert the wrong element. ${ORDER_ASSERTION_RULE} \`browser_visual_snapshot\` only for a look no property expresses (image, gradient, chart).
|
|
69
70
|
|
|
70
71
|
Do NOT use \`browser_take_screenshot\` to record a visual check: it only captures a throwaway image, is dropped at export (it produces no assertion), and will NOT appear in the generated test. Use it only to view the page yourself while recording. \`filename\` is optional on \`browser_visual_snapshot\`: if the instruction names a baseline file (e.g. \`page-001.png\`) pass it as \`filename\`, otherwise omit it and one is auto-generated per \`snapshotType\` (\`page-NNN.png\` for a page, \`el-NNN.png\` for an element, \`region-NNN.png\` for a region).
|
|
71
72
|
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When to record a swap, move or reorder as an order assertion. It lives in the
|
|
3
|
+
* trace-recording prompt, which the testbot flow includes; the top-level testbot
|
|
4
|
+
* prompt does not repeat test-type details. The type's parameters are
|
|
5
|
+
* documented by browser_assert's own description, and the pairing rule (two
|
|
6
|
+
* sibling blocks, not a block and something inside it) is enforced and
|
|
7
|
+
* explained by the tool, so neither is repeated here.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ORDER_ASSERTION_RULE = "**A swap, move or reorder** (left/right columns swapped, a sidebar moved, blocks reordered) is recorded with `browser_assert` `type: \"order\"` on two blocks (`ref` and `otherRef`), so the test fails if the swap is undone; `flex-direction` alone is not enough when the diff reorders markup.";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* When to record a swap, move or reorder as an order assertion. It lives in the
|
|
3
|
+
* trace-recording prompt, which the testbot flow includes; the top-level testbot
|
|
4
|
+
* prompt does not repeat test-type details. The type's parameters are
|
|
5
|
+
* documented by browser_assert's own description, and the pairing rule (two
|
|
6
|
+
* sibling blocks, not a block and something inside it) is enforced and
|
|
7
|
+
* explained by the tool, so neither is repeated here.
|
|
8
|
+
*/
|
|
9
|
+
export const ORDER_ASSERTION_RULE = '**A swap, move or reorder** (left/right columns swapped, a sidebar moved, blocks reordered) is recorded with `browser_assert` `type: "order"` on two blocks (`ref` and `otherRef`), so the test fails if the swap is undone; `flex-direction` alone is not enough when the diff reorders markup.';
|
|
@@ -55,6 +55,8 @@ ${opts.domainNotes}
|
|
|
55
55
|
|
|
56
56
|
**When uncertain, use VERIFY not IGNORE:** If the diff touches code this test could exercise but you cannot confirm impact without reading more context, assign VERIFY — do not default to IGNORE.
|
|
57
57
|
|
|
58
|
+
**If your rationale concludes the test is unaffected, assign IGNORE, not VERIFY.** Assign VERIFY only for a doubt that the code you read does not settle. A rule that forbids IGNORE still applies.
|
|
59
|
+
|
|
58
60
|
**A stated requirement wins over the code.** Before you write a new value or refresh a screenshot in an existing test, compare it with what the pull request title, the description and any requirements file they name state. If the new value breaks a stated requirement, do not write it: keep that assertion on the requirement's value and make only the other edits. If no other edit remains, mark the test VERIFY. The test stays red; do not relax it. If the requirements file is unchanged and the description presents the new behaviour as intended, the file is out of date: update the test.
|
|
59
61
|
|
|
60
62
|
**Pre-commit verification — confirm before finalizing UPDATE/REGENERATE/DELETE:**
|
|
@@ -6,7 +6,7 @@ import { setReportLanguage } from "../../utils/reportLanguage.js";
|
|
|
6
6
|
import { setPlanOnlyMode } from "../../utils/planOnlyMode.js";
|
|
7
7
|
import { parseMaxFixAttemptsArg, resolveMaxFixAttempts, setMaxFixAttempts, } from "../../utils/fixAttempts.js";
|
|
8
8
|
import { setPrimaryRepository } from "../../utils/AnalysisStateManager.js";
|
|
9
|
-
import { recordPullRequestText } from "../../recommendation/pullRequestText.js";
|
|
9
|
+
import { recordPullRequestText, recordUiCredentials, } from "../../recommendation/pullRequestText.js";
|
|
10
10
|
import { AUTH_MIDDLEWARE_PATTERNS_STR } from "../../utils/workspaceAuth.js";
|
|
11
11
|
import { TASK_ANALYZE_MAINTAIN, TASK_GENERATE, TASK_SUBMIT, TESTBOT_TASK1_MAINTAIN_LABELS, TESTBOT_TASK1_MULTIREPO_LABELS, TESTBOT_TASK1_STEP_LABELS, orderedLabels, stepSubRef, taskRef, } from "../test-recommendation/recommendationShared.js";
|
|
12
12
|
import { getTraceRecordingPromptText } from "../../playwright/traceRecordingPrompt.js";
|
|
@@ -239,6 +239,9 @@ export function getTestbotPrompt(opts) {
|
|
|
239
239
|
setPrimaryRepository({ repositoryPath, repo: primaryRepo });
|
|
240
240
|
// Kept in the process for the plan tool, which is registered on this server.
|
|
241
241
|
recordPullRequestText(prTitle, prDescription);
|
|
242
|
+
// Beside it and for the same reason: a `credential-missing` blocker is checked
|
|
243
|
+
// against whether this run was given any.
|
|
244
|
+
recordUiCredentials(uiCredentials);
|
|
242
245
|
// The legacy count arguments are DECLARED NOWHERE, so zod strips them and their
|
|
243
246
|
// names never reach the agent — not in a schema it lists, not in rendered text.
|
|
244
247
|
// A number in front of the agent reads as a ceiling whatever the words around it
|
|
@@ -332,12 +335,12 @@ export function getTestbotPrompt(opts) {
|
|
|
332
335
|
let maintenanceBeforeExecStep;
|
|
333
336
|
let verifyExternalStep;
|
|
334
337
|
if (planOnly) {
|
|
335
|
-
verifyExternalStep = ` ${MAINTAIN.VERIFY_EXTERNAL}. Plan-only run: do not run the external tests you edited.`;
|
|
338
|
+
verifyExternalStep = ` ${MAINTAIN.VERIFY_EXTERNAL}. Plan-only run: do not run the external tests you edited or the VERIFY files.`;
|
|
336
339
|
maintenanceBeforeExecStep = ` ${MAINTAIN.BASELINE}. Plan-only run: skip the pre-edit baseline execution — the application is not running. Record every maintenance verdict from static drift analysis alone; execution statuses simply remain unrecorded.`;
|
|
337
340
|
}
|
|
338
341
|
else {
|
|
339
|
-
verifyExternalStep = ` ${MAINTAIN.VERIFY_EXTERNAL}. Run the external tests you edited again. Do this step right after ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.APPLY_ACTIONS)}. It applies to each \`[external]\` test file that you edited in ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.APPLY_ACTIONS)} and that ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.BASELINE)} ran. Run each such file with \`skyramp_execute_test\`, \`phase: "after"\` and \`stateFile
|
|
340
|
-
maintenanceBeforeExecStep = ` ${MAINTAIN.BASELINE}. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every Skyramp test you will change, and for every test the repository owns that you will mark UPDATE: ${runTestCall()}. Run them one at a time, not in parallel. Run the command even when you expect the runner or a package to be missing: the failure output is what names what is missing, and a check you make before the run reaches no repair. On this run repair the environment only — install what is missing and run the file again — and never edit the test file, because the result of the file as the repository wrote it IS the baseline. This captures the pre-edit baseline — do not skip it even when you expect the test to fail. \`skyramp_actions\` refuses an UPDATE of the repository's own test until that test has this run. Nothing else owes one: a VERIFY changes no file, and a REGENERATE or a DELETE of the repository's own test is a recommendation you report rather than apply. Never pass \`rebaselineSnapshots\` on this run: a \`Screenshot comparison failed\` here is the evidence that a visual baseline is stale, and the refresh belongs to the final execution.`;
|
|
342
|
+
verifyExternalStep = ` ${MAINTAIN.VERIFY_EXTERNAL}. Run the external tests you edited again, and run each VERIFY file once. Do this step right after ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.APPLY_ACTIONS)}, before you plan new tests. It applies to each \`[external]\` test file that you edited in ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.APPLY_ACTIONS)} and that ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.BASELINE)} ran, and to each file you marked VERIFY, except a test in a related repository. Run each such file with \`skyramp_execute_test\`, \`phase: "after"\` and \`stateFile\`. For an edited file, use the same command as in ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.BASELINE)}. Record each result as the file's \`afterStatus\`. The final execution excludes \`[external]\` files and VERIFY files, so this step is where they get their after-run. If no file applies, say so in your report. If a file still fails, report it — do not run it again.`;
|
|
343
|
+
maintenanceBeforeExecStep = ` ${MAINTAIN.BASELINE}. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every Skyramp test you will change, and for every test the repository owns that you will mark UPDATE: ${runTestCall()}. Run them one at a time, not in parallel. Run the command even when you expect the runner or a package to be missing: the failure output is what names what is missing, and a check you make before the run reaches no repair. On this run repair the environment only — install what is missing and run the file again — and never edit the test file, because the result of the file as the repository wrote it IS the baseline. This captures the pre-edit baseline — do not skip it even when you expect the test to fail. \`skyramp_actions\` refuses an UPDATE of the repository's own test until that test has this run. Nothing else owes one: a VERIFY changes no file, so its one run comes in ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.VERIFY_EXTERNAL)}, and a REGENERATE or a DELETE of the repository's own test is a recommendation you report rather than apply. Never pass \`rebaselineSnapshots\` on this run: a \`Screenshot comparison failed\` here is the evidence that a visual baseline is stale, and the refresh belongs to the final execution.`;
|
|
341
344
|
}
|
|
342
345
|
// For follow-up requests: emit the @skyramp-testbot header + guardrails, both stop-early.
|
|
343
346
|
// For first-run prompts: emit the full Task 1 analysis + maintenance section.
|
|
@@ -536,7 +539,7 @@ ${CONTRACT_MODE_GUIDANCE}
|
|
|
536
539
|
- **Removal guard — when \`uiContext.removedElements\` is non-empty**: the server lists which elements this PR removed — each identified by a \`data-*\` attribute and value, with the file it came from (renames, moves and deleted files are excluded). Read that file and the route or page files that render it to decide which page each appeared on. Per PAGE: \`browser_navigate\` to it, wait for the app to render, then call \`browser_assert_hidden\` once per listed element using the target the list shows — \`testId: "<value>"\` when the attribute is \`data-testid\`, otherwise \`attribute: "<name>", value: "<value>"\` (e.g. \`attribute: "data-cy"\`); no \`ref\` — the element no longer exists, so no snapshot has one; the tool checks the live page and refuses to record on a blank or un-rendered page. Then at least one \`browser_assert\` on a RETAINED element of the same page verifying its text, value or state. If a UI trace for that page is already planned, record the guard assertions inside that trace; otherwise this is its own trace and spec, \`verify-removed-ui-<page-slug>\`. The generated test asserts \`toBeHidden()\` on \`getByTestId(...)\` or \`page.locator('[data-cy="..."]')\` accordingly. A removed element the server did not list still gets its test (the removal check requires it) — ground that one in the diff and prefix its \`reasoning\` with \`[removed-element diff-grounded]\`; prefix \`reasoning\` with \`[removed-element server-listed]\` when the element came from \`uiContext.removedElements\`. When the removed element lived inside a control the page has to open — an \`option\` of a \`select\`, an item of a closed menu — open that control before asserting: a closed control's contents are never visible, so a hidden assertion recorded against a shut one passes whether the element was removed or not. \`browser_assert_hidden\` stays the tool either way, because \`browser_assert\` needs a snapshot \`ref\` and a removed element has none; where the control cannot be opened, say so in \`reasoning\` so the assertion step turns that guard into a count. **Deleted routes or pages are deleted features, not removed elements**: test the removal through the link or entry point that survives. A trace may still open the retired URL — to assert the app's fallback there, or that the old page's elements are gone — after it has recorded a positive \`browser_assert\` on a surviving page; a trace whose only visited page is the dead URL has no positive companion and is not a guard spec.
|
|
537
540
|
- **Renames are not removals — on every PR, whatever \`uiContext.removedElements\` holds**: when a test id, \`aria-label\` or other accessible name changed, assert the element by its new identifier and leave the retired identifier alone (no \`browser_assert_hidden\` on it). A rename PR removes nothing, so this rule applies when the removed list is empty.
|
|
538
541
|
- **\`browser_visual_snapshot\` — for visual/appearance checks**: when the instruction asks to take a screenshot, capture a baseline, or verify how a page/element/region *looks* (not its text or value), call \`browser_visual_snapshot\` — it records a \`toHaveScreenshot()\` assertion so the generated test pixel-compares against a baseline on every run. **EXCEPTION:** when the look in question IS the change under test (a colour, class, attribute or element-order the diff makes), assert the property directly instead — see the next bullet. Do NOT use \`browser_take_screenshot\` for this: it captures a throwaway image that is dropped at export and never appears in the generated test (use it only to view the page yourself).
|
|
539
|
-
- **A colour, class or attribute change the diff makes**: record it directly — \`browser_assert\` \`type: css/class/attribute\` on the element that OWNS the property (the flex container for \`flex-direction\`, not its children). If that owner has no stable ref, say so in \`reasoning\` as a recording gap — do not assert the wrong element or screenshot it.
|
|
542
|
+
- **A colour, class or attribute change the diff makes**: record it directly — \`browser_assert\` \`type: css/class/attribute\` on the element that OWNS the property (the flex container for \`flex-direction\`, not its children). If that owner has no stable ref, say so in \`reasoning\` as a recording gap — do not assert the wrong element or screenshot it. \`browser_visual_snapshot\` only for a look no property expresses (image, gradient, chart).
|
|
540
543
|
- **Wait for stable state before the second capture**: After performing an action that affects computed fields (filling a discount, submitting a form, adding an item), check the current page state before calling the second \`browser_blueprint\` (the capture after the action). If a computed field — total, price, count, derived text — still shows its initial empty or zero value (e.g. \`$0.00\`, \`0\`, \`Loading...\`, empty string), that means async data hasn't finished loading yet. Use \`browser_wait_for\` to wait up to 10 seconds for the field to update to a real value (for example, wait for the total to show a non-zero amount like \`$799.99\` instead of \`$0.00\`). Once the field shows a real value, THEN call the second \`browser_blueprint\` to capture stable state. If after 10 seconds the field still hasn't updated, skip the assertion on that field — don't capture and assert a value that hasn't loaded.
|
|
541
544
|
If \`browser_navigate\` fails (app not running / connection refused), apply skip condition (a) above: leave the planned test in your plan, say in \`businessCaseAnalysis\` why it was not written, AND record the outage in \`issuesFound\`.
|
|
542
545
|
Each trace costs browser tool calls, so record the ones your plan holds and no more. One trace that asserts a real outcome is worth more than three that assert a heading.
|
|
@@ -641,9 +644,9 @@ Do not make any changes other than the code-reuse refactoring (step ${POSTGEN.UI
|
|
|
641
644
|
${FINISH_CHECKS_BLOCK}
|
|
642
645
|
|
|
643
646
|
**Execution timing:**
|
|
644
|
-
- **beforeStatus** (maintained tests only): execute each maintained test file **once at the start** (before any edits) to capture \`beforeStatus\`. This is the only execution allowed before edits.
|
|
647
|
+
- **beforeStatus** (maintained tests only): execute each maintained test file you will edit **once at the start** (before any edits) to capture \`beforeStatus\`. This is the only execution allowed before edits.
|
|
645
648
|
- **Probe residue**: before the final execution, delete through the API the records your own probes and recordings created. Find each one by the id its create response returned, or by the exact value you typed for it during a recording. Do not delete anything else, and do not delete by a pattern. Do not use the database or a container. Do not add the deletion to a test. List in the report the records the API could not remove.
|
|
646
|
-
- **Final execution**: Apart from ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.VERIFY_EXTERNAL)}, do NOT call \`skyramp_execute_test\` again until ALL maintenance edits AND ALL new test generation/enhancement are complete. Then execute every test file once — maintained files (for \`afterStatus\`) and new files together — passing \`stateFile\` and \`repository\` on every call: the attempt cap and the \`afterStatus\` the report reads both live in that file, and \`repository\` names the section the result lands in. Omitting \`stateFile\` does not skip the cap — the tool falls back to this run's own state file — but a wrong or missing \`repository\` fails the call. For a maintained spec whose \`skyramp_actions\` entry carried \`rebaseline_snapshots\`, pass that exact list as \`rebaselineSnapshots\` on this run and only this run. **Execute tests SEQUENTIALLY (one at a time)** — do NOT send multiple \`skyramp_execute_test\` calls in the same tool call batch, as concurrent execution overwhelms the stdio transport and causes MCP disconnection. Exclude tests marked \`[external]
|
|
649
|
+
- **Final execution**: Apart from ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.VERIFY_EXTERNAL)}, do NOT call \`skyramp_execute_test\` again until ALL maintenance edits AND ALL new test generation/enhancement are complete. Then execute every test file once — maintained files (for \`afterStatus\`) and new files together — passing \`stateFile\` and \`repository\` on every call: the attempt cap and the \`afterStatus\` the report reads both live in that file, and \`repository\` names the section the result lands in. Omitting \`stateFile\` does not skip the cap — the tool falls back to this run's own state file — but a wrong or missing \`repository\` fails the call. For a maintained spec whose \`skyramp_actions\` entry carried \`rebaseline_snapshots\`, pass that exact list as \`rebaselineSnapshots\` on this run and only this run. **Execute tests SEQUENTIALLY (one at a time)** — do NOT send multiple \`skyramp_execute_test\` calls in the same tool call batch, as concurrent execution overwhelms the stdio transport and causes MCP disconnection. Exclude tests marked \`[external]\` and VERIFY files: both ran in ${stepSubRef(TASK1.MAINTAIN, MAINTAIN.VERIFY_EXTERNAL)}.
|
|
647
650
|
- Only report test results for files you actually ran.
|
|
648
651
|
|
|
649
652
|
### Post-Execution: Assertion Alignment
|
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
import { ObjectionAnswer, Objection } from "./types.js";
|
|
1
|
+
import { ObjectionAnswer, Objection, PlanBlocker } from "./types.js";
|
|
2
|
+
import { BlockerVerdict } from "./blockers.js";
|
|
3
|
+
export { BLOCKER_DESCRIPTION } from "./blockers.js";
|
|
4
|
+
/** What the server made of one answer's blocker. Passed in, never computed here:
|
|
5
|
+
* the probes are network and git calls and the verifiers reach neither. UNDEFINED
|
|
6
|
+
* where the caller ran no probes, which reads as `unverifiable` — the position
|
|
7
|
+
* every blocker close stood in before the checks existed. */
|
|
8
|
+
export type BlockerJudge = (blocker: PlanBlocker) => BlockerVerdict;
|
|
2
9
|
/** The objection raised below carries this prefix and the id it objects to. */
|
|
3
10
|
export declare const UNKNOWN_ANSWER_PREFIX = "answers:unknown:";
|
|
4
|
-
/** The
|
|
5
|
-
* agent reorders its list between registrations. A whitespace-only
|
|
6
|
-
* nothing, and the FIRST NON-BLANK entry wins when one id is
|
|
7
|
-
|
|
11
|
+
/** The entry that closed one objection, matched by `objectionId` and never by
|
|
12
|
+
* position — the agent reorders its list between registrations. A whitespace-only
|
|
13
|
+
* answer closes nothing, and the FIRST NON-BLANK entry wins when one id is
|
|
14
|
+
* answered twice. Its `answer` comes back trimmed.
|
|
15
|
+
*
|
|
16
|
+
* The ENTRY, so a caller that files the blocker files the one that arrived with
|
|
17
|
+
* THIS answer. Searching for the two separately took them from different entries:
|
|
18
|
+
* a blank-answer entry supplied the blocker, and a refuted claim was published
|
|
19
|
+
* beside an answer a later entry had closed. */
|
|
20
|
+
export declare function closingAnswerFor(objection: Objection, acknowledged: ObjectionAnswer[], judge?: BlockerJudge): ObjectionAnswer | undefined;
|
|
21
|
+
/** The closing answer as the sentence alone, for a caller that files no blocker. */
|
|
22
|
+
export declare function answerFor(objection: Objection, acknowledged: ObjectionAnswer[], judge?: BlockerJudge): string | undefined;
|
|
8
23
|
/** One objection per answer naming an objection this call did not raise — a stale
|
|
9
24
|
* id, a typo and a pre-emptive answer all used to pass in silence. An id is
|
|
10
25
|
* recognised BY ITS PREFIX and never draws an objection of its own. `scope` and
|
|
@@ -13,29 +28,49 @@ export declare function unknownAnswerObjections(objections: Objection[], acknowl
|
|
|
13
28
|
scope: string;
|
|
14
29
|
suggestion: string;
|
|
15
30
|
}): Objection[];
|
|
16
|
-
/** The
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
31
|
+
/** The objections an answer does not close, only a planned test.
|
|
32
|
+
*
|
|
33
|
+
* `coverage:stateTest:` — the missed bugs it exists for were reached by prose twice
|
|
34
|
+
* and skipped twice, so an answer would close the objection and leave the bug.
|
|
35
|
+
* `verifierContracts.ts` carries the evidence.
|
|
36
|
+
*
|
|
37
|
+
* `defects:untested:` — a declared defect's test can always be written, even when
|
|
38
|
+
* the service it runs against is down: it is committed and runs once the service is
|
|
39
|
+
* back. Every blocker Wipro repo-1 sent for a defect (14 on one run, 2 on the next)
|
|
40
|
+
* named a limit of the browser tools, and none got a test.
|
|
41
|
+
*
|
|
42
|
+
* `defects:expectedPass:` — a test that passes today proves nothing about a defect,
|
|
43
|
+
* and a sentence here would let one cited passing test stand in for the red one. */
|
|
44
|
+
export declare const NON_ANSWERABLE_PREFIXES: readonly ["coverage:stateTest:", "defects:untested:", "defects:expectedPass:"];
|
|
21
45
|
export declare function isNonAnswerable(objectionId: unknown): boolean;
|
|
22
46
|
/** The objections a sentence alone does not close either. A change with no test
|
|
23
|
-
* needs the test, a value a page and API change refuses needs a UI
|
|
24
|
-
*
|
|
25
|
-
* `verifierContracts.ts`)
|
|
26
|
-
* the
|
|
27
|
-
* answer also names what stopped the test: a service that is not running, a
|
|
28
|
-
* branch that no longer exists, the one credential the run holds. Measured on run
|
|
47
|
+
* needs the test, a value a page and API change accepts or refuses needs a UI step
|
|
48
|
+
* that names it (the XR02 evidence is at `surfaceCase` and `surfaceReject` in
|
|
49
|
+
* `verifierContracts.ts`). Each closes only when the answer also names what stopped
|
|
50
|
+
* the test: a service that is not running, or the one credential the run holds. Measured on run
|
|
29
51
|
* 34176038240: across eight fixtures 45 objections were raised and 45 were
|
|
30
52
|
* closed, and the one class that a sentence cannot close was never raised at all.
|
|
31
53
|
* Prose closed every uncovered change, including changes nothing prevented a test
|
|
32
54
|
* from reaching. Runs 34423214322 and 34423217796 then lost three defects the
|
|
33
|
-
* baseline reported: each was listed and closed by a sentence.
|
|
34
|
-
|
|
55
|
+
* baseline reported: each was listed and closed by a sentence.
|
|
56
|
+
*
|
|
57
|
+
* A bug the report states is the same promise made after the tests run, so it
|
|
58
|
+
* closes the same way. Runs 34434835959 (repo15 RG21 and RG15) and 34434838150
|
|
59
|
+
* (repo10 RG03) answered three `issueTraceability:` objections with "outside this
|
|
60
|
+
* PR's surface" and "unobservable" and wrote no test — and RG21 and RG15 were the
|
|
61
|
+
* same 404 bootstrap bug, reported twice and tested neither time. */
|
|
62
|
+
export declare const BLOCKER_ONLY_PREFIXES: readonly ["coverage:change:", "coverage:surfaceCase:", "coverage:surfaceReject:", "issueTraceability:"];
|
|
35
63
|
export declare function needsBlocker(objectionId: unknown): boolean;
|
|
36
64
|
/** The objection raised below carries this prefix and the id it objects to. */
|
|
37
65
|
export declare const REFUSED_ANSWER_PREFIX = "answers:refused:";
|
|
38
66
|
/** One objection per answer to an objection only a planned test closes. The answered
|
|
39
67
|
* objection STAYS OPEN — this says so, rather than letting the answer pass in
|
|
40
68
|
* silence while the plan still owes the test. */
|
|
41
|
-
export declare function refusedAnswerObjections(objections: Objection[], acknowledged: ObjectionAnswer[]
|
|
69
|
+
export declare function refusedAnswerObjections(objections: Objection[], acknowledged: ObjectionAnswer[], judge?: BlockerJudge,
|
|
70
|
+
/** Whether an EARLIER call already closed this objection and that close still
|
|
71
|
+
* stands. The report tool carries answers across calls, so an objection closed
|
|
72
|
+
* on call 1 is raised again on call 2 and answered again there. Without this,
|
|
73
|
+
* a weaker second answer raised `answers:refused:` beside a close the report
|
|
74
|
+
* publishes as answered — the report then said both. `registerPlan` carries
|
|
75
|
+
* the previous registration's closes the same way. */
|
|
76
|
+
closedEarlier?: (objectionId: string) => boolean): Objection[];
|
|
@@ -1,9 +1,34 @@
|
|
|
1
|
+
import { hasBlocker, refutedEvidence } from "./blockers.js";
|
|
2
|
+
export { BLOCKER_DESCRIPTION } from "./blockers.js";
|
|
3
|
+
/** Whether this answer's blocker closes the objection: present, and not refuted by
|
|
4
|
+
* the server's own probe. */
|
|
5
|
+
function blockerCloses(blocker, judge) {
|
|
6
|
+
if (!hasBlocker(blocker))
|
|
7
|
+
return false;
|
|
8
|
+
return !judge || judge(blocker) !== "refuted";
|
|
9
|
+
}
|
|
10
|
+
/** Whether one entry closes an objection that needs a blocker: the SAME rule
|
|
11
|
+
* `closingAnswerFor` applies, so a sibling that suppresses a refusal is a sibling
|
|
12
|
+
* that really closed. Reading the blocker alone let an entry with a good blocker
|
|
13
|
+
* and an empty answer suppress the refusal while closing nothing. */
|
|
14
|
+
function closes(entry, judge) {
|
|
15
|
+
const answer = entry?.answer;
|
|
16
|
+
if (typeof answer !== "string" || answer.trim().length === 0)
|
|
17
|
+
return false;
|
|
18
|
+
return blockerCloses(entry?.blocker, judge);
|
|
19
|
+
}
|
|
1
20
|
/** The objection raised below carries this prefix and the id it objects to. */
|
|
2
21
|
export const UNKNOWN_ANSWER_PREFIX = "answers:unknown:";
|
|
3
|
-
/** The
|
|
4
|
-
* agent reorders its list between registrations. A whitespace-only
|
|
5
|
-
* nothing, and the FIRST NON-BLANK entry wins when one id is
|
|
6
|
-
|
|
22
|
+
/** The entry that closed one objection, matched by `objectionId` and never by
|
|
23
|
+
* position — the agent reorders its list between registrations. A whitespace-only
|
|
24
|
+
* answer closes nothing, and the FIRST NON-BLANK entry wins when one id is
|
|
25
|
+
* answered twice. Its `answer` comes back trimmed.
|
|
26
|
+
*
|
|
27
|
+
* The ENTRY, so a caller that files the blocker files the one that arrived with
|
|
28
|
+
* THIS answer. Searching for the two separately took them from different entries:
|
|
29
|
+
* a blank-answer entry supplied the blocker, and a refuted claim was published
|
|
30
|
+
* beside an answer a later entry had closed. */
|
|
31
|
+
export function closingAnswerFor(objection, acknowledged, judge) {
|
|
7
32
|
const wantsBlocker = needsBlocker(objection.objectionId);
|
|
8
33
|
for (const entry of acknowledged) {
|
|
9
34
|
if (entry.objectionId !== objection.objectionId)
|
|
@@ -12,12 +37,16 @@ export function answerFor(objection, acknowledged) {
|
|
|
12
37
|
const answer = typeof rawAnswer === "string" ? rawAnswer.trim() : "";
|
|
13
38
|
if (answer.length === 0)
|
|
14
39
|
continue;
|
|
15
|
-
if (wantsBlocker &&
|
|
40
|
+
if (wantsBlocker && !blockerCloses(entry.blocker, judge))
|
|
16
41
|
continue;
|
|
17
|
-
return answer;
|
|
42
|
+
return { ...entry, answer };
|
|
18
43
|
}
|
|
19
44
|
return undefined;
|
|
20
45
|
}
|
|
46
|
+
/** The closing answer as the sentence alone, for a caller that files no blocker. */
|
|
47
|
+
export function answerFor(objection, acknowledged, judge) {
|
|
48
|
+
return closingAnswerFor(objection, acknowledged, judge)?.answer;
|
|
49
|
+
}
|
|
21
50
|
/** One objection per answer naming an objection this call did not raise — a stale
|
|
22
51
|
* id, a typo and a pre-emptive answer all used to pass in silence. An id is
|
|
23
52
|
* recognised BY ITS PREFIX and never draws an objection of its own. `scope` and
|
|
@@ -43,31 +72,50 @@ export function unknownAnswerObjections(objections, acknowledged, { scope, sugge
|
|
|
43
72
|
}
|
|
44
73
|
return raisedHere;
|
|
45
74
|
}
|
|
46
|
-
/** The
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
|
|
75
|
+
/** The objections an answer does not close, only a planned test.
|
|
76
|
+
*
|
|
77
|
+
* `coverage:stateTest:` — the missed bugs it exists for were reached by prose twice
|
|
78
|
+
* and skipped twice, so an answer would close the objection and leave the bug.
|
|
79
|
+
* `verifierContracts.ts` carries the evidence.
|
|
80
|
+
*
|
|
81
|
+
* `defects:untested:` — a declared defect's test can always be written, even when
|
|
82
|
+
* the service it runs against is down: it is committed and runs once the service is
|
|
83
|
+
* back. Every blocker Wipro repo-1 sent for a defect (14 on one run, 2 on the next)
|
|
84
|
+
* named a limit of the browser tools, and none got a test.
|
|
85
|
+
*
|
|
86
|
+
* `defects:expectedPass:` — a test that passes today proves nothing about a defect,
|
|
87
|
+
* and a sentence here would let one cited passing test stand in for the red one. */
|
|
88
|
+
export const NON_ANSWERABLE_PREFIXES = [
|
|
89
|
+
"coverage:stateTest:",
|
|
90
|
+
"defects:untested:",
|
|
91
|
+
"defects:expectedPass:",
|
|
92
|
+
];
|
|
93
|
+
const DEFECT_PREFIXES = ["defects:untested:", "defects:expectedPass:"];
|
|
51
94
|
export function isNonAnswerable(objectionId) {
|
|
52
95
|
return (typeof objectionId === "string" &&
|
|
53
|
-
objectionId.startsWith(
|
|
96
|
+
NON_ANSWERABLE_PREFIXES.some((prefix) => objectionId.startsWith(prefix)));
|
|
54
97
|
}
|
|
55
98
|
/** The objections a sentence alone does not close either. A change with no test
|
|
56
|
-
* needs the test, a value a page and API change refuses needs a UI
|
|
57
|
-
*
|
|
58
|
-
* `verifierContracts.ts`)
|
|
59
|
-
* the
|
|
60
|
-
* answer also names what stopped the test: a service that is not running, a
|
|
61
|
-
* branch that no longer exists, the one credential the run holds. Measured on run
|
|
99
|
+
* needs the test, a value a page and API change accepts or refuses needs a UI step
|
|
100
|
+
* that names it (the XR02 evidence is at `surfaceCase` and `surfaceReject` in
|
|
101
|
+
* `verifierContracts.ts`). Each closes only when the answer also names what stopped
|
|
102
|
+
* the test: a service that is not running, or the one credential the run holds. Measured on run
|
|
62
103
|
* 34176038240: across eight fixtures 45 objections were raised and 45 were
|
|
63
104
|
* closed, and the one class that a sentence cannot close was never raised at all.
|
|
64
105
|
* Prose closed every uncovered change, including changes nothing prevented a test
|
|
65
106
|
* from reaching. Runs 34423214322 and 34423217796 then lost three defects the
|
|
66
|
-
* baseline reported: each was listed and closed by a sentence.
|
|
107
|
+
* baseline reported: each was listed and closed by a sentence.
|
|
108
|
+
*
|
|
109
|
+
* A bug the report states is the same promise made after the tests run, so it
|
|
110
|
+
* closes the same way. Runs 34434835959 (repo15 RG21 and RG15) and 34434838150
|
|
111
|
+
* (repo10 RG03) answered three `issueTraceability:` objections with "outside this
|
|
112
|
+
* PR's surface" and "unobservable" and wrote no test — and RG21 and RG15 were the
|
|
113
|
+
* same 404 bootstrap bug, reported twice and tested neither time. */
|
|
67
114
|
export const BLOCKER_ONLY_PREFIXES = [
|
|
68
115
|
"coverage:change:",
|
|
116
|
+
"coverage:surfaceCase:",
|
|
69
117
|
"coverage:surfaceReject:",
|
|
70
|
-
"
|
|
118
|
+
"issueTraceability:",
|
|
71
119
|
];
|
|
72
120
|
export function needsBlocker(objectionId) {
|
|
73
121
|
return (typeof objectionId === "string" &&
|
|
@@ -78,7 +126,14 @@ export const REFUSED_ANSWER_PREFIX = "answers:refused:";
|
|
|
78
126
|
/** One objection per answer to an objection only a planned test closes. The answered
|
|
79
127
|
* objection STAYS OPEN — this says so, rather than letting the answer pass in
|
|
80
128
|
* silence while the plan still owes the test. */
|
|
81
|
-
export function refusedAnswerObjections(objections, acknowledged
|
|
129
|
+
export function refusedAnswerObjections(objections, acknowledged, judge,
|
|
130
|
+
/** Whether an EARLIER call already closed this objection and that close still
|
|
131
|
+
* stands. The report tool carries answers across calls, so an objection closed
|
|
132
|
+
* on call 1 is raised again on call 2 and answered again there. Without this,
|
|
133
|
+
* a weaker second answer raised `answers:refused:` beside a close the report
|
|
134
|
+
* publishes as answered — the report then said both. `registerPlan` carries
|
|
135
|
+
* the previous registration's closes the same way. */
|
|
136
|
+
closedEarlier) {
|
|
82
137
|
const raised = new Set(objections
|
|
83
138
|
.map((objection) => objection.objectionId)
|
|
84
139
|
.filter((id) => isNonAnswerable(id) || needsBlocker(id)));
|
|
@@ -91,21 +146,37 @@ export function refusedAnswerObjections(objections, acknowledged) {
|
|
|
91
146
|
const answer = entry.answer;
|
|
92
147
|
if (typeof answer !== "string" || answer.trim().length === 0)
|
|
93
148
|
continue;
|
|
94
|
-
// An answer
|
|
95
|
-
//
|
|
96
|
-
|
|
149
|
+
// An answer whose blocker the server did not refute DOES close an untested
|
|
150
|
+
// change. Refused: the sentence on its own, and a claim the probe found untrue.
|
|
151
|
+
// Read across EVERY entry for this id, not just this one: the agent may send
|
|
152
|
+
// two, and one of them closing means the objection is closed — refusing on the
|
|
153
|
+
// other raised a refusal beside a close for the same objection.
|
|
154
|
+
if (needsBlocker(id) &&
|
|
155
|
+
(acknowledged.some((other) => other.objectionId === id && closes(other, judge)) ||
|
|
156
|
+
closedEarlier?.(id))) {
|
|
97
157
|
continue;
|
|
158
|
+
}
|
|
98
159
|
seen.add(id);
|
|
160
|
+
const blocker = entry?.blocker;
|
|
161
|
+
const refutedBlocker = needsBlocker(id) && hasBlocker(blocker) ? blocker : undefined;
|
|
99
162
|
refused.push({
|
|
100
163
|
objectionId: `${REFUSED_ANSWER_PREFIX}${id}`,
|
|
101
164
|
verifier: "answers",
|
|
102
|
-
message:
|
|
103
|
-
? "The
|
|
104
|
-
:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
165
|
+
message: refutedBlocker
|
|
166
|
+
? "The server checked the blocker this answer names and found the opposite, so the objection stays open."
|
|
167
|
+
: needsBlocker(id)
|
|
168
|
+
? "The answer says why there is no test, and it does not say what stopped this run from writing one, so the objection stays open."
|
|
169
|
+
: "This objection is closed only by a planned test, so the answer closed nothing and the objection stays open.",
|
|
170
|
+
evidence: refutedBlocker
|
|
171
|
+
? `answered "${id}" with \`${refutedBlocker.kind}\`; ${refutedEvidence(refutedBlocker)}`
|
|
172
|
+
: `answered "${id}"${needsBlocker(id) ? " with no `blocker`" : ", which no answer closes"}`,
|
|
173
|
+
suggestion: refutedBlocker
|
|
174
|
+
? "Write the test: what you named as the blocker is not in the way. If something else is, send the blocker that names it — the server checks each kind against the run it can see."
|
|
175
|
+
: needsBlocker(id)
|
|
176
|
+
? "Plan the test and register the plan again. If this run cannot write one, send the same answer with `blocker` — `service-unreachable` with the URL, or `credential-missing` with the URL of the page that asked for the login. A reason it is not worth testing is not a blocker."
|
|
177
|
+
: DEFECT_PREFIXES.some((prefix) => id.startsWith(prefix))
|
|
178
|
+
? "Plan a test that cites the defect in `defects` and sets `expected.outcome` to `fail`, then register the plan again. If a service the test needs is down, write the test anyway: it is committed and runs once the service is back."
|
|
179
|
+
: "Add a planned test that names the record's state as `startState` and runs this route's mutation on it, then register the plan again. Every other objection takes an answer; this one takes a test.",
|
|
109
180
|
});
|
|
110
181
|
}
|
|
111
182
|
return refused;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BlockerChecks } from "./blockers.js";
|
|
2
|
+
import { PlanBlocker } from "./types.js";
|
|
3
|
+
/** The three checks, resolved. Every await happens HERE, before the plan verifiers
|
|
4
|
+
* run: they are synchronous, so a probe cannot be reached from inside one. Each
|
|
5
|
+
* distinct URL and branch is probed once however many answers name it.
|
|
6
|
+
*
|
|
7
|
+
* Called by both tools. The plan tool has a `VerifyContext` to hang the result on
|
|
8
|
+
* and the report tool does not, so this returns the checks rather than the context. */
|
|
9
|
+
export declare function probeBlockers(blockers: PlanBlocker[]): Promise<BlockerChecks>;
|
|
10
|
+
/** The blockers an answers list carries. */
|
|
11
|
+
export declare function blockersIn(answers: ReadonlyArray<{
|
|
12
|
+
blocker?: PlanBlocker;
|
|
13
|
+
}>): PlanBlocker[];
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { probeUrl } from "./blockers.js";
|
|
2
|
+
import { uiCredentialsSupplied } from "./pullRequestText.js";
|
|
3
|
+
import { capturedBlueprintDigests } from "../playwright/blueprintDigestStore.js";
|
|
4
|
+
import { logger } from "../utils/logger.js";
|
|
5
|
+
/** How long a service gets to answer before the run calls it unreachable. Long
|
|
6
|
+
* enough for a container still warming up, short enough that a plan registration
|
|
7
|
+
* with several blockers does not stall the agent. */
|
|
8
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
9
|
+
/** Whether the service at `url` answered. ANY status counts — a 500 is a service
|
|
10
|
+
* that is running, and the claim under test is only that the run could not reach
|
|
11
|
+
* it. UNDEFINED is never returned here: a request either gets a response or does
|
|
12
|
+
* not, and `verifyBlocker` decides what an unparseable URL means. */
|
|
13
|
+
async function serviceAnswers(url) {
|
|
14
|
+
for (const method of ["HEAD", "GET"]) {
|
|
15
|
+
try {
|
|
16
|
+
await fetch(url, {
|
|
17
|
+
method,
|
|
18
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS),
|
|
19
|
+
redirect: "manual",
|
|
20
|
+
});
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// HEAD is refused by some servers that answer GET perfectly well, so a
|
|
25
|
+
// failure on it is not yet an unreachable service.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
/** A URL's path with no trailing slash, or UNDEFINED. The host is not compared: the
|
|
31
|
+
* agent writes `localhost` where the capture wrote `127.0.0.1`, and the reverse. */
|
|
32
|
+
function pagePath(raw) {
|
|
33
|
+
try {
|
|
34
|
+
const path = new URL(raw).pathname.replace(/\/+$/, "");
|
|
35
|
+
return path || "/";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Whether a capture this run took reached the page at `url` and read elements
|
|
42
|
+
* from it. */
|
|
43
|
+
function pageLoaded(url) {
|
|
44
|
+
const wanted = pagePath(url);
|
|
45
|
+
return capturedBlueprintDigests().some((capture) => capture.elements.length > 0 && pagePath(capture.url) === wanted);
|
|
46
|
+
}
|
|
47
|
+
/** The three checks, resolved. Every await happens HERE, before the plan verifiers
|
|
48
|
+
* run: they are synchronous, so a probe cannot be reached from inside one. Each
|
|
49
|
+
* distinct URL and branch is probed once however many answers name it.
|
|
50
|
+
*
|
|
51
|
+
* Called by both tools. The plan tool has a `VerifyContext` to hang the result on
|
|
52
|
+
* and the report tool does not, so this returns the checks rather than the context. */
|
|
53
|
+
export async function probeBlockers(blockers) {
|
|
54
|
+
const urls = new Set();
|
|
55
|
+
for (const blocker of blockers) {
|
|
56
|
+
const detail = String(blocker?.detail ?? "").trim();
|
|
57
|
+
if (!detail)
|
|
58
|
+
continue;
|
|
59
|
+
if (blocker.kind === "service-unreachable")
|
|
60
|
+
urls.add(detail);
|
|
61
|
+
}
|
|
62
|
+
const serviceResults = new Map();
|
|
63
|
+
await Promise.all([
|
|
64
|
+
...[...urls].map(async (raw) => {
|
|
65
|
+
// The SAME filter `verifyBlocker` applies, and it gates the request. Reading
|
|
66
|
+
// the detail with a bare `new URL` sent the server after whatever the agent
|
|
67
|
+
// wrote — `file:`, `data:`, a link-local address — and made the verdict a
|
|
68
|
+
// reachability oracle for anything the URL could name.
|
|
69
|
+
const url = probeUrl(raw);
|
|
70
|
+
if (!url)
|
|
71
|
+
return;
|
|
72
|
+
serviceResults.set(url, await serviceAnswers(url));
|
|
73
|
+
}),
|
|
74
|
+
]).catch((error) => {
|
|
75
|
+
// A probe that throws leaves its entry unset, and an unset entry reads as
|
|
76
|
+
// "could not tell". A blocker must never close because a check crashed, and it
|
|
77
|
+
// must never be refused for that either.
|
|
78
|
+
logger.warning(`Blocker probe failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
serviceAnswers: (url) => serviceResults.get(url),
|
|
82
|
+
credentialsSupplied: uiCredentialsSupplied(),
|
|
83
|
+
pageLoaded,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** The blockers an answers list carries. */
|
|
87
|
+
export function blockersIn(answers) {
|
|
88
|
+
return answers
|
|
89
|
+
.map((entry) => entry.blocker)
|
|
90
|
+
.filter((blocker) => blocker !== undefined);
|
|
91
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { PlanBlocker } from "./types.js";
|
|
3
|
+
/** What `blocker` is, in the words both tools' schemas give the agent. One string:
|
|
4
|
+
* the plan states the rule before the tests run and the report states it after, and
|
|
5
|
+
* two copies drifted the moment either was edited. */
|
|
6
|
+
export declare const BLOCKER_DESCRIPTION = "What stopped this run from writing the test, as one of two claims the server checks: `service-unreachable` with the URL the run needed, or `credential-missing` with the URL of the page the missing login guards. An objection about a change or a reported bug with no test, or about a value a page and API change accepts or refuses that no UI test names, closes only with one of these, and the server probes it \u2014 a claim it finds untrue leaves the objection open. A reason the test is not worth writing is not a blocker. A step that the browser tools cannot record is not a blocker either: record the steps they can, then edit the test file the generator writes and add the missing step yourself.";
|
|
7
|
+
/** The `kind` values, in the schema's own words. */
|
|
8
|
+
export declare const BLOCKER_KIND_DESCRIPTION = "Which of the two checkable things stopped the test. `service-unreachable`: the run could not reach a service it needed. `credential-missing`: the run holds no login for a page the test has to get past.";
|
|
9
|
+
export declare const BLOCKER_DETAIL_DESCRIPTION = "The fact behind the claim: for `service-unreachable` the URL, spelled as the run tried it \u2014 the server requests it; for `credential-missing` the URL of the page that asked for the login \u2014 the server reads it against the pages this run already loaded, and a page the run loaded with no login needs none.";
|
|
10
|
+
/** The blocker both tools take, as the schema that rejects anything else. An
|
|
11
|
+
* OBJECT, not a sentence: free text let an argument close an untested defect, and
|
|
12
|
+
* each kind here names one fact the server can probe. `.strict()` so an invented
|
|
13
|
+
* kind or an extra field fails the schema rather than reading as a claim nothing
|
|
14
|
+
* could check. */
|
|
15
|
+
export declare const blockerSchema: z.ZodEffects<z.ZodObject<{
|
|
16
|
+
kind: z.ZodEnum<["service-unreachable", "credential-missing"]>;
|
|
17
|
+
detail: z.ZodEffects<z.ZodString, string, string>;
|
|
18
|
+
}, "strict", z.ZodTypeAny, {
|
|
19
|
+
kind: "service-unreachable" | "credential-missing";
|
|
20
|
+
detail: string;
|
|
21
|
+
}, {
|
|
22
|
+
kind: "service-unreachable" | "credential-missing";
|
|
23
|
+
detail: string;
|
|
24
|
+
}>, {
|
|
25
|
+
kind: "service-unreachable" | "credential-missing";
|
|
26
|
+
detail: string;
|
|
27
|
+
}, {
|
|
28
|
+
kind: "service-unreachable" | "credential-missing";
|
|
29
|
+
detail: string;
|
|
30
|
+
}>;
|
|
31
|
+
/** What the server made of a blocker's claim.
|
|
32
|
+
*
|
|
33
|
+
* `refuted` is the one that costs the agent something: the probe found the
|
|
34
|
+
* opposite of what the blocker said, so the answer closes nothing. `unverifiable`
|
|
35
|
+
* is not a soft refusal — it is the server saying it could not look, which is
|
|
36
|
+
* where every close stood before these checks existed. */
|
|
37
|
+
export type BlockerVerdict = "verified" | "refuted" | "unverifiable";
|
|
38
|
+
/** The three probes, INJECTED so this module reaches neither the network nor the
|
|
39
|
+
* filesystem — the same rule `VerifyContext.citedFileExists` follows. Each returns
|
|
40
|
+
* UNDEFINED when it could not tell, which is a verdict of its own and never a
|
|
41
|
+
* silent pass. */
|
|
42
|
+
export interface BlockerChecks {
|
|
43
|
+
/** Whether the service at this URL answered an HTTP request. Any status counts:
|
|
44
|
+
* a 500 is a service that is running. */
|
|
45
|
+
serviceAnswers(url: string): boolean | undefined;
|
|
46
|
+
/** Whether the run was given browser credentials. */
|
|
47
|
+
credentialsSupplied: boolean | undefined;
|
|
48
|
+
/** Whether a page capture this run took reached this URL's page and read its
|
|
49
|
+
* elements. A page that redirected to a login reports the login's URL. */
|
|
50
|
+
pageLoaded(url: string): boolean | undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Whether an answer carries a blocker at all: a kind the schema knows and a detail
|
|
53
|
+
* that says something. Read defensively — the plan comes off disk, so the declared
|
|
54
|
+
* type holds only on the validated path. */
|
|
55
|
+
export declare function hasBlocker(blocker: unknown): blocker is PlanBlocker;
|
|
56
|
+
/** The blocker as one line, for a report field and an objection's evidence. */
|
|
57
|
+
export declare function blockerText(blocker: PlanBlocker): string;
|
|
58
|
+
/** An http(s) URL the probe can be pointed at, or UNDEFINED. A detail that is not
|
|
59
|
+
* one is not a wrong claim — it is a claim nothing can be made of. */
|
|
60
|
+
export declare function probeUrl(detail: string): string | undefined;
|
|
61
|
+
/** What the server made of this blocker. Pure: every fact comes from `checks`. */
|
|
62
|
+
export declare function verifyBlocker(blocker: PlanBlocker, checks: BlockerChecks): BlockerVerdict;
|
|
63
|
+
/** What the server saw, in the words the refusal quotes back. Only ever called on a
|
|
64
|
+
* `refuted` verdict, so each branch states the opposite of the claim. */
|
|
65
|
+
export declare function refutedEvidence(blocker: PlanBlocker): string;
|