@skyramp/mcp 0.4.0-rc.2 → 0.4.1-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/prompts/enhance-assertions/sharedAssertionRules.js +20 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +4 -5
- package/build/prompts/testbot/testbot-prompts.js +10 -8
- package/build/recommendation/answers.d.ts +11 -7
- package/build/recommendation/answers.js +14 -10
- package/build/recommendation/pullRequestText.d.ts +18 -0
- package/build/recommendation/pullRequestText.js +31 -0
- package/build/recommendation/registerPlan.d.ts +5 -1
- package/build/recommendation/registerPlan.js +3 -1
- package/build/recommendation/runVerifiers.js +6 -0
- package/build/recommendation/types.d.ts +35 -0
- package/build/recommendation/verifierContracts.d.ts +94 -11
- package/build/recommendation/verifierContracts.js +129 -27
- package/build/recommendation/verifiers/defects.d.ts +9 -0
- package/build/recommendation/verifiers/defects.js +117 -0
- package/build/recommendation/verifiers/expectedValueSourced.d.ts +14 -0
- package/build/recommendation/verifiers/expectedValueSourced.js +149 -0
- package/build/recommendation/verifiers/issueTraceability.d.ts +52 -0
- package/build/recommendation/verifiers/issueTraceability.js +197 -0
- package/build/recommendation/verifiers/requirementSourced.d.ts +2 -0
- package/build/recommendation/verifiers/requirementSourced.js +168 -0
- package/build/tools/submitReportTool.js +19 -3
- package/build/tools/test-management/registerTestPlanTool.d.ts +31 -17
- package/build/tools/test-management/registerTestPlanTool.js +79 -5
- package/build/types/TestbotReport.d.ts +7 -3
- package/build/utils/assertion-verify/api-shared-lints.js +70 -0
- package/package.json +1 -1
- package/plugin/prompts/generate-tests/execution-plan.md +2 -2
- package/plugin/prompts/plan-tests.md +18 -9
- package/plugin/prompts/testbot-task1.md +3 -9
- package/build/prompts/testbot/planDeclarations.d.ts +0 -6
- package/build/prompts/testbot/planDeclarations.js +0 -9
- package/plugin/prompts/declaring-a-plan.md +0 -20
|
@@ -28,12 +28,13 @@ expect(response.statusCode).toBe(201);`,
|
|
|
28
28
|
],
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
|
-
title: "Error path (HTTP 4xx/5xx
|
|
32
|
-
description: "For every
|
|
31
|
+
title: "Error path (HTTP 4xx/5xx, or a 2xx carrying only an error body)",
|
|
32
|
+
description: "For every error response that includes a body, assert every error body field with its exact value — including `error.code`, `error.message`, `detail`, `errors[0].message`, and `errors[0].extensions.code`. An error response is any 4xx/5xx with a body, and any 2xx whose body is an error object (`error`, `message`, `errors[]`) with no resource payload.",
|
|
33
33
|
subPoints: [
|
|
34
34
|
"Also apply the array-validation rule to any errors array.",
|
|
35
35
|
"Asserting only the status code is never sufficient when a body is present.",
|
|
36
36
|
"For no-body responses such as a successful DELETE (204): assert the status code only.",
|
|
37
|
+
"For a 2xx error body, assert the exact status code, the error field's exact value, and that the resource fields carry no value (`toBeNull()` / `is None` — the SDK helper reads a missing path and an explicit null the same way, so this asserts \"no payload\", not \"key absent\"; use `checkSchema` when the two must be told apart). Never assert the success shape (a non-empty collection, `error` absent) against a response the recorded trace or `expected_response_body` shows returning an error or no-match body — classify the response by what this request returns, not by the outcome the test was meant to check for. The one exception is a test the plan declared `expected.outcome: fail` (typically a `bug_caught` or `requirement_conflict` candidate; the category is only the fallback when nothing was declared): it asserts the intended behaviour established by the diff, the PR statement, or the code's evident intent — keep that assertion and let the test fail.",
|
|
37
38
|
],
|
|
38
39
|
examples: [
|
|
39
40
|
{
|
|
@@ -43,6 +44,20 @@ expect(getValue(response, "errors.0.message")).toBe("Item not found");
|
|
|
43
44
|
expect(getValue(response, "errors.0.extensions.code")).toBe("RECORD_NOT_FOUND");
|
|
44
45
|
expect(getValue(response, "errors.1")).toBeNull();`,
|
|
45
46
|
},
|
|
47
|
+
{
|
|
48
|
+
language: "javascript",
|
|
49
|
+
code: `// Recorded response is a 200 carrying an error object and no payload
|
|
50
|
+
expect(searchPostResponse.statusCode, 'status code').toBe(200);
|
|
51
|
+
expect(getResponseValue(searchPostResponse, "error"), 'error').toBe("No items matched the criteria.");
|
|
52
|
+
expect(getResponseValue(searchPostResponse, "results"), 'results carries no value').toBeNull();`,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
language: "python",
|
|
56
|
+
code: `# Recorded response: 400 {"error": "level must be one of Intern, Junior, Senior"}
|
|
57
|
+
assert record_post_response.status_code == 400
|
|
58
|
+
assert skyramp.get_response_value(record_post_response, "error") == "level must be one of Intern, Junior, Senior"
|
|
59
|
+
assert skyramp.get_response_value(record_post_response, "insertedId") is None`,
|
|
60
|
+
},
|
|
46
61
|
],
|
|
47
62
|
},
|
|
48
63
|
{
|
|
@@ -77,7 +92,7 @@ expect(getValue(response, "errors.1")).toBeNull();`,
|
|
|
77
92
|
"When the response is a non-empty array: assert the exact length, key fields on each item, and that the index after the last item is absent.",
|
|
78
93
|
"Shape-only checks such as `Array.isArray` or `typeof` are not sufficient when the response contains actual values.",
|
|
79
94
|
"If the response is sorted or ordered: assert the ordering direction across the first two items.",
|
|
80
|
-
"When the response is an empty or minimal body (an empty object, empty array, null, or only a few keys): assert the empty or minimal shape and the absence of error fields — do not stop at the status code.",
|
|
95
|
+
"When the response is an empty or minimal body (an empty object, empty array, null, or only a few keys): assert the empty or minimal shape and the absence of error fields — do not stop at the status code. A minimal body that is itself an error object falls under the Error path rule instead: assert its error field exactly.",
|
|
81
96
|
"When the request includes pagination or filter parameters, assert the response reflects them.",
|
|
82
97
|
],
|
|
83
98
|
examples: [
|
|
@@ -207,7 +222,8 @@ Before editing the given file, you must output a \`<thinking>\` block. The aim o
|
|
|
207
222
|
2. Classify each response first by its response status type and then assign the applicable assertion rules to the response.
|
|
208
223
|
1. Success with body (2xx with a response body): all assertion rules below may apply — echo-back of request fields, computed response fields, array / items validation, and chained values across steps.
|
|
209
224
|
2. Success with no body (200/202/204 with an empty body — e.g. logout/cancel/submit action endpoints): assert the status code only. Also apply chained-values rules if a follow-up step uses this response's ID.
|
|
210
|
-
3. Error response (4xx/5xx with a body): assert every error body field with its exact value plus array / items validation on the \`errors[]\` array (exact length + per-item fields + next index null). Status code alone is never sufficient when a body is present — for example, also assert \`errors.0.extensions.code == 'INVALID_PAYLOAD'\` and that \`errors.1\` is null.
|
|
225
|
+
3. Error response (4xx/5xx with a body, or any status whose body is an error object such as \`{"error": "..."}\` with no resource payload): assert every error body field with its exact value plus array / items validation on the \`errors[]\` array (exact length + per-item fields + next index null). Status code alone is never sufficient when a body is present — for example, also assert \`errors.0.extensions.code == 'INVALID_PAYLOAD'\` and that \`errors.1\` is null.
|
|
226
|
+
Classify by the response the recorded trace or \`expected_response_body\` shows this request returning. The one exception is a test the plan declared \`expected.outcome: fail\` (typically a \`bug_caught\` or \`requirement_conflict\` candidate; read the declaration first and fall back to the category only when none was made): there the expected response is the intended behaviour established by the diff, the PR statement, or the code's evident intent, and the test is expected to fail until the code is fixed.
|
|
211
227
|
3. For each in-scope response, output one JSON object using the template below. The output is an array — one object per in-scope response.
|
|
212
228
|
- \`step\`: the HTTP method, path, and response variable name for this request (e.g. \`POST /products → products_POST_response\`).
|
|
213
229
|
- \`response_status\`: one of \`success\`, \`no_body\`, or \`error\` based on the classification in step 2.
|
|
@@ -4,7 +4,6 @@ import { logger } from "../../utils/logger.js";
|
|
|
4
4
|
import { buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildToolWorkflows, buildVerificationChecklist, } from "./recommendationSections.js";
|
|
5
5
|
import { buildExecutionPlan, EXEC_STEP_ENRICH } from "./diffExecutionPlan.js";
|
|
6
6
|
import { readPromptAsset } from "../promptAssets.js";
|
|
7
|
-
import { renderPlanDeclarationGuidance } from "../testbot/planDeclarations.js";
|
|
8
7
|
import { TASK_GENERATE, taskRef, } from "./recommendationShared.js";
|
|
9
8
|
// Re-export for backward compatibility (tests and external callers import this from this module)
|
|
10
9
|
function formatTestLocations(locs) {
|
|
@@ -22,11 +21,11 @@ function formatTestLocations(locs) {
|
|
|
22
21
|
// on subsequent testbot runs on the same PR.
|
|
23
22
|
const SKYRAMP_TEST_FILE_PATTERN = /(?:_test|_smoke|_contract|_fuzz|_integration|_load|_e2e|_ui)\.[^/]+$|scenario_[^/]+\.json$/;
|
|
24
23
|
/** How this prompt tells the agent to plan: the whole procedure, every time.
|
|
25
|
-
* Rendered from `plugin/prompts/plan-tests.md`
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* Rendered from `plugin/prompts/plan-tests.md` rather than retyped, so there is
|
|
25
|
+
* one text and nothing to drift from. `isDiffScope` is unused here — the
|
|
26
|
+
* procedure reads the same for a diff and for a repository. */
|
|
28
27
|
function planningInstruction(_isDiffScope) {
|
|
29
|
-
return
|
|
28
|
+
return readPromptAsset("plan-tests.md").trim();
|
|
30
29
|
}
|
|
31
30
|
export function buildRecommendationPrompt(analysis, analysisScope = AnalysisScope.FullRepo, prContext, workspaceAuthHeader, workspaceAuthType, workspaceAuthScheme, sessionId) {
|
|
32
31
|
const isDiffScope = isDiff(analysisScope);
|
|
@@ -4,10 +4,10 @@ import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
|
4
4
|
import { buildPathParamGuidance, } from "../test-recommendation/recommendationSections.js";
|
|
5
5
|
import { setReportLanguage } from "../../utils/reportLanguage.js";
|
|
6
6
|
import { setPlanOnlyMode } from "../../utils/planOnlyMode.js";
|
|
7
|
+
import { recordPullRequestText } from "../../recommendation/pullRequestText.js";
|
|
7
8
|
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";
|
|
8
9
|
import { getTraceRecordingPromptText } from "../../playwright/traceRecordingPrompt.js";
|
|
9
10
|
import { isContractConsumerModeEnabled, isPomReuseEnabled, isUtilsReuseEnabled, isSkillsLoaded, } from "../../utils/featureFlags.js";
|
|
10
|
-
import { renderPlanDeclarationGuidance } from "./planDeclarations.js";
|
|
11
11
|
import { fixErrorsInstruction } from "../../skills/fixTestImportErrorsSkill.js";
|
|
12
12
|
import { resolveServiceDetailsRef } from "../../utils/utils.js";
|
|
13
13
|
import { section, sectionBody } from "../promptAssets.js";
|
|
@@ -59,12 +59,12 @@ const PLAN_TESTS_MD = readPromptAsset("plan-tests.md").trim();
|
|
|
59
59
|
// rendered neither it nor a copy, so the agent planned without the claims step and
|
|
60
60
|
// without the backward surface map. Rendered here, never retyped.
|
|
61
61
|
//
|
|
62
|
-
// It sits OUTSIDE Task 1: the procedure is
|
|
62
|
+
// It sits OUTSIDE Task 1: the procedure is a twelve-item numbered list, Task 1's
|
|
63
63
|
// own steps are numbered too, and nested inside Task 1 the two share one ordinal
|
|
64
64
|
// space (`referenceIntegrity.test.ts` reads every `N.` there as a Task 1 step).
|
|
65
65
|
//
|
|
66
|
-
// Two steps run elsewhere in this lane — step
|
|
67
|
-
// tool step, step
|
|
66
|
+
// Two steps run elsewhere in this lane — step 7's maintenance update is Task 1's
|
|
67
|
+
// tool step, step 10's registration happens in Task 2 after it. The note states that
|
|
68
68
|
// order; the markdown stays the source.
|
|
69
69
|
//
|
|
70
70
|
// The count sentence sits HERE, beside the register instruction, because the agent
|
|
@@ -173,6 +173,8 @@ export function parseRelatedRepositories(raw) {
|
|
|
173
173
|
}
|
|
174
174
|
export function getTestbotPrompt(opts) {
|
|
175
175
|
const { prTitle, prDescription, repositoryPath, baseBranch, prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo, planOnly = false, language, } = opts;
|
|
176
|
+
// Kept in the process for the plan tool, which is registered on this server.
|
|
177
|
+
recordPullRequestText(prTitle, prDescription);
|
|
176
178
|
// The legacy count arguments are DECLARED NOWHERE, so zod strips them and their
|
|
177
179
|
// names never reach the agent — not in a schema it lists, not in rendered text.
|
|
178
180
|
// A number in front of the agent reads as a ceiling whatever the words around it
|
|
@@ -357,7 +359,7 @@ ${uiGroundingBlock}
|
|
|
357
359
|
This is a plan-only evaluation run: the application under test is NOT running, and this run evaluates test SELECTION only. Nothing is generated or executed in this task.
|
|
358
360
|
|
|
359
361
|
- Draft your complete plannedTest list — every test you would generate OR recommend for this PR, grounded in the analysis output and the diff. Favor tests that would FAIL if the changed logic were buggy, not just tests that exercise the new surface.
|
|
360
|
-
- Register that list through \`skyramp_register_test_plan\` as
|
|
362
|
+
- Register that list through \`skyramp_register_test_plan\` as the planning procedure above describes. What you register is the plan — nothing re-ranks or trims it.
|
|
361
363
|
- Include UI and E2E planned tests on the same footing as the API types. The app is not running, so you cannot capture a blueprint — plan them ungrounded rather than drop them. For every UI plannedTest, set \`elements.items\` to null and set \`elements.pageUrl\` to the route path the test would visit, read out of the source: \`/orders/1\`, not a full URL, because no host is serving the app. Set \`screenEvidence.file\` to the changed frontend file that renders that route — the app not running does not excuse this one, because you read the file out of the diff. Describe the page or feature the test would exercise; do not name an element you have not seen. This lane is the one case where you fall back without attempting a capture first, so do NOT log the fallback in \`issuesFound\` — capture was never applicable here, and nothing failed.
|
|
362
364
|
- Take no other actions in this task: no test generation tools, no browser traces or blueprint captures, no test files written, no test executions. Proceed directly to ${taskRef(TASK_SUBMIT)}.`;
|
|
363
365
|
}
|
|
@@ -573,7 +575,7 @@ ${FINISH_CHECKS_BLOCK}
|
|
|
573
575
|
// to name the changed file it is about, and cite the file it sits
|
|
574
576
|
// in, and cite `routes` from the checkout, and all of those happen while
|
|
575
577
|
// it analyzes in Task 1.
|
|
576
|
-
const planningBlock = `\n${planProcedureBlock}\n
|
|
578
|
+
const planningBlock = `\n${planProcedureBlock}\n`;
|
|
577
579
|
return `<TITLE>${prTitle}</TITLE>
|
|
578
580
|
<DESCRIPTION>${prDescription}</DESCRIPTION>
|
|
579
581
|
${primaryRepoBlock}<REPOSITORY PATH>${repositoryPath}</REPOSITORY PATH>
|
|
@@ -603,9 +605,9 @@ In these cases:
|
|
|
603
605
|
- \`issuesFound\` must be \`[]\` — do NOT add a "No testable behavioral surface" entry; the business case already explains the abstention
|
|
604
606
|
- \`businessCaseAnalysis\` must be a one-sentence summary of what the PR actually does (do NOT leave it blank)
|
|
605
607
|
|
|
606
|
-
**This zero-test path does NOT apply when the requirement check
|
|
608
|
+
**This zero-test path does NOT apply when the requirement check found a \`requirement_conflict\`.** A requirement the PR states and the diff did not implement is testable surface, whatever the diff otherwise contains. Report the conflict in \`issuesFound\` at severity \`high\` or above, and carry the failing \`requirement_conflict\` test in \`newTestsCreated\`. Where the requirement is observable nowhere you can reach, say that in \`businessCaseAnalysis\`. Abstaining there hands the author back their own code as though it were the requirement.
|
|
607
609
|
|
|
608
|
-
${task3CountRule ? `${task3CountRule}\n\n` : ""}${reportLanguageBlock}Call \`skyramp_submit_report\`. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
|
|
610
|
+
${task3CountRule ? `${task3CountRule}\n\n` : ""}${reportLanguageBlock}Call \`skyramp_submit_report\`. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly. A report check objects to an \`issuesFound\` entry with \`category: bug\` that no delivered test proves. The \`plannedTestId\` and \`defectId\` descriptions in the schema say what closes it. Answer an objection in one line.
|
|
609
611
|
|
|
610
612
|
${hasRelatedRepos
|
|
611
613
|
? `
|
|
@@ -19,13 +19,17 @@ export declare function unknownAnswerObjections(objections: Objection[], acknowl
|
|
|
19
19
|
* leave the bug. `verifierContracts.ts` carries the evidence. */
|
|
20
20
|
export declare const NON_ANSWERABLE_PREFIX = "coverage:stateTest:";
|
|
21
21
|
export declare function isNonAnswerable(objectionId: unknown): boolean;
|
|
22
|
-
/** The
|
|
23
|
-
* needs the test,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
|
|
22
|
+
/** The objections a sentence alone does not close either. A change with no test
|
|
23
|
+
* needs the test, and a defect with no test needs the test — a defect the code
|
|
24
|
+
* already had before this pull request included. Either one closes only when the
|
|
25
|
+
* answer also names what stopped the test: a service that is not running, a
|
|
26
|
+
* branch that no longer exists, the one credential the run holds. Measured on run
|
|
27
|
+
* 34176038240: across eight fixtures 45 objections were raised and 45 were
|
|
28
|
+
* closed, and the one class that a sentence cannot close was never raised at all.
|
|
29
|
+
* Prose closed every uncovered change, including changes nothing prevented a test
|
|
30
|
+
* from reaching. Runs 34423214322 and 34423217796 then lost three defects the
|
|
31
|
+
* baseline reported: each was listed and closed by a sentence. */
|
|
32
|
+
export declare const BLOCKER_ONLY_PREFIXES: readonly ["coverage:change:", "defects:untested:"];
|
|
29
33
|
export declare function needsBlocker(objectionId: unknown): boolean;
|
|
30
34
|
/** The objection raised below carries this prefix and the id it objects to. */
|
|
31
35
|
export declare const REFUSED_ANSWER_PREFIX = "answers:refused:";
|
|
@@ -49,15 +49,19 @@ export const NON_ANSWERABLE_PREFIX = "coverage:stateTest:";
|
|
|
49
49
|
export function isNonAnswerable(objectionId) {
|
|
50
50
|
return typeof objectionId === "string" && objectionId.startsWith(NON_ANSWERABLE_PREFIX);
|
|
51
51
|
}
|
|
52
|
-
/** The
|
|
53
|
-
* needs the test,
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
52
|
+
/** The objections a sentence alone does not close either. A change with no test
|
|
53
|
+
* needs the test, and a defect with no test needs the test — a defect the code
|
|
54
|
+
* already had before this pull request included. Either one closes only when the
|
|
55
|
+
* answer also names what stopped the test: a service that is not running, a
|
|
56
|
+
* branch that no longer exists, the one credential the run holds. Measured on run
|
|
57
|
+
* 34176038240: across eight fixtures 45 objections were raised and 45 were
|
|
58
|
+
* closed, and the one class that a sentence cannot close was never raised at all.
|
|
59
|
+
* Prose closed every uncovered change, including changes nothing prevented a test
|
|
60
|
+
* from reaching. Runs 34423214322 and 34423217796 then lost three defects the
|
|
61
|
+
* baseline reported: each was listed and closed by a sentence. */
|
|
62
|
+
export const BLOCKER_ONLY_PREFIXES = ["coverage:change:", "defects:untested:"];
|
|
59
63
|
export function needsBlocker(objectionId) {
|
|
60
|
-
return typeof objectionId === "string" && objectionId.startsWith(
|
|
64
|
+
return typeof objectionId === "string" && BLOCKER_ONLY_PREFIXES.some((prefix) => objectionId.startsWith(prefix));
|
|
61
65
|
}
|
|
62
66
|
/** The objection raised below carries this prefix and the id it objects to. */
|
|
63
67
|
export const REFUSED_ANSWER_PREFIX = "answers:refused:";
|
|
@@ -84,11 +88,11 @@ export function refusedAnswerObjections(objections, acknowledged) {
|
|
|
84
88
|
objectionId: `${REFUSED_ANSWER_PREFIX}${id}`,
|
|
85
89
|
verifier: "answers",
|
|
86
90
|
message: needsBlocker(id)
|
|
87
|
-
? "The answer says why
|
|
91
|
+
? "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."
|
|
88
92
|
: "This objection is closed only by a planned test, so the answer closed nothing and the objection stays open.",
|
|
89
93
|
evidence: `answered "${id}"${needsBlocker(id) ? " with no `blocker`" : ", which no answer closes"}`,
|
|
90
94
|
suggestion: needsBlocker(id)
|
|
91
|
-
? "Plan
|
|
95
|
+
? "Plan the test and register the plan again. If this run cannot write one, send the same answer with `blocker` naming what stopped it — a service that is not running, a paired branch that no longer exists, the one credential the run holds. A reason it is not worth testing is not a blocker."
|
|
92
96
|
: "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.",
|
|
93
97
|
});
|
|
94
98
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Called by every entry point that renders the testbot prompt, with the values it
|
|
2
|
+
* renders. */
|
|
3
|
+
export declare function recordPullRequestText(title: unknown, description: unknown): void;
|
|
4
|
+
/** UNDEFINED when this process never rendered the prompt — a caller that drives the
|
|
5
|
+
* tools directly, or a test. A check that reads the pull request stays silent then;
|
|
6
|
+
* it cannot tell an empty pull request from one it never saw. */
|
|
7
|
+
export declare function pullRequestText(): {
|
|
8
|
+
title: string;
|
|
9
|
+
description: string;
|
|
10
|
+
} | undefined;
|
|
11
|
+
export declare function clearPullRequestText(): void;
|
|
12
|
+
/** The pull request as one searchable string. Whitespace runs fold to one space, so
|
|
13
|
+
* a quote wrapped across lines still matches the sentence it was taken from. */
|
|
14
|
+
export declare const searchable: (pr: {
|
|
15
|
+
title: string;
|
|
16
|
+
description: string;
|
|
17
|
+
}) => string;
|
|
18
|
+
export declare const appearsIn: (haystack: string, needle: string) => boolean;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** The pull request's own title and description, as the run rendered its prompt with.
|
|
2
|
+
*
|
|
3
|
+
* IN PROCESS, never the state file: the prompt and the plan tool are registered on
|
|
4
|
+
* the same server, so the text is already here, and writing it would put the pull
|
|
5
|
+
* request's prose on disk once per registration for a reader in the same process.
|
|
6
|
+
*
|
|
7
|
+
* ONE SLOT, last write wins. A run renders the prompt once, and a second render is
|
|
8
|
+
* a second run whose text replaces the first. */
|
|
9
|
+
let recorded;
|
|
10
|
+
const asText = (value) => (typeof value === "string" ? value : "");
|
|
11
|
+
/** Called by every entry point that renders the testbot prompt, with the values it
|
|
12
|
+
* renders. */
|
|
13
|
+
export function recordPullRequestText(title, description) {
|
|
14
|
+
recorded = { title: asText(title), description: asText(description) };
|
|
15
|
+
}
|
|
16
|
+
/** UNDEFINED when this process never rendered the prompt — a caller that drives the
|
|
17
|
+
* tools directly, or a test. A check that reads the pull request stays silent then;
|
|
18
|
+
* it cannot tell an empty pull request from one it never saw. */
|
|
19
|
+
export function pullRequestText() {
|
|
20
|
+
return recorded;
|
|
21
|
+
}
|
|
22
|
+
export function clearPullRequestText() {
|
|
23
|
+
recorded = undefined;
|
|
24
|
+
}
|
|
25
|
+
/** The pull request as one searchable string. Whitespace runs fold to one space, so
|
|
26
|
+
* a quote wrapped across lines still matches the sentence it was taken from. */
|
|
27
|
+
export const searchable = (pr) => `${pr.title}\n${pr.description}`.replace(/\s+/g, " ").trim().toLowerCase();
|
|
28
|
+
export const appearsIn = (haystack, needle) => {
|
|
29
|
+
const folded = needle.replace(/\s+/g, " ").trim().toLowerCase();
|
|
30
|
+
return folded.length > 0 && haystack.includes(folded);
|
|
31
|
+
};
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { Objection, ObjectionAnswer, PlanChange, PlanInput, PlannedTest, VerifyContext } from "./types.js";
|
|
1
|
+
import { Objection, ObjectionAnswer, PlanChange, PlanDefect, PlanInput, 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 {
|
|
5
5
|
/** The changes the agent said the diff makes. Published in the report
|
|
6
6
|
* beside the tests delivered against each one. */
|
|
7
7
|
changes: PlanChange[];
|
|
8
|
+
/** The defects the code review found. skyramp_submit_report reads them to hold
|
|
9
|
+
* the report to the review: every one is an issue, and every issue names the
|
|
10
|
+
* test that proves it. */
|
|
11
|
+
defects: PlanDefect[];
|
|
8
12
|
plannedTests: PlannedTest[];
|
|
9
13
|
/** Objections the agent answered, with the answers. */
|
|
10
14
|
answeredObjections: Array<{
|
|
@@ -53,12 +53,13 @@ export function registerPlan(registration, ctx, previous) {
|
|
|
53
53
|
const acknowledged = Array.isArray(registration.answers) ? registration.answers : [];
|
|
54
54
|
const plannedTests = Array.isArray(registration.plannedTests) ? registration.plannedTests : [];
|
|
55
55
|
const changes = Array.isArray(registration.changes) ? registration.changes : [];
|
|
56
|
+
const defects = Array.isArray(registration.defects) ? registration.defects : [];
|
|
56
57
|
const registrationNumber = storedRegistrationNumber(registration);
|
|
57
58
|
// The verifiers see the same normalised fields the STORED plan gets, not the raw
|
|
58
59
|
// registration: handing them the raw one let a malformed top-level field crash
|
|
59
60
|
// every check at once, giving eight `:crashed` objections and no real ones.
|
|
60
61
|
// Crash containment is for a malformed field INSIDE a planned test.
|
|
61
|
-
const verified = runPlanTimeVerifiers({ ...registration, plannedTests, changes, answers: acknowledged }, ctx);
|
|
62
|
+
const verified = runPlanTimeVerifiers({ ...registration, plannedTests, changes, defects, answers: acknowledged }, ctx);
|
|
62
63
|
const objections = [
|
|
63
64
|
...verified,
|
|
64
65
|
...unknownAnswerObjections(verified, acknowledged, {
|
|
@@ -107,6 +108,7 @@ export function registerPlan(registration, ctx, previous) {
|
|
|
107
108
|
: {}),
|
|
108
109
|
plan: {
|
|
109
110
|
changes,
|
|
111
|
+
defects,
|
|
110
112
|
plannedTests,
|
|
111
113
|
answeredObjections,
|
|
112
114
|
unverifiedCloses,
|
|
@@ -6,7 +6,10 @@ import { expectedOutcomeAtPlanTime } from "./verifiers/expectedOutcome.js";
|
|
|
6
6
|
import { uiElementGrounded } from "./verifiers/uiElementGrounded.js";
|
|
7
7
|
import { screenRoute } from "./verifiers/screenRoute.js";
|
|
8
8
|
import { coverage } from "./verifiers/coverage.js";
|
|
9
|
+
import { expectedValueSourced } from "./verifiers/expectedValueSourced.js";
|
|
9
10
|
import { removedElementGuarded } from "./verifiers/removedElementGuarded.js";
|
|
11
|
+
import { requirementSourced } from "./verifiers/requirementSourced.js";
|
|
12
|
+
import { defects } from "./verifiers/defects.js";
|
|
10
13
|
/** Verifiers that need only the plan and the run's facts. The order is the order
|
|
11
14
|
* objections are reported in; it carries no priority. */
|
|
12
15
|
export const PLAN_TIME_VERIFIERS = [
|
|
@@ -18,7 +21,10 @@ export const PLAN_TIME_VERIFIERS = [
|
|
|
18
21
|
uiElementGrounded,
|
|
19
22
|
screenRoute,
|
|
20
23
|
coverage,
|
|
24
|
+
expectedValueSourced,
|
|
21
25
|
removedElementGuarded,
|
|
26
|
+
requirementSourced,
|
|
27
|
+
defects,
|
|
22
28
|
];
|
|
23
29
|
/** How a crashed verifier reads back. The id is the verifier's name plus a fixed
|
|
24
30
|
* word, so it stays stable across registrations and can be answered. */
|
|
@@ -23,6 +23,9 @@ export interface PlanChange {
|
|
|
23
23
|
id: string;
|
|
24
24
|
text: string;
|
|
25
25
|
source: string;
|
|
26
|
+
/** The requirement in the pull request's own words, when `source` names the pull
|
|
27
|
+
* request. Checked against the text the run rendered its prompt with. */
|
|
28
|
+
quote?: string;
|
|
26
29
|
/** Where a user or caller meets this change. Every change states it: an optional
|
|
27
30
|
* field the agent skips gives the check nothing to read. */
|
|
28
31
|
surfaces: Array<"api" | "page">;
|
|
@@ -34,8 +37,26 @@ export interface PlanChange {
|
|
|
34
37
|
value?: string | number | boolean | null;
|
|
35
38
|
absent?: true;
|
|
36
39
|
expect: "accept" | "reject";
|
|
40
|
+
/** What the response must carry when this case is sent. Declared here so the
|
|
41
|
+
* test asserts a value the plan decided, not the value the running app
|
|
42
|
+
* happened to return — the one source that always agrees with a defect. */
|
|
43
|
+
expectedValue?: string | number | boolean | null;
|
|
44
|
+
/** Where the agent read `expectedValue`: `pr-description`, `spec:<path>`,
|
|
45
|
+
* `convention:<file:line>`, or `code`. */
|
|
46
|
+
expectedFrom?: string;
|
|
37
47
|
}>;
|
|
38
48
|
}
|
|
49
|
+
/** One defect the code review found, as the agent read it. Nothing on the server
|
|
50
|
+
* parses the description or opens the file: the check reads the id, and a test
|
|
51
|
+
* that cites it and expects to fail is what proves it. */
|
|
52
|
+
export interface PlanDefect {
|
|
53
|
+
id: string;
|
|
54
|
+
file: string;
|
|
55
|
+
/** Advisory. Line numbers drift, so nothing checks it. */
|
|
56
|
+
line?: number;
|
|
57
|
+
description: string;
|
|
58
|
+
severity: "critical" | "high" | "medium" | "low";
|
|
59
|
+
}
|
|
39
60
|
/** What a planned test states so a verifier has a checkable change. Kept in step with
|
|
40
61
|
* `declarationFieldsSchema`, which the plan tool asserts against this type. */
|
|
41
62
|
export interface PlannedTestDetails {
|
|
@@ -74,6 +95,9 @@ export interface PlannedTestDetails {
|
|
|
74
95
|
};
|
|
75
96
|
/** The ids of the declared changes this test proves. */
|
|
76
97
|
changes?: string[];
|
|
98
|
+
/** The ids of the declared defects this test proves. A test proves a defect
|
|
99
|
+
* only when it expects to fail; the defects check reads both. */
|
|
100
|
+
defects?: string[];
|
|
77
101
|
/** The state the mutated record is in before the mutation. Presence only. */
|
|
78
102
|
startState?: string;
|
|
79
103
|
/** UI planned tests only. `items` entries are COPIES of elements a capture of
|
|
@@ -103,6 +127,10 @@ export interface ObjectionAnswer {
|
|
|
103
127
|
export interface PlanInput {
|
|
104
128
|
/** The changes the diff makes. Each planned test cites the ones it tests. */
|
|
105
129
|
changes: PlanChange[];
|
|
130
|
+
/** The defects the code review found. Each planned test cites the ones it
|
|
131
|
+
* proves. Empty is a statement — the review found none — and draws an
|
|
132
|
+
* objection the agent answers. */
|
|
133
|
+
defects: PlanDefect[];
|
|
106
134
|
plannedTests: PlannedTest[];
|
|
107
135
|
answers: ObjectionAnswer[];
|
|
108
136
|
/** The tool owns this and overwrites whatever the caller sends. */
|
|
@@ -141,6 +169,13 @@ export interface VerifyContext {
|
|
|
141
169
|
/** Identifiers a rename replaced, empty when the diff renames nothing. Only the
|
|
142
170
|
* server pairs the two sides of a diff. */
|
|
143
171
|
retiredUiElements: RemovedUiElement[];
|
|
172
|
+
/** The pull request's title and description, as the run rendered its prompt with.
|
|
173
|
+
* The plan tool fills it from the process the prompt was rendered in. Both fields
|
|
174
|
+
* blank MEANS the run had no title and no description. */
|
|
175
|
+
pullRequest: {
|
|
176
|
+
title: string;
|
|
177
|
+
description: string;
|
|
178
|
+
};
|
|
144
179
|
/** Whether a cited path names a file in any of the run's repositories. Injected
|
|
145
180
|
* to keep the verifiers filesystem-free. */
|
|
146
181
|
citedFileExists(relativePath: string): boolean;
|
|
@@ -17,8 +17,6 @@ export interface ContractObjection {
|
|
|
17
17
|
export interface VerifierContract {
|
|
18
18
|
/** The verifier's own `Verifier.name`. */
|
|
19
19
|
readonly id: string;
|
|
20
|
-
/** What the registered plan has to carry for the check to have anything to read. */
|
|
21
|
-
readonly declarationFields: readonly string[];
|
|
22
20
|
/** One entry per message the verifier can emit, keyed by the case. A key naming a
|
|
23
21
|
* segment of the `objectionId` uses that segment's spelling. The keys are widened
|
|
24
22
|
* here so every contract satisfies the interface, while each contract is `as
|
|
@@ -31,7 +29,6 @@ export interface VerifierContract {
|
|
|
31
29
|
}
|
|
32
30
|
export declare const CHANGED_FILE_CONTRACT: {
|
|
33
31
|
readonly id: "changedFile";
|
|
34
|
-
readonly declarationFields: readonly ["declarations.changedFile", "declarations.screenEvidence.file"];
|
|
35
32
|
readonly objections: {
|
|
36
33
|
readonly notStated: {
|
|
37
34
|
readonly message: "This test does not state which changed file it targets.";
|
|
@@ -54,7 +51,6 @@ export declare const CHANGED_FILE_CONTRACT: {
|
|
|
54
51
|
};
|
|
55
52
|
export declare const SCREEN_ROUTE_CONTRACT: {
|
|
56
53
|
readonly id: "screenRoute";
|
|
57
|
-
readonly declarationFields: readonly ["declarations.elements.pageUrl", "declarations.screenEvidence.file", "declarations.changedFile"];
|
|
58
54
|
readonly objections: {
|
|
59
55
|
readonly mismatch: {
|
|
60
56
|
readonly message: "The page this UI test opens is not a page that renders the changed file it is about.";
|
|
@@ -69,7 +65,6 @@ export declare const SCREEN_ROUTE_CONTRACT: {
|
|
|
69
65
|
};
|
|
70
66
|
export declare const ENDPOINT_GROUNDED_CONTRACT: {
|
|
71
67
|
readonly id: "endpointGrounded";
|
|
72
|
-
readonly declarationFields: readonly ["steps[]", "testType", "declarations.routes"];
|
|
73
68
|
readonly objections: {
|
|
74
69
|
readonly uncited: {
|
|
75
70
|
readonly message: "A call this test makes cites no file that declares it.";
|
|
@@ -88,7 +83,6 @@ export declare const ENDPOINT_GROUNDED_CONTRACT: {
|
|
|
88
83
|
};
|
|
89
84
|
export declare const STATED_DIFFERENCE_CONTRACT: {
|
|
90
85
|
readonly id: "statedDifference";
|
|
91
|
-
readonly declarationFields: readonly ["steps[]", "testType", "scenarioName", "declarations.differsFrom"];
|
|
92
86
|
readonly objections: {
|
|
93
87
|
readonly unexplainedPair: {
|
|
94
88
|
readonly message: "Another planned test in this plan tests the same endpoint and neither says how they differ.";
|
|
@@ -107,7 +101,6 @@ export declare const STATED_DIFFERENCE_CONTRACT: {
|
|
|
107
101
|
};
|
|
108
102
|
export declare const EXISTING_COVERAGE_CONTRACT: {
|
|
109
103
|
readonly id: "existingCoverage";
|
|
110
|
-
readonly declarationFields: readonly ["declarations.existingTests"];
|
|
111
104
|
readonly objections: {
|
|
112
105
|
readonly citedTestMissing: {
|
|
113
106
|
readonly message: "This planned test names an existing test file that is not in the checkout.";
|
|
@@ -118,7 +111,6 @@ export declare const EXISTING_COVERAGE_CONTRACT: {
|
|
|
118
111
|
};
|
|
119
112
|
export declare const EXPECTED_OUTCOME_CONTRACT: {
|
|
120
113
|
readonly id: "expectedOutcome";
|
|
121
|
-
readonly declarationFields: readonly ["category", "declarations.expected.outcome", "declarations.expected.why"];
|
|
122
114
|
readonly objections: {
|
|
123
115
|
readonly passInShouldFailCategory: {
|
|
124
116
|
readonly message: "This planned test is categorised \"{category}\" but expects to pass on the app as it stands.";
|
|
@@ -133,7 +125,6 @@ export declare const EXPECTED_OUTCOME_CONTRACT: {
|
|
|
133
125
|
};
|
|
134
126
|
export declare const UI_ELEMENT_GROUNDED_CONTRACT: {
|
|
135
127
|
readonly id: "uiElementGrounded";
|
|
136
|
-
readonly declarationFields: readonly ["testType", "steps[]", "declarations.elements.items", "declarations.elements.pageUrl", "declarations.asserts", "scenarioName", "description", "declarations.expected.why"];
|
|
137
128
|
readonly objections: {
|
|
138
129
|
readonly nogrounding: {
|
|
139
130
|
readonly message: "This UI test names no `elements.items`, so nothing says the elements it targets are on the page.";
|
|
@@ -156,7 +147,6 @@ export declare const UI_ELEMENT_GROUNDED_CONTRACT: {
|
|
|
156
147
|
};
|
|
157
148
|
export declare const COVERAGE_CONTRACT: {
|
|
158
149
|
readonly id: "coverage";
|
|
159
|
-
readonly declarationFields: readonly ["changes", "declarations.changes", "declarations.changedFile", "declarations.routes", "declarations.screenEvidence.file", "declarations.startState", "changes[].cases", "changes[].surfaces"];
|
|
160
150
|
readonly objections: {
|
|
161
151
|
readonly change: {
|
|
162
152
|
readonly message: "This declared change has no test in the plan.";
|
|
@@ -195,7 +185,6 @@ export declare const COVERAGE_CONTRACT: {
|
|
|
195
185
|
};
|
|
196
186
|
export declare const REMOVED_ELEMENT_GUARDED_CONTRACT: {
|
|
197
187
|
readonly id: "removedElementGuarded";
|
|
198
|
-
readonly declarationFields: readonly ["declarations.elements.items", "declarations.asserts"];
|
|
199
188
|
readonly objections: {
|
|
200
189
|
readonly missing: {
|
|
201
190
|
readonly message: "The diff removes `{element}` and no planned test says it is gone.";
|
|
@@ -208,6 +197,100 @@ export declare const REMOVED_ELEMENT_GUARDED_CONTRACT: {
|
|
|
208
197
|
};
|
|
209
198
|
readonly suggestion: "Does every element this diff removes have a planned test that asserts it is gone, and does no test claim a renamed identifier is gone?";
|
|
210
199
|
};
|
|
200
|
+
export declare const EXPECTED_VALUE_SOURCED_CONTRACT: {
|
|
201
|
+
readonly id: "expectedValueSourced";
|
|
202
|
+
readonly objections: {
|
|
203
|
+
readonly sourcedFromCode: {
|
|
204
|
+
readonly message: "This case expects a value it read from the code as it stands, so a defect in that code becomes what the test demands.";
|
|
205
|
+
readonly suggestion: "Read the value from the pull request title or description, from a requirements file the description names, or from a convention the application already follows elsewhere, and name that source. If none of them states it, leave `expectedValue` out and assert the shape instead.";
|
|
206
|
+
};
|
|
207
|
+
readonly missingSource: {
|
|
208
|
+
readonly message: "This case states an expected value and does not say where the value came from.";
|
|
209
|
+
readonly suggestion: "Set `expectedFrom` to `pr-title`, `pr-description`, `spec:<path>`, `convention:<file:line>` or `code`.";
|
|
210
|
+
};
|
|
211
|
+
readonly unreadableSource: {
|
|
212
|
+
readonly message: "This case names a file as the source of its expected value, and that file is not in the checkout.";
|
|
213
|
+
readonly suggestion: "Give the path as the repository spells it, from the repository root. If the file is not there, the value has no source: read it from the pull request title or description instead, or leave `expectedValue` out and assert the shape.";
|
|
214
|
+
};
|
|
215
|
+
readonly changedConvention: {
|
|
216
|
+
readonly message: "This case reads its expected value from a line this pull request writes, so the same change decides both the behaviour and what counts as correct.";
|
|
217
|
+
readonly suggestion: "Cite a file the pull request leaves alone, or read the value from the description or a requirements file. If the cited file is changed but has nothing to do with this case, say so as the answer.";
|
|
218
|
+
};
|
|
219
|
+
readonly quoteMissing: {
|
|
220
|
+
readonly message: "This case reads its value from the pull request, and its change does not quote the sentence that states the rule.";
|
|
221
|
+
readonly suggestion: "Put the sentence from the title or description on the change as `quote`, as written there. Derive this case's value from that rule; the value itself need not appear in the pull request.";
|
|
222
|
+
};
|
|
223
|
+
readonly failWithoutValue: {
|
|
224
|
+
readonly message: "This test expects to fail, and no case on the changes it cites states the value it must assert.";
|
|
225
|
+
readonly suggestion: "Put the value the code does not return today on the case this test sends, as `expectedValue`, and name where you read it. If no source states the value, answer with what the test asserts instead and where that comes from.";
|
|
226
|
+
};
|
|
227
|
+
};
|
|
228
|
+
readonly suggestion: "Where does the correct value for this case come from?";
|
|
229
|
+
};
|
|
230
|
+
export declare const REQUIREMENT_SOURCED_CONTRACT: {
|
|
231
|
+
readonly id: "requirementSourced";
|
|
232
|
+
readonly objections: {
|
|
233
|
+
readonly unsourced: {
|
|
234
|
+
readonly message: "Change `{change}` does not name a source the plan accepts: `pr-title`, `pr-description`, `spec:<path>` or `diff`.";
|
|
235
|
+
readonly suggestion: "Set `source` on this change to `pr-title` or `pr-description`, to `spec:<path>` for the requirements file that states it, or to `diff`.";
|
|
236
|
+
};
|
|
237
|
+
readonly unreadable: {
|
|
238
|
+
readonly message: "Change `{change}` cites `{source}`, and the checkout holds no file at that path.";
|
|
239
|
+
readonly suggestion: "Cite the requirements file by the path the checkout holds, or name the source you actually read this change in.";
|
|
240
|
+
};
|
|
241
|
+
readonly fromDiff: {
|
|
242
|
+
readonly message: "This test reports a requirement conflict, and none of the changes it cites was read from the pull request or a requirements file.";
|
|
243
|
+
readonly suggestion: "Cite a change you read in the pull request or in a requirements file, or report this test as something other than a requirement conflict.";
|
|
244
|
+
};
|
|
245
|
+
readonly notNamed: {
|
|
246
|
+
readonly message: "Change `{change}` cites `{source}`, and the pull request names no such file.";
|
|
247
|
+
readonly suggestion: "Cite a file the title or description names. If neither names it, you read the change in the repository, so set `source` to `diff`.";
|
|
248
|
+
};
|
|
249
|
+
readonly emptyPullRequest: {
|
|
250
|
+
readonly message: "Change `{change}` says it was read from the pull request, and the pull request has no title or description.";
|
|
251
|
+
readonly suggestion: "Set `source` to `spec:<path>` for the requirements file that states this change, or to `diff` if only the code states it.";
|
|
252
|
+
};
|
|
253
|
+
readonly notQuoted: {
|
|
254
|
+
readonly message: "This test reports a requirement conflict, and change `{change}` quotes words the pull request does not carry.";
|
|
255
|
+
readonly suggestion: "Quote the requirement as the title or description spells it, word for word. If you cannot find those words there, the change came from somewhere else: say where in `source`.";
|
|
256
|
+
};
|
|
257
|
+
readonly unquoted: {
|
|
258
|
+
readonly message: "This test reports a requirement conflict, and change `{change}` does not quote the requirement.";
|
|
259
|
+
readonly suggestion: "Set `quote` on that change to the sentence in the title or description that states the requirement.";
|
|
260
|
+
};
|
|
261
|
+
readonly noChange: {
|
|
262
|
+
readonly message: "This test reports a requirement conflict and cites no change, so nothing says where the requirement came from.";
|
|
263
|
+
readonly suggestion: "Name in `changes` the change that states the requirement, or report this test as something other than a requirement conflict.";
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
readonly suggestion: "Does every change name where you read it, and does every requirement-conflict test cite a change that came from the pull request or a requirements file?";
|
|
267
|
+
};
|
|
268
|
+
export declare const DEFECTS_CONTRACT: {
|
|
269
|
+
readonly id: "defects";
|
|
270
|
+
readonly objections: {
|
|
271
|
+
readonly none: {
|
|
272
|
+
readonly message: "The plan declares no defects, so nothing says the code review happened before the plan.";
|
|
273
|
+
readonly suggestion: "Review the code that serves each change — the handler, the functions it calls to read or write data, the component and what it calls — and list each defect in `defects`. If the review found none, answer in one line what you read.";
|
|
274
|
+
};
|
|
275
|
+
readonly untested: {
|
|
276
|
+
readonly message: "This declared defect has no test in the plan.";
|
|
277
|
+
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 something stopped this run from writing the test, answer it and set `blocker` to what stopped it.";
|
|
278
|
+
};
|
|
279
|
+
readonly expectedPass: {
|
|
280
|
+
readonly message: "Every test that cites this defect expects to pass, and a test that passes today proves nothing about a defect.";
|
|
281
|
+
readonly suggestion: "Assert what the fixed code returns, set `expected.outcome` to `fail` with `why`, or answer in one line why a passing test is the proof.";
|
|
282
|
+
};
|
|
283
|
+
readonly unknownId: {
|
|
284
|
+
readonly message: "A planned test cites a defect id that this plan does not declare.";
|
|
285
|
+
readonly suggestion: "Spell the defect's `id` exactly as your own `defects` list gives it — the match is exact apart from surrounding space. Declare the defect if it is missing.";
|
|
286
|
+
};
|
|
287
|
+
readonly conflictWithoutDefect: {
|
|
288
|
+
readonly message: "This test reports a requirement conflict and cites no defect.";
|
|
289
|
+
readonly suggestion: "List the conflict in `defects`: the file and line that contradict the requirement, and a description that quotes the requirement. Cite that id from this test.";
|
|
290
|
+
};
|
|
291
|
+
};
|
|
292
|
+
readonly suggestion: "Which planned test proves each defect the review found, and for one nothing proves, why does it need no test?";
|
|
293
|
+
};
|
|
211
294
|
/** Fills a contract sentence's `{placeholder}` spans from the run's or the
|
|
212
295
|
* planned test's own values. A placeholder with no value is left as written rather
|
|
213
296
|
* than blanked, so a missed key shows up as `{knob}` instead of reading as a gap
|