@skyramp/mcp 0.4.3-rc.2 → 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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** What `blocker` is, in the words both tools' schemas give the agent. One string:
|
|
3
|
+
* the plan states the rule before the tests run and the report states it after, and
|
|
4
|
+
* two copies drifted the moment either was edited. */
|
|
5
|
+
export 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 — 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.";
|
|
6
|
+
/** The `kind` values, in the schema's own words. */
|
|
7
|
+
export 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.";
|
|
8
|
+
export const BLOCKER_DETAIL_DESCRIPTION = "The fact behind the claim: for `service-unreachable` the URL, spelled as the run tried it — the server requests it; for `credential-missing` the URL of the page that asked for the login — the server reads it against the pages this run already loaded, and a page the run loaded with no login needs none.";
|
|
9
|
+
/** The blocker both tools take, as the schema that rejects anything else. An
|
|
10
|
+
* OBJECT, not a sentence: free text let an argument close an untested defect, and
|
|
11
|
+
* each kind here names one fact the server can probe. `.strict()` so an invented
|
|
12
|
+
* kind or an extra field fails the schema rather than reading as a claim nothing
|
|
13
|
+
* could check. */
|
|
14
|
+
export const blockerSchema = z
|
|
15
|
+
.object({
|
|
16
|
+
kind: z
|
|
17
|
+
.enum(["service-unreachable", "credential-missing"])
|
|
18
|
+
.describe(BLOCKER_KIND_DESCRIPTION),
|
|
19
|
+
detail: z
|
|
20
|
+
.string()
|
|
21
|
+
.refine((value) => value.trim().length > 0, {
|
|
22
|
+
message: "a blocker's detail must not be blank",
|
|
23
|
+
})
|
|
24
|
+
.describe(BLOCKER_DETAIL_DESCRIPTION),
|
|
25
|
+
})
|
|
26
|
+
.strict()
|
|
27
|
+
// A detail the server cannot make anything of used to be accepted and then called
|
|
28
|
+
// `unverifiable`, which closes — so `service-unreachable: outside this PR` was the
|
|
29
|
+
// free-text escape this schema exists to remove. Each kind states its detail in the
|
|
30
|
+
// shape its check takes, or the plan is refused here. `credential-missing` was a
|
|
31
|
+
// sentence, and it closed an XR02 page objection on a page that needs no login.
|
|
32
|
+
.superRefine((blocker, ctx) => {
|
|
33
|
+
const detail = String(blocker?.detail ?? "").trim();
|
|
34
|
+
if (!detail || probeUrl(detail))
|
|
35
|
+
return;
|
|
36
|
+
ctx.addIssue({
|
|
37
|
+
code: z.ZodIssueCode.custom,
|
|
38
|
+
path: ["detail"],
|
|
39
|
+
message: blocker.kind === "credential-missing"
|
|
40
|
+
? "a `credential-missing` detail is the URL of the page that asked for the login, with an `http://` or `https://` scheme — the server reads it against the pages this run loaded."
|
|
41
|
+
: "a `service-unreachable` detail is the URL the run tried, with an `http://` or `https://` scheme — the server requests it. A cloud metadata address is not requested.",
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
/** Whether an answer carries a blocker at all: a kind the schema knows and a detail
|
|
45
|
+
* that says something. Read defensively — the plan comes off disk, so the declared
|
|
46
|
+
* type holds only on the validated path. */
|
|
47
|
+
export function hasBlocker(blocker) {
|
|
48
|
+
const kind = blocker?.kind;
|
|
49
|
+
const detail = blocker?.detail;
|
|
50
|
+
return ((kind === "service-unreachable" || kind === "credential-missing") &&
|
|
51
|
+
typeof detail === "string" &&
|
|
52
|
+
detail.trim().length > 0);
|
|
53
|
+
}
|
|
54
|
+
/** The blocker as one line, for a report field and an objection's evidence. */
|
|
55
|
+
export function blockerText(blocker) {
|
|
56
|
+
return `${blocker.kind}: ${String(blocker.detail ?? "").trim()}`;
|
|
57
|
+
}
|
|
58
|
+
/** The addresses a probe must never be pointed at: the cloud metadata endpoint, in
|
|
59
|
+
* the two spellings it wears. The service under test runs on the runner, so
|
|
60
|
+
* loopback, `host.docker.internal` and the private ranges stay reachable — they
|
|
61
|
+
* are the probe's whole purpose. Only the link-local range, which serves nothing
|
|
62
|
+
* a test needs and serves instance credentials on every major cloud, is closed. */
|
|
63
|
+
function isMetadataAddress(hostname) {
|
|
64
|
+
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
65
|
+
// 169.254.0.0/16, the same range mapped into IPv6 (the URL parser writes it as
|
|
66
|
+
// `::ffff:a9fe:…`), and fe80::/10 written as any of its four leading nibbles.
|
|
67
|
+
if (/^169\.254\./.test(host) ||
|
|
68
|
+
/^::ffff:a9fe:/.test(host) ||
|
|
69
|
+
/^fe[89ab][0-9a-f]:/.test(host))
|
|
70
|
+
return true;
|
|
71
|
+
return host === "metadata" || host === "metadata.google.internal";
|
|
72
|
+
}
|
|
73
|
+
/** An http(s) URL the probe can be pointed at, or UNDEFINED. A detail that is not
|
|
74
|
+
* one is not a wrong claim — it is a claim nothing can be made of. */
|
|
75
|
+
export function probeUrl(detail) {
|
|
76
|
+
try {
|
|
77
|
+
const url = new URL(detail.trim());
|
|
78
|
+
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
79
|
+
return undefined;
|
|
80
|
+
// `fetch` throws on a URL with a login in it, which would read as unreachable.
|
|
81
|
+
if (url.username || url.password)
|
|
82
|
+
return undefined;
|
|
83
|
+
return isMetadataAddress(url.hostname) ? undefined : url.toString();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** What the server made of this blocker. Pure: every fact comes from `checks`. */
|
|
90
|
+
export function verifyBlocker(blocker, checks) {
|
|
91
|
+
const detail = String(blocker?.detail ?? "").trim();
|
|
92
|
+
switch (blocker?.kind) {
|
|
93
|
+
case "service-unreachable": {
|
|
94
|
+
const url = probeUrl(detail);
|
|
95
|
+
if (!url)
|
|
96
|
+
return "unverifiable";
|
|
97
|
+
const answered = checks?.serviceAnswers(url);
|
|
98
|
+
return answered === undefined
|
|
99
|
+
? "unverifiable"
|
|
100
|
+
: answered
|
|
101
|
+
? "refuted"
|
|
102
|
+
: "verified";
|
|
103
|
+
}
|
|
104
|
+
case "credential-missing": {
|
|
105
|
+
// The run records ONE opaque credentials string, so `true` says a login was
|
|
106
|
+
// supplied and nothing about which one — and that login may be what let a
|
|
107
|
+
// capture reach the page. Only `false` states a fact about the claim, and then
|
|
108
|
+
// a capture of the page says the page needed no login.
|
|
109
|
+
const url = probeUrl(detail);
|
|
110
|
+
if (!url || checks?.credentialsSupplied !== false)
|
|
111
|
+
return "unverifiable";
|
|
112
|
+
const loaded = checks?.pageLoaded(url);
|
|
113
|
+
return loaded === undefined
|
|
114
|
+
? "unverifiable"
|
|
115
|
+
: loaded
|
|
116
|
+
? "refuted"
|
|
117
|
+
: "verified";
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
return "unverifiable";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** What the server saw, in the words the refusal quotes back. Only ever called on a
|
|
124
|
+
* `refuted` verdict, so each branch states the opposite of the claim. */
|
|
125
|
+
export function refutedEvidence(blocker) {
|
|
126
|
+
const detail = String(blocker?.detail ?? "").trim();
|
|
127
|
+
switch (blocker?.kind) {
|
|
128
|
+
case "service-unreachable":
|
|
129
|
+
return `${detail} answered an HTTP request, so the service is running`;
|
|
130
|
+
case "credential-missing":
|
|
131
|
+
return `this run loaded ${detail} with no login and read its elements, so the page needs none`;
|
|
132
|
+
default:
|
|
133
|
+
return blockerText(blocker);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -9,6 +9,8 @@ export declare function pullRequestText(): {
|
|
|
9
9
|
description: string;
|
|
10
10
|
} | undefined;
|
|
11
11
|
export declare function clearPullRequestText(): void;
|
|
12
|
+
export declare function recordUiCredentials(value: unknown): void;
|
|
13
|
+
export declare function uiCredentialsSupplied(): boolean | undefined;
|
|
12
14
|
/** The pull request as one searchable string. Whitespace runs fold to one space, so
|
|
13
15
|
* a quote wrapped across lines still matches the sentence it was taken from. */
|
|
14
16
|
export declare const searchable: (pr: {
|
|
@@ -21,6 +21,22 @@ export function pullRequestText() {
|
|
|
21
21
|
}
|
|
22
22
|
export function clearPullRequestText() {
|
|
23
23
|
recorded = undefined;
|
|
24
|
+
credentialsSupplied = undefined;
|
|
25
|
+
}
|
|
26
|
+
/** Whether the run was given browser credentials, recorded from the same place and
|
|
27
|
+
* for the same reason as the text above: the prompt and the plan tool share a
|
|
28
|
+
* process, so the flag is already here. A blocker of kind `credential-missing`
|
|
29
|
+
* is checked against it.
|
|
30
|
+
*
|
|
31
|
+
* UNDEFINED when this process never rendered the prompt, which is not `false` —
|
|
32
|
+
* the server cannot tell a run with no credentials from a run it never saw, and
|
|
33
|
+
* the blocker is then unverifiable rather than true. */
|
|
34
|
+
let credentialsSupplied;
|
|
35
|
+
export function recordUiCredentials(value) {
|
|
36
|
+
credentialsSupplied = typeof value === "string" && value.trim().length > 0;
|
|
37
|
+
}
|
|
38
|
+
export function uiCredentialsSupplied() {
|
|
39
|
+
return credentialsSupplied;
|
|
24
40
|
}
|
|
25
41
|
/** The pull request as one searchable string. Whitespace runs fold to one space, so
|
|
26
42
|
* a quote wrapped across lines still matches the sentence it was taken from. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Objection, ObjectionAnswer, PlanChange, PlanDefect, PlanInput, PlanMaintenance, PlannedTest, VerifyContext } from "./types.js";
|
|
1
|
+
import { Objection, ObjectionAnswer, PlanBlocker, PlanChange, PlanDefect, PlanInput, PlanMaintenance, PlannedTest, VerifyContext } from "./types.js";
|
|
2
2
|
/** The plan as stored for the run. Every registration produces one; the newest
|
|
3
3
|
* replaces the last. */
|
|
4
4
|
export interface Plan {
|
|
@@ -14,11 +14,14 @@ export interface Plan {
|
|
|
14
14
|
* `coverage` counts the changes they cite, so a reader of the plan has to be
|
|
15
15
|
* able to see which coverage came from maintenance rather than a new test. */
|
|
16
16
|
maintains?: PlanMaintenance[];
|
|
17
|
-
/** Objections the agent answered, with the answers.
|
|
17
|
+
/** Objections the agent answered, with the answers. A `blocker` here is one the
|
|
18
|
+
* server PROBED and could not refute — an unreachable service that really did not
|
|
19
|
+
* answer, a branch that really is gone — so the close is a checked one. */
|
|
18
20
|
answeredObjections: Array<{
|
|
19
21
|
objection: Objection;
|
|
20
22
|
answer: string;
|
|
21
|
-
blocker?:
|
|
23
|
+
blocker?: PlanBlocker;
|
|
24
|
+
blockerVerified?: boolean;
|
|
22
25
|
}>;
|
|
23
26
|
/** Closed by a blocker nothing checked. Its own bucket rather than a flag on
|
|
24
27
|
* `answeredObjections`, so a run's count of them is a number the report can
|
|
@@ -29,7 +32,7 @@ export interface Plan {
|
|
|
29
32
|
unverifiedCloses: Array<{
|
|
30
33
|
objection: Objection;
|
|
31
34
|
answer: string;
|
|
32
|
-
blocker:
|
|
35
|
+
blocker: PlanBlocker;
|
|
33
36
|
}>;
|
|
34
37
|
/** Objections with no answer. Rendered in the report — this is the accountability. */
|
|
35
38
|
openObjections: Objection[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { hasBlocker } from "./blockers.js";
|
|
2
|
+
import { answerFor, closingAnswerFor, isNonAnswerable, refusedAnswerObjections, unknownAnswerObjections, } from "./answers.js";
|
|
2
3
|
import { runPlanTimeVerifiers } from "./runVerifiers.js";
|
|
3
4
|
import { logger } from "../utils/logger.js";
|
|
4
5
|
function storedRegistrationNumber(registration) {
|
|
@@ -37,7 +38,7 @@ function carriedAnswerFor(objectionId, previous) {
|
|
|
37
38
|
return {
|
|
38
39
|
objectionId,
|
|
39
40
|
answer,
|
|
40
|
-
...(entry.blocker ? { blocker: entry.blocker } : {}),
|
|
41
|
+
...(hasBlocker(entry.blocker) ? { blocker: entry.blocker } : {}),
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
return undefined;
|
|
@@ -65,6 +66,9 @@ export function registerPlan(registration, ctx, previous) {
|
|
|
65
66
|
const changes = registration.changes;
|
|
66
67
|
const defects = registration.defects;
|
|
67
68
|
const registrationNumber = storedRegistrationNumber(registration);
|
|
69
|
+
// Awaited by the caller before this context was built: every probe is an HTTP
|
|
70
|
+
// request or a `git` call, and nothing from here down is asynchronous.
|
|
71
|
+
const judge = ctx?.blockerVerdict?.bind(ctx);
|
|
68
72
|
const verified = runPlanTimeVerifiers(registration, ctx);
|
|
69
73
|
const objections = [
|
|
70
74
|
...verified,
|
|
@@ -72,7 +76,15 @@ export function registerPlan(registration, ctx, previous) {
|
|
|
72
76
|
scope: "on this plan",
|
|
73
77
|
suggestion: "Answer only the ids in the Objections list this registration returns. An objection you have since fixed is gone and needs no answer, and an objection raised after the tests run cannot be answered before them.",
|
|
74
78
|
}),
|
|
75
|
-
...refusedAnswerObjections(verified, acknowledged)
|
|
79
|
+
...refusedAnswerObjections(verified, acknowledged, judge, (id) => {
|
|
80
|
+
// The previous registration's close is carried below when this answer does
|
|
81
|
+
// not close. If that carried close still stands, it is the close.
|
|
82
|
+
const objection = verified.find((o) => o.objectionId === id);
|
|
83
|
+
const carried = carriedAnswerFor(id, previous);
|
|
84
|
+
return (!!objection &&
|
|
85
|
+
!!carried &&
|
|
86
|
+
!!closingAnswerFor(objection, [carried], judge));
|
|
87
|
+
}),
|
|
76
88
|
];
|
|
77
89
|
const carried = carriedAnswers(objections, acknowledged, previous);
|
|
78
90
|
// The carried answers go last: `answerFor` takes the first non-blank entry, so
|
|
@@ -87,15 +99,29 @@ export function registerPlan(registration, ctx, previous) {
|
|
|
87
99
|
// a planned test too, unless the answer names what stopped this run from
|
|
88
100
|
// writing one. Either way an answer that does not close it leaves it open and
|
|
89
101
|
// draws the refusal objection raised above.
|
|
90
|
-
const
|
|
102
|
+
const closing = isNonAnswerable(objection.objectionId)
|
|
91
103
|
? undefined
|
|
92
|
-
:
|
|
93
|
-
if (
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
104
|
+
: closingAnswerFor(objection, answers, judge);
|
|
105
|
+
if (closing) {
|
|
106
|
+
const answer = closing.answer;
|
|
107
|
+
// The blocker that arrived with THIS answer. Searching for it apart from the
|
|
108
|
+
// answer took the two from different entries, and filed a refuted blocker,
|
|
109
|
+
// or one no answer closed, as the close.
|
|
110
|
+
const blocker = hasBlocker(closing.blocker) && judge?.(closing.blocker) !== "refuted"
|
|
111
|
+
? closing.blocker
|
|
112
|
+
: undefined;
|
|
113
|
+
// A blocker the server PROBED and could not refute is a checked close and sits
|
|
114
|
+
// with the answers. One nothing could look at is counted apart: a run whose
|
|
115
|
+
// objections all close that way must not read like one that answered.
|
|
116
|
+
if (blocker && judge?.(blocker) === "verified") {
|
|
117
|
+
answeredObjections.push({
|
|
118
|
+
objection,
|
|
119
|
+
answer,
|
|
120
|
+
blocker,
|
|
121
|
+
blockerVerified: true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
else if (blocker)
|
|
99
125
|
unverifiedCloses.push({ objection, answer, blocker });
|
|
100
126
|
else
|
|
101
127
|
answeredObjections.push({ objection, answer });
|
|
@@ -164,13 +164,24 @@ export interface PlannedTest {
|
|
|
164
164
|
* included — the name is what joins the test to the diff it is checked against. */
|
|
165
165
|
repository: string;
|
|
166
166
|
}
|
|
167
|
+
/** The kinds of thing that stop a run from writing a test AND that the server can
|
|
168
|
+
* check. Free text let an argument close an untested defect — run 34434835959
|
|
169
|
+
* closed one with "the PR description states no redirect or fallback" — so a
|
|
170
|
+
* blocker is now a claim of one of these, and the server probes it. */
|
|
171
|
+
export type PlanBlockerKind = "service-unreachable" | "credential-missing";
|
|
172
|
+
/** What stopped this run from writing the test, as a checkable claim. */
|
|
173
|
+
export interface PlanBlocker {
|
|
174
|
+
kind: PlanBlockerKind;
|
|
175
|
+
/** `service-unreachable`: the URL the run needed. `credential-missing`: the URL
|
|
176
|
+
* of the page that asked for the login. */
|
|
177
|
+
detail: string;
|
|
178
|
+
}
|
|
167
179
|
export interface ObjectionAnswer {
|
|
168
180
|
objectionId: string;
|
|
169
181
|
answer: string;
|
|
170
|
-
/** What stopped this run from writing the test
|
|
171
|
-
*
|
|
172
|
-
|
|
173
|
-
blocker?: string;
|
|
182
|
+
/** What stopped this run from writing the test. An objection about an untested
|
|
183
|
+
* change, an untested defect or a reported bug closes only with one. */
|
|
184
|
+
blocker?: PlanBlocker;
|
|
174
185
|
}
|
|
175
186
|
/** An existing test this run maintains instead of planning a new one, and the
|
|
176
187
|
* declared changes that maintenance covers. A removal already covered by a spec
|
|
@@ -255,6 +266,12 @@ export interface VerifyContext {
|
|
|
255
266
|
* the verifiers filesystem-free. Named a repository it asks that one, and a name
|
|
256
267
|
* this run does not know answers false. Omitted, it asks every one. */
|
|
257
268
|
citedFileExists(relativePath: string, repository?: string): boolean;
|
|
269
|
+
/** What the server made of one answer's blocker. Injected for the same reason as
|
|
270
|
+
* `citedFileExists`, and more so: the probes are HTTP requests and `git` calls,
|
|
271
|
+
* and they are awaited before this context is built because the verifiers are
|
|
272
|
+
* synchronous. UNDEFINED where the caller ran no probes — every blocker then
|
|
273
|
+
* reads as unverifiable, which is where they all stood before the checks. */
|
|
274
|
+
blockerVerdict?(blocker: PlanBlocker): "verified" | "refuted" | "unverifiable";
|
|
258
275
|
/** The text of a `spec:` file a plan change cites, from the first repository that
|
|
259
276
|
* holds it. UNDEFINED for a path no repository holds, a path outside every
|
|
260
277
|
* checkout, or a file too large to read. Injected for the same reason as
|
|
@@ -126,7 +126,7 @@ export declare const EXPECTED_OUTCOME_CONTRACT: {
|
|
|
126
126
|
};
|
|
127
127
|
readonly executed: {
|
|
128
128
|
readonly message: "This test failed, but it did not reach the behavior it was written for — it fell over earlier. A failure like this is not a caught bug.";
|
|
129
|
-
readonly suggestion: "If the blocker is the test's own setup — sign-in, token, a wrong path — fix it and run the test again. If the blocker is the defect this run reports (a route that is not mounted, a server error before the behaviour), answer this objection naming that issue: the failure then stands as evidence of that issue, not as a caught bug.";
|
|
129
|
+
readonly suggestion: "If the blocker is the test's own setup — sign-in, token, a wrong path — fix it and run the test again. If the blocker is the defect this run reports (a route that is not mounted, a server error before the behaviour), answer this objection naming that issue: the failure then stands as evidence of that issue, not as a caught bug. If a service the test needs is down, answer this objection and set `blocker` to `service-unreachable` with the URL: the test stays committed and runs once the service is back.";
|
|
130
130
|
};
|
|
131
131
|
};
|
|
132
132
|
readonly suggestion: "Should this test fail until the defect is fixed, or is the category wrong for a test that pins behaviour the app already has?";
|
|
@@ -158,7 +158,7 @@ export declare const COVERAGE_CONTRACT: {
|
|
|
158
158
|
readonly objections: {
|
|
159
159
|
readonly change: {
|
|
160
160
|
readonly message: "This declared change has no test in the plan.";
|
|
161
|
-
readonly suggestion: "Plan a test that exercises it. If an existing test already covers it and this run edits that test rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this change's id — maintenance is coverage, and a duplicate spec beside the one you edited is the mistake. If this run cannot write one, answer it and set `blocker` to what stopped the run — a service that is not running,
|
|
161
|
+
readonly suggestion: "Plan a test that exercises it. If an existing test already covers it and this run edits that test, or runs it unchanged as a VERIFY, rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this change's id — maintenance is coverage, and a duplicate spec beside the one you edited is the mistake. If this run cannot write one, answer it and set `blocker` to what stopped the run — a service that is not running, the one credential the run holds. An answer with no blocker leaves this open: a change nothing prevented a test from reaching takes the test. Coverage per file is the basic minimum; coverage per change is what the plan is judged by, so a file covered by a shape test still leaves the change it makes untested.";
|
|
162
162
|
};
|
|
163
163
|
readonly noChanges: {
|
|
164
164
|
readonly message: "The plan declares no changes, so nothing says what the pull request must make different.";
|
|
@@ -190,11 +190,11 @@ export declare const COVERAGE_CONTRACT: {
|
|
|
190
190
|
};
|
|
191
191
|
readonly surfaceCase: {
|
|
192
192
|
readonly message: "This change is on the page and the API, and no ui or e2e test that cites it chooses `{case}` on the page.";
|
|
193
|
-
readonly suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step
|
|
193
|
+
readonly suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.";
|
|
194
194
|
};
|
|
195
195
|
readonly surfaceReject: {
|
|
196
196
|
readonly message: "This change is on the page and the API, and no ui or e2e test that cites it tries `{case}` on the page or checks that the page no longer offers it.";
|
|
197
|
-
readonly suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step
|
|
197
|
+
readonly suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step: try it and assert the page refuses it, or assert the page no longer offers it. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.";
|
|
198
198
|
};
|
|
199
199
|
};
|
|
200
200
|
readonly suggestion: "Which planned test tests each behaviour this change promises and each file it touches, and for one nothing covers, why does it need none?";
|
|
@@ -302,11 +302,11 @@ export declare const DEFECTS_CONTRACT: {
|
|
|
302
302
|
};
|
|
303
303
|
readonly untested: {
|
|
304
304
|
readonly message: "This declared defect has no test in the plan.";
|
|
305
|
-
readonly suggestion: "Add a planned test that cites the defect in `defects` and sets `expected.outcome` to `fail`. A defect in a file this pull request touches gets the test whether or not this pull request introduced it. If an existing test already proves it and this run edits that test rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this defect's id — maintenance is proof, and a duplicate spec beside the one you edited is the mistake.
|
|
305
|
+
readonly suggestion: "Add a planned test that cites the defect in `defects` and sets `expected.outcome` to `fail`. A defect in a file this pull request touches gets the test whether or not this pull request introduced it. If an existing test already proves it and this run edits that test, or runs it unchanged as a VERIFY, rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this defect's id — maintenance is proof, and a duplicate spec beside the one you edited is the mistake. Only a test closes this objection. If a service the test needs is down, write the test anyway: it is committed and runs once the service is back.";
|
|
306
306
|
};
|
|
307
307
|
readonly expectedPass: {
|
|
308
308
|
readonly message: "Every test that cites this defect expects to pass, and a test that passes today proves nothing about a defect.";
|
|
309
|
-
readonly suggestion: "Assert what the fixed code returns
|
|
309
|
+
readonly suggestion: "Assert what the fixed code returns and set `expected.outcome` to `fail` with `why`. Only a test that expects to fail closes this objection.";
|
|
310
310
|
};
|
|
311
311
|
readonly unknownId: {
|
|
312
312
|
readonly message: "A planned test cites a defect id that this plan does not declare.";
|
|
@@ -114,7 +114,7 @@ export const EXPECTED_OUTCOME_CONTRACT = {
|
|
|
114
114
|
// 33988304641, P11), which the run that wrote `true` never drew.
|
|
115
115
|
executed: {
|
|
116
116
|
message: "This test failed, but it did not reach the behavior it was written for — it fell over earlier. A failure like this is not a caught bug.",
|
|
117
|
-
suggestion: "If the blocker is the test's own setup — sign-in, token, a wrong path — fix it and run the test again. If the blocker is the defect this run reports (a route that is not mounted, a server error before the behaviour), answer this objection naming that issue: the failure then stands as evidence of that issue, not as a caught bug.",
|
|
117
|
+
suggestion: "If the blocker is the test's own setup — sign-in, token, a wrong path — fix it and run the test again. If the blocker is the defect this run reports (a route that is not mounted, a server error before the behaviour), answer this objection naming that issue: the failure then stands as evidence of that issue, not as a caught bug. If a service the test needs is down, answer this objection and set `blocker` to `service-unreachable` with the URL: the test stays committed and runs once the service is back.",
|
|
118
118
|
},
|
|
119
119
|
},
|
|
120
120
|
suggestion: "Should this test fail until the defect is fixed, or is the category wrong for a test that pins behaviour the app already has?",
|
|
@@ -149,7 +149,7 @@ export const COVERAGE_CONTRACT = {
|
|
|
149
149
|
objections: {
|
|
150
150
|
change: {
|
|
151
151
|
message: "This declared change has no test in the plan.",
|
|
152
|
-
suggestion: "Plan a test that exercises it. If an existing test already covers it and this run edits that test rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this change's id — maintenance is coverage, and a duplicate spec beside the one you edited is the mistake. If this run cannot write one, answer it and set `blocker` to what stopped the run — a service that is not running,
|
|
152
|
+
suggestion: "Plan a test that exercises it. If an existing test already covers it and this run edits that test, or runs it unchanged as a VERIFY, rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this change's id — maintenance is coverage, and a duplicate spec beside the one you edited is the mistake. If this run cannot write one, answer it and set `blocker` to what stopped the run — a service that is not running, the one credential the run holds. An answer with no blocker leaves this open: a change nothing prevented a test from reaching takes the test. Coverage per file is the basic minimum; coverage per change is what the plan is judged by, so a file covered by a shape test still leaves the change it makes untested.",
|
|
153
153
|
},
|
|
154
154
|
noChanges: {
|
|
155
155
|
message: "The plan declares no changes, so nothing says what the pull request must make different.",
|
|
@@ -203,17 +203,18 @@ export const COVERAGE_CONTRACT = {
|
|
|
203
203
|
suggestion: "Add a test on that surface that cites this change, or answer in one line why that surface needs no test.",
|
|
204
204
|
},
|
|
205
205
|
// `{case}` is the rendered case, as in `level="Principal" (accept)`. SKYR-4529: a form offered Lead and Principal, and
|
|
206
|
-
// only an API test sent Principal.
|
|
206
|
+
// only an API test sent Principal. SKYR-4554: a later XR02 run closed this with "a third red for one defect adds
|
|
207
|
+
// nothing", and no UI test saved Principal.
|
|
207
208
|
surfaceCase: {
|
|
208
209
|
message: "This change is on the page and the API, and no ui or e2e test that cites it chooses `{case}` on the page.",
|
|
209
|
-
suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step
|
|
210
|
+
suggestion: "Add a ui or e2e test, or a step in one, that chooses that value on the page and names it in the step. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.",
|
|
210
211
|
},
|
|
211
212
|
// `{case}` is the rendered case, as in `level="Principal" (accept)`. The XR02 run removed Intern from a form; the page
|
|
212
213
|
// objection closed by a sentence, and no generated test checked the option was
|
|
213
214
|
// gone.
|
|
214
215
|
surfaceReject: {
|
|
215
216
|
message: "This change is on the page and the API, and no ui or e2e test that cites it tries `{case}` on the page or checks that the page no longer offers it.",
|
|
216
|
-
suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step
|
|
217
|
+
suggestion: "Add a ui or e2e test, or a step in one, that names that value in the step: try it and assert the page refuses it, or assert the page no longer offers it. If this run cannot write the test, answer it and set `blocker` to what stopped the run; an answer with no blocker leaves this open.",
|
|
217
218
|
},
|
|
218
219
|
},
|
|
219
220
|
suggestion: "Which planned test tests each behaviour this change promises and each file it touches, and for one nothing covers, why does it need none?",
|
|
@@ -374,11 +375,11 @@ export const DEFECTS_CONTRACT = {
|
|
|
374
375
|
},
|
|
375
376
|
untested: {
|
|
376
377
|
message: "This declared defect has no test in the plan.",
|
|
377
|
-
suggestion: "Add a planned test that cites the defect in `defects` and sets `expected.outcome` to `fail`. A defect in a file this pull request touches gets the test whether or not this pull request introduced it. If an existing test already proves it and this run edits that test rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this defect's id — maintenance is proof, and a duplicate spec beside the one you edited is the mistake.
|
|
378
|
+
suggestion: "Add a planned test that cites the defect in `defects` and sets `expected.outcome` to `fail`. A defect in a file this pull request touches gets the test whether or not this pull request introduced it. If an existing test already proves it and this run edits that test, or runs it unchanged as a VERIFY, rather than writing a new one, declare it in `maintains` with the file, what your edit changes, and this defect's id — maintenance is proof, and a duplicate spec beside the one you edited is the mistake. Only a test closes this objection. If a service the test needs is down, write the test anyway: it is committed and runs once the service is back.",
|
|
378
379
|
},
|
|
379
380
|
expectedPass: {
|
|
380
381
|
message: "Every test that cites this defect expects to pass, and a test that passes today proves nothing about a defect.",
|
|
381
|
-
suggestion: "Assert what the fixed code returns
|
|
382
|
+
suggestion: "Assert what the fixed code returns and set `expected.outcome` to `fail` with `why`. Only a test that expects to fail closes this objection.",
|
|
382
383
|
},
|
|
383
384
|
unknownId: {
|
|
384
385
|
message: "A planned test cites a defect id that this plan does not declare.",
|
|
@@ -3,8 +3,8 @@ import { PlanMaintenance, VerifyContext } from "../types.js";
|
|
|
3
3
|
/** Whether a `maintains` entry names a file that stays inside the repository and is
|
|
4
4
|
* present in the repository the entry names.
|
|
5
5
|
*
|
|
6
|
-
* An entry credits its ids only when it is. `coverage:change:`
|
|
7
|
-
* `defects:untested:`
|
|
6
|
+
* An entry credits its ids only when it is. `coverage:change:` closes only with a
|
|
7
|
+
* blocker and `defects:untested:` only with a test, so crediting a file nobody can
|
|
8
8
|
* open would route around both. */
|
|
9
9
|
export declare function maintainedFileIsInItsRepository(entry: PlanMaintenance, ctx: VerifyContext): boolean;
|
|
10
10
|
/** A cited path reduced to the spelling `git diff --name-status` produces: `./`,
|
|
@@ -3,8 +3,8 @@ import { escapesRepo } from "./endpointGrounded.js";
|
|
|
3
3
|
/** Whether a `maintains` entry names a file that stays inside the repository and is
|
|
4
4
|
* present in the repository the entry names.
|
|
5
5
|
*
|
|
6
|
-
* An entry credits its ids only when it is. `coverage:change:`
|
|
7
|
-
* `defects:untested:`
|
|
6
|
+
* An entry credits its ids only when it is. `coverage:change:` closes only with a
|
|
7
|
+
* blocker and `defects:untested:` only with a test, so crediting a file nobody can
|
|
8
8
|
* open would route around both. */
|
|
9
9
|
export function maintainedFileIsInItsRepository(entry, ctx) {
|
|
10
10
|
const path = entry.file.trim();
|
|
@@ -744,27 +744,24 @@ const REFUSING_VERBS = new Set([
|
|
|
744
744
|
"assert",
|
|
745
745
|
]);
|
|
746
746
|
/** The text that can name a value on the page: the steps of the UI tests whose verb
|
|
747
|
-
* is in `verbs
|
|
748
|
-
*
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
...maintained.map((entry) => entry.differsBy),
|
|
755
|
-
];
|
|
747
|
+
* is in `verbs`. A maintained spec's edit note is prose with no verb to read: an
|
|
748
|
+
* XR02 note named Principal in "4 radios (incl. Principal) render" and Intern in
|
|
749
|
+
* "switches from the removed Intern radio", and neither chose or refused it. */
|
|
750
|
+
function pageTexts(uiTests, verbs) {
|
|
751
|
+
return uiTests.flatMap((plannedTest) => plannedTest.scenario.steps
|
|
752
|
+
.filter((step) => verbs.has(normalizeStepMethod(step.method) ?? ""))
|
|
753
|
+
.flatMap((step) => [step.path, step.description ?? ""]));
|
|
756
754
|
}
|
|
757
755
|
/** A change on the page AND the API needs each accepted value chosen on the page,
|
|
758
756
|
* and each refused value tried or shown absent there, not only sent by an API
|
|
759
757
|
* test. `surfaceObjections` passes the page once any UI test cites the change,
|
|
760
758
|
* whatever value it picks. A UI step is free text (`Senior radio`), so the check
|
|
761
759
|
* reads whether a page step of a citing ui or e2e test that chooses the value (or,
|
|
762
|
-
* for a refused one, asserts it)
|
|
763
|
-
* names the value.
|
|
760
|
+
* for a refused one, asserts it) names the value.
|
|
764
761
|
*
|
|
765
|
-
*
|
|
766
|
-
* sentence: in
|
|
767
|
-
* prose, and
|
|
762
|
+
* Either value takes a UI test step or a blocker, never a
|
|
763
|
+
* sentence: in XR02 runs the page objection for a removed option and for an added
|
|
764
|
+
* one each closed by prose, and no generated test checked that value on the page. */
|
|
768
765
|
function surfaceCaseObjections(registration, ctx) {
|
|
769
766
|
const objections = [];
|
|
770
767
|
const seen = new Set();
|
|
@@ -777,15 +774,15 @@ function surfaceCaseObjections(registration, ctx) {
|
|
|
777
774
|
const uiTests = registration.plannedTests.filter((plannedTest) => citesChange(plannedTest, id) &&
|
|
778
775
|
pageTypes.has(plannedTest.scenario.testType.toLowerCase()));
|
|
779
776
|
// A maintained spec counts only when its file resolves, the bar
|
|
780
|
-
// `citedBehaviors` sets
|
|
781
|
-
//
|
|
777
|
+
// `citedBehaviors` sets. It decides whether the values are checked, never
|
|
778
|
+
// whether one is met.
|
|
782
779
|
const maintained = (registration.maintains ?? []).filter((entry) => entry.changes.some((cited) => cited.trim() === id) &&
|
|
783
780
|
maintainedFileIsInItsRepository(entry, ctx));
|
|
784
781
|
const citedAtAll = maintained.length > 0 ||
|
|
785
782
|
registration.plannedTests.some((plannedTest) => citesChange(plannedTest, id));
|
|
786
783
|
const texts = {
|
|
787
|
-
surfaceCase: pageTexts(uiTests,
|
|
788
|
-
surfaceReject: pageTexts(uiTests,
|
|
784
|
+
surfaceCase: pageTexts(uiTests, CHOOSING_VERBS),
|
|
785
|
+
surfaceReject: pageTexts(uiTests, REFUSING_VERBS),
|
|
789
786
|
};
|
|
790
787
|
for (const testCase of change.testCases ?? []) {
|
|
791
788
|
const kind = pageObjectionKind(testCase);
|
|
@@ -817,19 +814,22 @@ function surfaceCaseObjections(registration, ctx) {
|
|
|
817
814
|
if (seen.has(key))
|
|
818
815
|
continue;
|
|
819
816
|
seen.add(key);
|
|
820
|
-
const citers =
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
817
|
+
const citers = uiTests.map((plannedTest) => plannedTest.plannedTestId);
|
|
818
|
+
// A maintained spec has no steps to name the value, so say its note is not
|
|
819
|
+
// read rather than fault it for a step it cannot carry.
|
|
820
|
+
const notes = maintained.length > 0
|
|
821
|
+
? ` The edit note of ${maintained.map((entry) => entry.file).join(", ")} is not read.`
|
|
822
|
+
: "";
|
|
824
823
|
objections.push({
|
|
825
824
|
objectionId,
|
|
826
825
|
verifier: "coverage",
|
|
827
826
|
message: fillPlaceholders(COVERAGE_CONTRACT.objections[kind].message, {
|
|
828
827
|
case: rendered,
|
|
829
828
|
}),
|
|
830
|
-
evidence: citers.length === 0
|
|
831
|
-
? `No ui or e2e test
|
|
832
|
-
: `${citers.join(", ")} cite ${id}; no step
|
|
829
|
+
evidence: (citers.length === 0
|
|
830
|
+
? `No ui or e2e test cites ${id}.`
|
|
831
|
+
: `${citers.join(", ")} cite ${id}; no page step of theirs names ${word}.`) +
|
|
832
|
+
notes,
|
|
833
833
|
suggestion: COVERAGE_CONTRACT.objections[kind].suggestion,
|
|
834
834
|
});
|
|
835
835
|
}
|
|
@@ -81,9 +81,9 @@ export function checkMaintenanceDelivered(plan, maintained) {
|
|
|
81
81
|
objections.push({
|
|
82
82
|
objectionId,
|
|
83
83
|
verifier: "deliveredMatchesPlan",
|
|
84
|
-
message: "The plan says this existing test covers a change, and no maintenance row says it was edited.",
|
|
85
|
-
evidence: `plan maintains ${file}; the maintenance rows that
|
|
86
|
-
suggestion:
|
|
84
|
+
message: "The plan says this existing test covers a change, and no maintenance row says it was edited or run unchanged after the edits.",
|
|
85
|
+
evidence: `plan maintains ${file}; the maintenance rows that edited or ran a file name ${editedPaths.length > 0 ? editedPaths.join(", ") : "no file"}`,
|
|
86
|
+
suggestion: 'The maintenance rows come from the triage, not from this call, so this is not a field to fill in here: make the edit on the file you claimed and let the maintenance step record it, run a file you marked VERIFY with `skyramp_execute_test` and `phase: "after"`, or drop the entry from `maintains` and register the plan again — coverage counted the change because the plan said you were editing that test. If the test already covered the change and needed no edit, or the edit landed somewhere the rows do not name, say which as the answer.',
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
return objections;
|
|
@@ -46,11 +46,7 @@ export interface TraceabilityLane {
|
|
|
46
46
|
* accepted that entry — the same claim a planned test makes. See
|
|
47
47
|
* {@link maintainedDefects} for how that acceptance is read.
|
|
48
48
|
* `deliveredMatchesPlan:maintains:` checks separately that the edit landed.
|
|
49
|
-
|
|
50
|
-
* A declared `defectId` also counts when the agent answered `defects:untested:<id>`
|
|
51
|
-
* at plan time (or closed it with a blocker). The plan-time verifier asked why no
|
|
52
|
-
* test proves that defect and the agent said; asking again here is the same
|
|
53
|
-
* question twice, and the answer is already published with the plan. */
|
|
49
|
+
*/
|
|
54
50
|
export declare function checkIssueTraceability(plan: Plan, delivered: DeliveredForTraceability[], issues: ReportedIssueForTraceability[], lane?: TraceabilityLane): Objection[];
|
|
55
51
|
/** Report-time. Every defect the plan declared is an `issuesFound` entry that
|
|
56
52
|
* names it by `defectId`. The review found it; the report must show it. */
|