@skyramp/mcp 0.4.0 → 0.4.1-rc.2
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/contractProviderAssertionsPrompt.js +2 -1
- package/build/prompts/enhance-assertions/integrationAssertionsPrompt.js +2 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +57 -22
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +17 -9
- package/build/prompts/test-recommendation/diffExecutionPlan.js +0 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +11 -6
- package/build/prompts/testbot/testbot-prompts.js +19 -12
- 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 +9 -1
- package/build/recommendation/registerPlan.js +8 -1
- package/build/recommendation/runVerifiers.js +6 -0
- package/build/recommendation/types.d.ts +58 -0
- package/build/recommendation/verifierContracts.d.ts +108 -13
- package/build/recommendation/verifierContracts.js +147 -29
- package/build/recommendation/verifiers/coverage.d.ts +10 -0
- package/build/recommendation/verifiers/coverage.js +144 -22
- package/build/recommendation/verifiers/defects.d.ts +9 -0
- package/build/recommendation/verifiers/defects.js +117 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.d.ts +22 -0
- package/build/recommendation/verifiers/deliveredMatchesPlan.js +43 -0
- package/build/recommendation/verifiers/existingCoverage.js +53 -0
- package/build/recommendation/verifiers/expectedValueSourced.d.ts +14 -0
- package/build/recommendation/verifiers/expectedValueSourced.js +246 -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/services/TestGenerationService.js +3 -1
- package/build/tools/code-refactor/codeReuseTool.js +1 -1
- package/build/tools/code-refactor/reuse-outcome.d.ts +1 -1
- package/build/tools/code-refactor/reuse-state.d.ts +85 -7
- package/build/tools/code-refactor/reuse-state.js +239 -34
- package/build/tools/code-refactor/utils-verify-gates.d.ts +5 -0
- package/build/tools/code-refactor/utils-verify-gates.js +103 -11
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -1
- package/build/tools/submitReportTool.js +276 -39
- package/build/tools/test-management/actionsTool.js +5 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +53 -0
- package/build/tools/test-management/analyzeChangesTool.js +55 -2
- package/build/tools/test-management/registerTestPlanTool.d.ts +32 -17
- package/build/tools/test-management/registerTestPlanTool.js +105 -16
- package/build/types/ReuseOutcome.d.ts +73 -7
- package/build/types/TestAnalysis.d.ts +6 -0
- package/build/types/TestbotReport.d.ts +22 -4
- package/build/utils/AnalysisStateManager.d.ts +7 -1
- package/build/utils/AnalysisStateManager.js +5 -1
- package/build/utils/assertion-verify/api-shared-lints.js +118 -11
- package/build/utils/assertion-verify/format.js +2 -2
- package/build/utils/assertion-verify/helper-imports.d.ts +7 -0
- package/build/utils/assertion-verify/helper-imports.js +119 -27
- package/build/utils/assertion-verify/lint-types.d.ts +31 -2
- package/build/utils/assertion-verify/lint-types.js +66 -0
- package/build/utils/assertion-verify/metrics.d.ts +13 -0
- package/build/utils/assertion-verify/metrics.js +16 -0
- package/build/utils/assertion-verify/verify.d.ts +11 -6
- package/build/utils/assertion-verify/verify.js +56 -15
- package/build/utils/canonicalJson.d.ts +11 -0
- package/build/utils/canonicalJson.js +17 -0
- package/build/utils/utils-verify/action-key.d.ts +27 -0
- package/build/utils/utils-verify/action-key.js +292 -0
- package/build/utils/utils-verify/allow.d.ts +8 -1
- package/build/utils/utils-verify/allow.js +14 -1
- package/build/utils/utils-verify/call-sites.d.ts +76 -8
- package/build/utils/utils-verify/call-sites.js +256 -70
- package/build/utils/utils-verify/language-spec.d.ts +3 -2
- package/build/utils/utils-verify/parse.d.ts +22 -3
- package/build/utils/utils-verify/parse.js +123 -52
- package/build/utils/utils-verify/verify.d.ts +33 -3
- package/build/utils/utils-verify/verify.js +126 -12
- package/build/utils/workspaceAuth.d.ts +59 -19
- package/build/utils/workspaceAuth.js +228 -31
- package/package.json +1 -1
- package/plugin/prompts/generate-tests/execution-plan.md +3 -3
- package/plugin/prompts/generate-tests/generation.md +1 -0
- package/plugin/prompts/plan-tests.md +41 -15
- 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
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
// evidence stays computed in the verifier.
|
|
12
12
|
export const CHANGED_FILE_CONTRACT = {
|
|
13
13
|
id: "changedFile",
|
|
14
|
-
declarationFields: ["declarations.changedFile", "declarations.screenEvidence.file"],
|
|
15
14
|
objections: {
|
|
16
15
|
notStated: {
|
|
17
16
|
message: "This test does not state which changed file it targets.",
|
|
@@ -34,7 +33,6 @@ export const CHANGED_FILE_CONTRACT = {
|
|
|
34
33
|
};
|
|
35
34
|
export const SCREEN_ROUTE_CONTRACT = {
|
|
36
35
|
id: "screenRoute",
|
|
37
|
-
declarationFields: ["declarations.elements.pageUrl", "declarations.screenEvidence.file", "declarations.changedFile"],
|
|
38
36
|
objections: {
|
|
39
37
|
mismatch: {
|
|
40
38
|
message: "The page this UI test opens is not a page that renders the changed file it is about.",
|
|
@@ -49,7 +47,6 @@ export const SCREEN_ROUTE_CONTRACT = {
|
|
|
49
47
|
};
|
|
50
48
|
export const ENDPOINT_GROUNDED_CONTRACT = {
|
|
51
49
|
id: "endpointGrounded",
|
|
52
|
-
declarationFields: ["steps[]", "testType", "declarations.routes"],
|
|
53
50
|
objections: {
|
|
54
51
|
uncited: {
|
|
55
52
|
message: "A call this test makes cites no file that declares it.",
|
|
@@ -68,7 +65,6 @@ export const ENDPOINT_GROUNDED_CONTRACT = {
|
|
|
68
65
|
};
|
|
69
66
|
export const STATED_DIFFERENCE_CONTRACT = {
|
|
70
67
|
id: "statedDifference",
|
|
71
|
-
declarationFields: ["steps[]", "testType", "scenarioName", "declarations.differsFrom"],
|
|
72
68
|
objections: {
|
|
73
69
|
unexplainedPair: {
|
|
74
70
|
message: "Another planned test in this plan tests the same endpoint and neither says how they differ.",
|
|
@@ -87,18 +83,24 @@ export const STATED_DIFFERENCE_CONTRACT = {
|
|
|
87
83
|
};
|
|
88
84
|
export const EXISTING_COVERAGE_CONTRACT = {
|
|
89
85
|
id: "existingCoverage",
|
|
90
|
-
declarationFields: ["declarations.existingTests"],
|
|
91
86
|
objections: {
|
|
92
87
|
citedTestMissing: {
|
|
93
88
|
message: "This planned test names an existing test file that is not in the checkout.",
|
|
94
89
|
suggestion: "Name the file repository-relative, spelled as the analysis lists it. Drop the entry if it names a test you did not read — naming none is an answer too.",
|
|
95
90
|
},
|
|
91
|
+
maintainedTestMissing: {
|
|
92
|
+
message: "`maintains` names an existing test file that is not in the checkout.",
|
|
93
|
+
suggestion: "Name the file repository-relative, spelled as the analysis lists it. A maintenance entry is what makes its changes count as covered, so it has to name a file this run can edit.",
|
|
94
|
+
},
|
|
95
|
+
maintainedUnknownChange: {
|
|
96
|
+
message: "A `maintains` entry cites a change id that this plan does not declare.",
|
|
97
|
+
suggestion: "Spell the change's `id` exactly as your own `changes` list gives it — the match is exact apart from surrounding space. Declare the change if it is missing.",
|
|
98
|
+
},
|
|
96
99
|
},
|
|
97
|
-
suggestion: "Which existing tests did you read on this change,
|
|
100
|
+
suggestion: "Which existing tests did you read on this change, how does this planned test differ from them, and which changes does the maintenance you are doing cover?",
|
|
98
101
|
};
|
|
99
102
|
export const EXPECTED_OUTCOME_CONTRACT = {
|
|
100
103
|
id: "expectedOutcome",
|
|
101
|
-
declarationFields: ["category", "declarations.expected.outcome", "declarations.expected.why"],
|
|
102
104
|
objections: {
|
|
103
105
|
// `{category}` is the planned test's own `category` value, quoted back to it so
|
|
104
106
|
// the agent recognises the value it sent.
|
|
@@ -119,16 +121,6 @@ export const EXPECTED_OUTCOME_CONTRACT = {
|
|
|
119
121
|
};
|
|
120
122
|
export const UI_ELEMENT_GROUNDED_CONTRACT = {
|
|
121
123
|
id: "uiElementGrounded",
|
|
122
|
-
declarationFields: [
|
|
123
|
-
"testType",
|
|
124
|
-
"steps[]",
|
|
125
|
-
"declarations.elements.items",
|
|
126
|
-
"declarations.elements.pageUrl",
|
|
127
|
-
"declarations.asserts",
|
|
128
|
-
"scenarioName",
|
|
129
|
-
"description",
|
|
130
|
-
"declarations.expected.why",
|
|
131
|
-
],
|
|
132
124
|
objections: {
|
|
133
125
|
nogrounding: {
|
|
134
126
|
message: "This UI test names no `elements.items`, so nothing says the elements it targets are on the page.",
|
|
@@ -154,20 +146,10 @@ export const UI_ELEMENT_GROUNDED_CONTRACT = {
|
|
|
154
146
|
};
|
|
155
147
|
export const COVERAGE_CONTRACT = {
|
|
156
148
|
id: "coverage",
|
|
157
|
-
declarationFields: [
|
|
158
|
-
"changes",
|
|
159
|
-
"declarations.changes",
|
|
160
|
-
"declarations.changedFile",
|
|
161
|
-
"declarations.routes",
|
|
162
|
-
"declarations.screenEvidence.file",
|
|
163
|
-
"declarations.startState",
|
|
164
|
-
"changes[].cases",
|
|
165
|
-
"changes[].surfaces",
|
|
166
|
-
],
|
|
167
149
|
objections: {
|
|
168
150
|
change: {
|
|
169
151
|
message: "This declared change has no test in the plan.",
|
|
170
|
-
suggestion: "Plan a test that exercises it. If this run cannot write one, answer it and set `blocker` to what stopped the run — a service that is not running, a paired branch that no longer exists, 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.",
|
|
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, a paired branch that no longer exists, 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.",
|
|
171
153
|
},
|
|
172
154
|
noChanges: {
|
|
173
155
|
message: "The plan declares no changes, so nothing says what the pull request must make different.",
|
|
@@ -225,7 +207,6 @@ export const COVERAGE_CONTRACT = {
|
|
|
225
207
|
};
|
|
226
208
|
export const REMOVED_ELEMENT_GUARDED_CONTRACT = {
|
|
227
209
|
id: "removedElementGuarded",
|
|
228
|
-
declarationFields: ["declarations.elements.items", "declarations.asserts"],
|
|
229
210
|
objections: {
|
|
230
211
|
// v1 resolved a removed element to a page in the server, and fell back to the
|
|
231
212
|
// root pages when the import graph found none, so that an element whose page
|
|
@@ -246,6 +227,143 @@ export const REMOVED_ELEMENT_GUARDED_CONTRACT = {
|
|
|
246
227
|
},
|
|
247
228
|
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?",
|
|
248
229
|
};
|
|
230
|
+
export const EXPECTED_VALUE_SOURCED_CONTRACT = {
|
|
231
|
+
id: "expectedValueSourced",
|
|
232
|
+
objections: {
|
|
233
|
+
// The whole point of the check. A value read off the running app is the one
|
|
234
|
+
// source that cannot disagree with a defect, so a test built from it passes
|
|
235
|
+
// on the broken code and fails once the code is fixed.
|
|
236
|
+
sourcedFromCode: {
|
|
237
|
+
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.",
|
|
238
|
+
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.",
|
|
239
|
+
},
|
|
240
|
+
missingSource: {
|
|
241
|
+
message: "This case states an expected value and does not say where the value came from.",
|
|
242
|
+
suggestion: "Set `expectedFrom` to `pr-title`, `pr-description`, `spec:<path>`, `convention:<file:line>` or `code`.",
|
|
243
|
+
},
|
|
244
|
+
unreadableSource: {
|
|
245
|
+
message: "This case names a file as the source of its expected value, and that file is not in the checkout.",
|
|
246
|
+
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.",
|
|
247
|
+
},
|
|
248
|
+
// `convention:` is trusted for one reason: the pull request did not write the rule,
|
|
249
|
+
// so the rule can still disagree with the pull request. Cited inside the diff, the
|
|
250
|
+
// same change decides the behaviour and decides what counts as correct.
|
|
251
|
+
//
|
|
252
|
+
// Says only that, and not that the value came from the code under test: a large
|
|
253
|
+
// diff carries changed files that have nothing to do with this case, and the check
|
|
254
|
+
// cannot tell those from the ones it tests. Answerable for that reason.
|
|
255
|
+
changedConvention: {
|
|
256
|
+
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.",
|
|
257
|
+
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.",
|
|
258
|
+
},
|
|
259
|
+
// The server rendered the prompt, so it holds the words the run was given. It
|
|
260
|
+
// checks the RULE and not the value: "over 60 is rejected" states no 61, and run
|
|
261
|
+
// 34414643135 objected to five cases whose values that rule decides.
|
|
262
|
+
quoteMissing: {
|
|
263
|
+
message: "This case reads its value from the pull request, and its change does not quote the sentence that states the rule.",
|
|
264
|
+
suggestion: "Put the sentence from the title or description on the change as `quote`, as written there. If that sentence states the rule and not the number, put the rule in `derived` and leave `expectedValue` out.",
|
|
265
|
+
},
|
|
266
|
+
// The other half of the same reading. Run 34434835959 declared
|
|
267
|
+
// `expectedValue: 159.998` from `pr-description` on a change whose quote states
|
|
268
|
+
// how a percentage discount works and writes no number; the agent computed 10%
|
|
269
|
+
// of a subtotal it had seen and attributed the result to the description.
|
|
270
|
+
valueNotQuoted: {
|
|
271
|
+
message: "This case reads its value from the pull request, and the pull request does not state this value.",
|
|
272
|
+
suggestion: "Quote the sentence that states it, or, if the pull request states a rule, put the rule in `derived` and leave `expectedValue` out; the test computes the value from what it sent.",
|
|
273
|
+
},
|
|
274
|
+
// A test that expects to fail asserts what the code does not return today, so
|
|
275
|
+
// some source outside the code states that value. Run 34406282133 declared such
|
|
276
|
+
// tests and put `expectedValue` on no case at all.
|
|
277
|
+
failWithoutValue: {
|
|
278
|
+
message: "This test expects to fail, and no case on the changes it cites states the value it must assert.",
|
|
279
|
+
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 the source states a rule rather than a number, put the rule in `derived` instead. If no source states either, answer with what the test asserts instead and where that comes from.",
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
suggestion: "Where does the correct value for this case come from?",
|
|
283
|
+
};
|
|
284
|
+
export const REQUIREMENT_SOURCED_CONTRACT = {
|
|
285
|
+
id: "requirementSourced",
|
|
286
|
+
objections: {
|
|
287
|
+
// `{change}` is the change id. Nothing on the server parses a pull request, so
|
|
288
|
+
// this field is the only record of where the change was read. An unrecognised
|
|
289
|
+
// spelling lands here: one that passed would be a check a typo switches off.
|
|
290
|
+
unsourced: {
|
|
291
|
+
message: "Change `{change}` does not name a source the plan accepts: `pr-title`, `pr-description`, `spec:<path>` or `diff`.",
|
|
292
|
+
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`.",
|
|
293
|
+
},
|
|
294
|
+
// `{source}` is the source text as the plan wrote it. A path that no file
|
|
295
|
+
// answers is a citation nobody can check.
|
|
296
|
+
unreadable: {
|
|
297
|
+
message: "Change `{change}` cites `{source}`, and the checkout holds no file at that path.",
|
|
298
|
+
suggestion: "Cite the requirements file by the path the checkout holds, or name the source you actually read this change in.",
|
|
299
|
+
},
|
|
300
|
+
// A conflict is a disagreement between a requirement and the code. A change
|
|
301
|
+
// read off the diff is the code, so it has no second side. The check reads the
|
|
302
|
+
// sources the cited changes name, and says only that, not where they came from.
|
|
303
|
+
fromDiff: {
|
|
304
|
+
message: "This test reports a requirement conflict, and none of the changes it cites was read from the pull request or a requirements file.",
|
|
305
|
+
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.",
|
|
306
|
+
},
|
|
307
|
+
// The server rendered the prompt, so it holds the title and description the run
|
|
308
|
+
// was given. A requirements file the pull request never names is a file the agent
|
|
309
|
+
// found by looking, which is reading the repository, not reading a requirement.
|
|
310
|
+
notNamed: {
|
|
311
|
+
message: "Change `{change}` cites `{source}`, and the pull request names no such file.",
|
|
312
|
+
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`.",
|
|
313
|
+
},
|
|
314
|
+
// Says only what was read: this pull request arrived with no words in it.
|
|
315
|
+
emptyPullRequest: {
|
|
316
|
+
message: "Change `{change}` says it was read from the pull request, and the pull request has no title or description.",
|
|
317
|
+
suggestion: "Set `source` to `spec:<path>` for the requirements file that states this change, or to `diff` if only the code states it.",
|
|
318
|
+
},
|
|
319
|
+
// The quote is what makes the claim checkable: without it the server has the
|
|
320
|
+
// pull request's words and no way to know which of them the change rests on.
|
|
321
|
+
notQuoted: {
|
|
322
|
+
message: "This test reports a requirement conflict, and change `{change}` quotes words the pull request does not carry.",
|
|
323
|
+
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`.",
|
|
324
|
+
},
|
|
325
|
+
unquoted: {
|
|
326
|
+
message: "This test reports a requirement conflict, and change `{change}` does not quote the requirement.",
|
|
327
|
+
suggestion: "Set `quote` on that change to the sentence in the title or description that states the requirement.",
|
|
328
|
+
},
|
|
329
|
+
// Nothing else objects to this: the coverage check reads a change and asks which
|
|
330
|
+
// test proves it, so a test that cites nothing is a test it never reaches.
|
|
331
|
+
noChange: {
|
|
332
|
+
message: "This test reports a requirement conflict and cites no change, so nothing says where the requirement came from.",
|
|
333
|
+
suggestion: "Name in `changes` the change that states the requirement, or report this test as something other than a requirement conflict.",
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
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?",
|
|
337
|
+
};
|
|
338
|
+
export const DEFECTS_CONTRACT = {
|
|
339
|
+
id: "defects",
|
|
340
|
+
objections: {
|
|
341
|
+
// Run 34283539284 reported six bugs with no test behind them: the review
|
|
342
|
+
// happened at report time, after the plan was frozen. A plan that lists no
|
|
343
|
+
// defect is where the review is asked for, before the plan.
|
|
344
|
+
none: {
|
|
345
|
+
message: "The plan declares no defects, so nothing says the code review happened before the plan.",
|
|
346
|
+
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.",
|
|
347
|
+
},
|
|
348
|
+
untested: {
|
|
349
|
+
message: "This declared defect has no test in the plan.",
|
|
350
|
+
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.",
|
|
351
|
+
},
|
|
352
|
+
expectedPass: {
|
|
353
|
+
message: "Every test that cites this defect expects to pass, and a test that passes today proves nothing about a defect.",
|
|
354
|
+
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.",
|
|
355
|
+
},
|
|
356
|
+
unknownId: {
|
|
357
|
+
message: "A planned test cites a defect id that this plan does not declare.",
|
|
358
|
+
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.",
|
|
359
|
+
},
|
|
360
|
+
conflictWithoutDefect: {
|
|
361
|
+
message: "This test reports a requirement conflict and cites no defect.",
|
|
362
|
+
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.",
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
suggestion: "Which planned test proves each defect the review found, and for one nothing proves, why does it need no test?",
|
|
366
|
+
};
|
|
249
367
|
/** Fills a contract sentence's `{placeholder}` spans from the run's or the
|
|
250
368
|
* planned test's own values. A placeholder with no value is left as written rather
|
|
251
369
|
* than blanked, so a missed key shows up as `{knob}` instead of reading as a gap
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { Verifier } from "../types.js";
|
|
2
|
+
/** One declared case as the objection spells it.
|
|
3
|
+
*
|
|
4
|
+
* Exported: `expectedValueSourced` keys its objections on the same rendering, so
|
|
5
|
+
* two cases on one parameter are two objections there too. */
|
|
6
|
+
export declare function renderCase(declared: {
|
|
7
|
+
param: string;
|
|
8
|
+
value?: unknown;
|
|
9
|
+
absent?: true;
|
|
10
|
+
expect: string;
|
|
11
|
+
}): string;
|
|
2
12
|
/** The completeness check, over three surfaces: every declared behaviour, every
|
|
3
13
|
* mutated route, and every changed file needs a test in the plan. More is
|
|
4
14
|
* welcome, fewer is the objection. ONE RULE for the file half, no classification
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { allChangedFiles } from "../types.js";
|
|
2
|
-
import {
|
|
2
|
+
import { canonicalJson } from "../../utils/canonicalJson.js";
|
|
3
|
+
import { escapesRepo, LOCKFILE, NOT_A_ROUTE_FILE, NOT_SOURCE_DIR } from "./endpointGrounded.js";
|
|
3
4
|
import { isTestPath } from "../../utils/testFileClassification.js";
|
|
4
5
|
import { normalizeCitedPath } from "./citedPath.js";
|
|
5
6
|
import { COVERAGE_CONTRACT, fillPlaceholders } from "../verifierContracts.js";
|
|
@@ -37,9 +38,31 @@ function citedFiles(plannedTests) {
|
|
|
37
38
|
}
|
|
38
39
|
return cited;
|
|
39
40
|
}
|
|
40
|
-
/** The behaviour ids
|
|
41
|
-
|
|
41
|
+
/** The behaviour ids the plan cites, trimmed — from its planned tests AND from the
|
|
42
|
+
* existing tests it maintains. A change covered by a spec this run edits is tested;
|
|
43
|
+
* counting only planned tests made `coverage:change:` unanswerable for a correct
|
|
44
|
+
* maintenance decision, since that objection closes with a `blocker` alone and
|
|
45
|
+
* nothing stopped the run.
|
|
46
|
+
*
|
|
47
|
+
* A maintenance entry credits its ids only when the file it names RESOLVES in the
|
|
48
|
+
* checkout. `existingCoverage` objects about an unresolvable one, but that objection
|
|
49
|
+
* takes prose, so crediting first would have handed `coverage:change:` — the one
|
|
50
|
+
* class a sentence cannot close, and the reason `BLOCKER_ONLY_PREFIX` exists — a
|
|
51
|
+
* route out through a file name nobody can open. */
|
|
52
|
+
function citedBehaviors(plannedTests, maintains = [], fileResolves = () => false) {
|
|
42
53
|
const cited = new Set();
|
|
54
|
+
for (const entry of maintains) {
|
|
55
|
+
const ids = entry?.changes;
|
|
56
|
+
if (!Array.isArray(ids))
|
|
57
|
+
continue;
|
|
58
|
+
if (!fileResolves(String(entry?.file ?? "").trim()))
|
|
59
|
+
continue;
|
|
60
|
+
for (const id of ids) {
|
|
61
|
+
const trimmed = String(id ?? "").trim();
|
|
62
|
+
if (trimmed)
|
|
63
|
+
cited.add(trimmed);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
43
66
|
for (const plannedTest of plannedTests) {
|
|
44
67
|
const declared = plannedTest?.declarations?.changes;
|
|
45
68
|
if (!Array.isArray(declared))
|
|
@@ -58,7 +81,7 @@ function citedBehaviors(plannedTests) {
|
|
|
58
81
|
*
|
|
59
82
|
* Checked whether or not the run has a diff: the agent declared these itself, so
|
|
60
83
|
* there is always something to hold it to. */
|
|
61
|
-
function claimObjections(registration) {
|
|
84
|
+
function claimObjections(registration, ctx) {
|
|
62
85
|
const changes = Array.isArray(registration.changes) ? registration.changes : [];
|
|
63
86
|
const plannedTests = registration.plannedTests ?? [];
|
|
64
87
|
const objections = [];
|
|
@@ -79,7 +102,8 @@ function claimObjections(registration) {
|
|
|
79
102
|
if (id)
|
|
80
103
|
declared.add(id);
|
|
81
104
|
}
|
|
82
|
-
const
|
|
105
|
+
const maintains = Array.isArray(registration.maintains) ? registration.maintains : [];
|
|
106
|
+
const cited = citedBehaviors(plannedTests, maintains, (file) => !!file && !escapesRepo(file) && ctx.citedFileExists(file));
|
|
83
107
|
const seen = new Set();
|
|
84
108
|
for (const change of changes) {
|
|
85
109
|
const id = String(change?.id ?? "").trim();
|
|
@@ -207,28 +231,100 @@ function sameValue(sent, declared) {
|
|
|
207
231
|
return false;
|
|
208
232
|
if (declared === null)
|
|
209
233
|
return sent === null;
|
|
234
|
+
// A list or an object compares WHOLE, and only against another one. `String` made
|
|
235
|
+
// every structure a scalar: `String(["alpha","beta"])` is `"alpha,beta"`, so a
|
|
236
|
+
// case declaring that string was answered by a list no entry of which sends it,
|
|
237
|
+
// and `String([])` is `""`, so an empty list answered `tags=""`. An entry of the
|
|
238
|
+
// list is still a scalar of its own and still answers a scalar case.
|
|
239
|
+
const sentIsStructure = sent !== null && typeof sent === "object";
|
|
240
|
+
const declaredIsStructure = typeof declared === "object";
|
|
241
|
+
if (sentIsStructure || declaredIsStructure) {
|
|
242
|
+
// Both sides are non-null objects by the guard above, so neither call can be
|
|
243
|
+
// the `undefined` the helper returns for a value JSON renders as nothing.
|
|
244
|
+
return sentIsStructure && declaredIsStructure && canonicalJson(sent) === canonicalJson(declared);
|
|
245
|
+
}
|
|
210
246
|
const sentNumber = asNumber(sent);
|
|
211
247
|
const declaredNumber = asNumber(declared);
|
|
212
248
|
if (sentNumber !== undefined && declaredNumber !== undefined)
|
|
213
249
|
return sentNumber === declaredNumber;
|
|
214
250
|
return String(sent) === String(declared);
|
|
215
251
|
}
|
|
216
|
-
/**
|
|
217
|
-
*
|
|
218
|
-
|
|
219
|
-
|
|
252
|
+
/** The path a case's `param` names, as segments. Numeric segments drop: a plan
|
|
253
|
+
* writes `items.0` for "the line items", not for the first one only. */
|
|
254
|
+
function fieldPath(param) {
|
|
255
|
+
const segments = param
|
|
256
|
+
.split(/[.[\]]/)
|
|
257
|
+
.map((segment) => segment.trim())
|
|
258
|
+
.filter((segment) => segment.length > 0 && !/^\d+$/.test(segment));
|
|
259
|
+
return segments.length > 0 ? segments : [param];
|
|
260
|
+
}
|
|
261
|
+
/** Every value at `rest`, below a key that already matched the path's first
|
|
262
|
+
* segment. Only arrays are transparent between segments, so `billing.postcode` is
|
|
263
|
+
* not sent by `billing.shipping.postcode`. A matched array yields the array AND
|
|
264
|
+
* its entries, because `String(["beta"]) === "beta"` matched a one-item list only.
|
|
265
|
+
*
|
|
266
|
+
* `open` holds the objects on the way down to this one, so a payload that points
|
|
267
|
+
* back at itself ends rather than recurses. It is the way down and not every
|
|
268
|
+
* object seen, because one object reached twice by two different routes is two
|
|
269
|
+
* values the payload really sends. */
|
|
270
|
+
function valuesAt(value, rest, open) {
|
|
271
|
+
if (rest.length === 0)
|
|
272
|
+
return [value, ...(Array.isArray(value) ? value : [])];
|
|
273
|
+
if (value === null || typeof value !== "object" || open.has(value))
|
|
274
|
+
return [];
|
|
275
|
+
open.add(value);
|
|
276
|
+
const found = Array.isArray(value)
|
|
277
|
+
? value.flatMap((entry) => valuesAt(entry, rest, open))
|
|
278
|
+
: (() => {
|
|
279
|
+
const [head, ...next] = rest;
|
|
280
|
+
const fields = value;
|
|
281
|
+
return head in fields ? valuesAt(fields[head], next, open) : [];
|
|
282
|
+
})();
|
|
283
|
+
open.delete(value);
|
|
284
|
+
return found;
|
|
285
|
+
}
|
|
286
|
+
/** Every value carried at `path`, anywhere inside one payload. Only the FIRST
|
|
287
|
+
* segment is found at any depth, which is what makes `quantity` find
|
|
288
|
+
* `items.0.quantity`; the rest of the path is matched by `valuesAt`. */
|
|
289
|
+
function valuesUnder(payload, path, open = new Set()) {
|
|
290
|
+
// A body that points back at itself ends the walk; how deeply a real body nests
|
|
291
|
+
// is the payload's business, and a depth cap here read a valid one as sending
|
|
292
|
+
// nothing.
|
|
293
|
+
if (payload === null || typeof payload !== "object" || open.has(payload))
|
|
294
|
+
return [];
|
|
295
|
+
open.add(payload);
|
|
296
|
+
const found = [];
|
|
297
|
+
if (Array.isArray(payload)) {
|
|
298
|
+
for (const entry of payload)
|
|
299
|
+
found.push(...valuesUnder(entry, path, open));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
303
|
+
if (key === path[0])
|
|
304
|
+
found.push(...valuesAt(value, path.slice(1), open));
|
|
305
|
+
// The same head, deeper: this is the any-depth half of the search.
|
|
306
|
+
found.push(...valuesUnder(value, path, open));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
open.delete(payload);
|
|
310
|
+
return found;
|
|
311
|
+
}
|
|
312
|
+
/** Every value of one parameter a step sends: the query string, the `queryParams`
|
|
313
|
+
* object, or anywhere inside the request body. */
|
|
220
314
|
function valuesSent(step, param) {
|
|
221
315
|
const found = [];
|
|
316
|
+
// A query parameter is flat and its own name can hold a dot, so the query answers
|
|
317
|
+
// the parameter as written. Only the body is read as a path.
|
|
222
318
|
const query = String(step?.path ?? "").split("?").slice(1).join("?");
|
|
223
319
|
if (query)
|
|
224
320
|
found.push(...new URLSearchParams(query).getAll(param));
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
321
|
+
const queryParams = step?.queryParams;
|
|
322
|
+
if (queryParams && typeof queryParams === "object" && !Array.isArray(queryParams)) {
|
|
323
|
+
const fields = queryParams;
|
|
324
|
+
if (param in fields)
|
|
325
|
+
found.push(fields[param]);
|
|
231
326
|
}
|
|
327
|
+
found.push(...valuesUnder(step?.requestBody, fieldPath(param)));
|
|
232
328
|
return found;
|
|
233
329
|
}
|
|
234
330
|
/** The parameter name that names the request's verb rather than one of its inputs. */
|
|
@@ -316,6 +412,15 @@ function sendsInPath(step, slot, value) {
|
|
|
316
412
|
}
|
|
317
413
|
return sameValue(segments[slot.index], value);
|
|
318
414
|
}
|
|
415
|
+
/** Whether a step writes the declared value as one of its own path segments. The
|
|
416
|
+
* fallback for a plan that spells every route concretely, so no slot exists. Only
|
|
417
|
+
* used when the parameter has no slot at all: where the plan does say where the
|
|
418
|
+
* parameter sits, the slot rule stands. */
|
|
419
|
+
function sendsAsPathSegment(step, value) {
|
|
420
|
+
if (value === undefined || value === null || String(value).trim().length === 0)
|
|
421
|
+
return false;
|
|
422
|
+
return pathSegments(step).some((segment) => sameValue(segment.trim(), value));
|
|
423
|
+
}
|
|
319
424
|
/** Whether a step's verb is the declared value. A rejected method is the input. */
|
|
320
425
|
function sendsAsMethod(step, value) {
|
|
321
426
|
const method = String(step?.method ?? "").trim();
|
|
@@ -332,8 +437,11 @@ function omittedBy(step, param) {
|
|
|
332
437
|
return false;
|
|
333
438
|
return omits.some((named) => String(named ?? "").trim() === param);
|
|
334
439
|
}
|
|
335
|
-
/** One declared case as the objection spells it.
|
|
336
|
-
|
|
440
|
+
/** One declared case as the objection spells it.
|
|
441
|
+
*
|
|
442
|
+
* Exported: `expectedValueSourced` keys its objections on the same rendering, so
|
|
443
|
+
* two cases on one parameter are two objections there too. */
|
|
444
|
+
export function renderCase(declared) {
|
|
337
445
|
const param = String(declared.param).trim();
|
|
338
446
|
const sent = declared.absent === true ? "absent" : `=${JSON.stringify(declared.value)}`;
|
|
339
447
|
return `${param}${declared.absent === true ? " " : ""}${sent} (${declared.expect})`;
|
|
@@ -381,6 +489,7 @@ function caseObjections(registration) {
|
|
|
381
489
|
const steps = plannedTest?.scenario?.steps;
|
|
382
490
|
return Array.isArray(steps) ? steps : [];
|
|
383
491
|
});
|
|
492
|
+
const writesAnyPlaceholder = citingSteps.some((step) => pathSegments(step).some((segment) => placeholderName(segment) !== undefined));
|
|
384
493
|
const missing = cases.filter((declared) => {
|
|
385
494
|
const param = String(declared?.param ?? "").trim();
|
|
386
495
|
if (!param)
|
|
@@ -389,9 +498,14 @@ function caseObjections(registration) {
|
|
|
389
498
|
return !citingSteps.some((step) => omittedBy(step, param));
|
|
390
499
|
const slots = pathSlotsFor(citingSteps, cases, param);
|
|
391
500
|
const isMethod = param.toLowerCase() === METHOD_PARAM;
|
|
501
|
+
// Only for a plan that wrote NO template at all, and only for a parameter that
|
|
502
|
+
// names a record. Wider than that, the URL answered everything: a `quantity=5`
|
|
503
|
+
// case passed on `GET /api/v1/orders/5`.
|
|
504
|
+
const byValueInPath = slots.length === 0 && !writesAnyPlaceholder && ID_PARAM.test(param) && !isMethod;
|
|
392
505
|
return !citingSteps.some((step) => valuesSent(step, param).some((sent) => sameValue(sent, declared.value)) ||
|
|
393
506
|
(isMethod && sendsAsMethod(step, declared.value)) ||
|
|
394
|
-
slots.some((slot) => sendsInPath(step, slot, declared.value))
|
|
507
|
+
slots.some((slot) => sendsInPath(step, slot, declared.value)) ||
|
|
508
|
+
(byValueInPath && sendsAsPathSegment(step, declared.value)));
|
|
395
509
|
});
|
|
396
510
|
// One objection per case. A single objection listing three cases took one
|
|
397
511
|
// answer that addressed two of them and closed: run 34167252679 answered
|
|
@@ -405,7 +519,13 @@ function caseObjections(registration) {
|
|
|
405
519
|
objectionId: `coverage:cases:${id}:${rendered}`,
|
|
406
520
|
verifier: "coverage",
|
|
407
521
|
message: fillPlaceholders(COVERAGE_CONTRACT.objections.cases.message, { missing: rendered }),
|
|
408
|
-
evidence: `${citing.map((plannedTest) => plannedTest.plannedTestId).join(", ")} cite ${id}; no step of theirs sends ${rendered}
|
|
522
|
+
evidence: `${citing.map((plannedTest) => plannedTest.plannedTestId).join(", ")} cite ${id}; no step of theirs sends ${rendered}.` +
|
|
523
|
+
// Which spelling WOULD have matched, or the agent re-sends the same plan.
|
|
524
|
+
// Id parameters only: it asked for `{quantity}` in a route otherwise.
|
|
525
|
+
(ID_PARAM.test(String(declared?.param ?? "").trim()) &&
|
|
526
|
+
citingSteps.some((step) => sendsAsPathSegment(step, declared.value))
|
|
527
|
+
? ` A step's path carries that value, in a segment this plan's own placeholders do not name \`${String(declared?.param ?? "").trim()}\`; write the route as a template, with \`{${String(declared?.param ?? "").trim()}}\` where it sits.`
|
|
528
|
+
: ""),
|
|
409
529
|
suggestion: COVERAGE_CONTRACT.objections.cases.suggestion,
|
|
410
530
|
});
|
|
411
531
|
}
|
|
@@ -416,8 +536,10 @@ function caseObjections(registration) {
|
|
|
416
536
|
* sends the same spelling, so `{memberUserId}` in a case is that record and a
|
|
417
537
|
* literal is a value the plan chose. */
|
|
418
538
|
const PLACEHOLDER_VALUE = /^\{[A-Za-z][A-Za-z0-9]*\}$/;
|
|
419
|
-
/** A parameter that names a record by id, by its own name
|
|
420
|
-
|
|
539
|
+
/** A parameter that names a record by id, by its own name: `id` itself, `id` after
|
|
540
|
+
* a separator, or `Id`/`ID` starting a word. `/id$/i` also read `paid`, `valid` and
|
|
541
|
+
* `fluid` as record ids, which let `valid=5` take its value from `/orders/5`. */
|
|
542
|
+
const ID_PARAM = /(?:^|[_\-.])[Ii][Dd]$|[a-z0-9](?:Id|ID)$/;
|
|
421
543
|
function isPlaceholderValue(value) {
|
|
422
544
|
return typeof value === "string" && PLACEHOLDER_VALUE.test(value.trim());
|
|
423
545
|
}
|
|
@@ -578,7 +700,7 @@ export const coverage = {
|
|
|
578
700
|
name: "coverage",
|
|
579
701
|
run(registration, ctx) {
|
|
580
702
|
const objections = [
|
|
581
|
-
...claimObjections(registration),
|
|
703
|
+
...claimObjections(registration, ctx),
|
|
582
704
|
...stateTestObjections(registration),
|
|
583
705
|
...caseObjections(registration),
|
|
584
706
|
...rejectObjections(registration),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Verifier } from "../types.js";
|
|
2
|
+
/** Every declared defect needs one planned test that cites it and expects to fail,
|
|
3
|
+
* or an answer. Read off the PLAN alone, like the behaviour half of coverage: the
|
|
4
|
+
* agent declared the defects itself, so there is always something to hold it to.
|
|
5
|
+
*
|
|
6
|
+
* The one objection about the list as a whole, `defects:none`, is what asks for
|
|
7
|
+
* the review before the plan: without it the review's only output was
|
|
8
|
+
* `issuesFound`, which exists at report time, after every test is generated. */
|
|
9
|
+
export declare const defects: Verifier;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { normalizeCategory } from "../../types/TestRecommendation.js";
|
|
2
|
+
import { DEFECTS_CONTRACT } from "../verifierContracts.js";
|
|
3
|
+
/** A defect id as the plan spells it: trimmed, and exact otherwise. */
|
|
4
|
+
function idKey(raw) {
|
|
5
|
+
return typeof raw === "string" ? raw.trim() : "";
|
|
6
|
+
}
|
|
7
|
+
/** The defect ids one planned test cites, trimmed and without repeats. Read
|
|
8
|
+
* defensively: the plan comes off disk, so the array type holds only on the
|
|
9
|
+
* validated path, and a throw here would hide every other objection. */
|
|
10
|
+
function citedDefects(plannedTest) {
|
|
11
|
+
const declared = plannedTest?.declarations?.defects;
|
|
12
|
+
if (!Array.isArray(declared))
|
|
13
|
+
return [];
|
|
14
|
+
return [...new Set(declared.map(idKey))].filter(Boolean);
|
|
15
|
+
}
|
|
16
|
+
/** Whether a planned test expects to fail. Undeclared reads as not failing: a test
|
|
17
|
+
* that says nothing about its outcome proves nothing about a defect either. */
|
|
18
|
+
function expectsToFail(plannedTest) {
|
|
19
|
+
return plannedTest?.declarations?.expected?.outcome === "fail";
|
|
20
|
+
}
|
|
21
|
+
/** Where the defect sits, as the objection quotes it. */
|
|
22
|
+
function locate(defect) {
|
|
23
|
+
const file = String(defect?.file ?? "").trim() || "no file";
|
|
24
|
+
return typeof defect?.line === "number" ? `${file}:${defect.line}` : file;
|
|
25
|
+
}
|
|
26
|
+
/** Every declared defect needs one planned test that cites it and expects to fail,
|
|
27
|
+
* or an answer. Read off the PLAN alone, like the behaviour half of coverage: the
|
|
28
|
+
* agent declared the defects itself, so there is always something to hold it to.
|
|
29
|
+
*
|
|
30
|
+
* The one objection about the list as a whole, `defects:none`, is what asks for
|
|
31
|
+
* the review before the plan: without it the review's only output was
|
|
32
|
+
* `issuesFound`, which exists at report time, after every test is generated. */
|
|
33
|
+
export const defects = {
|
|
34
|
+
name: "defects",
|
|
35
|
+
run(registration, _ctx) {
|
|
36
|
+
const declared = (Array.isArray(registration.defects) ? registration.defects : []).filter((defect) => idKey(defect?.id).length > 0);
|
|
37
|
+
const plannedTests = registration.plannedTests ?? [];
|
|
38
|
+
const objections = [];
|
|
39
|
+
// An empty list is its own objection and NOT an early return: a plan that
|
|
40
|
+
// declares nothing can still cite `D1` from a planned test, and that citation
|
|
41
|
+
// is a second mistake with a second answer. Returning here left it unraised,
|
|
42
|
+
// so one answer to `defects:none` closed the whole thing.
|
|
43
|
+
if (declared.length === 0) {
|
|
44
|
+
objections.push({
|
|
45
|
+
objectionId: "defects:none",
|
|
46
|
+
verifier: "defects",
|
|
47
|
+
message: DEFECTS_CONTRACT.objections.none.message,
|
|
48
|
+
evidence: "the registered plan declares an empty `defects` list.",
|
|
49
|
+
suggestion: DEFECTS_CONTRACT.objections.none.suggestion,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const known = new Set(declared.map((defect) => idKey(defect.id)));
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
for (const defect of declared) {
|
|
55
|
+
const id = idKey(defect.id);
|
|
56
|
+
if (seen.has(id))
|
|
57
|
+
continue;
|
|
58
|
+
seen.add(id);
|
|
59
|
+
const citing = plannedTests.filter((plannedTest) => citedDefects(plannedTest).includes(id));
|
|
60
|
+
if (citing.length === 0) {
|
|
61
|
+
objections.push({
|
|
62
|
+
// Keyed on the defect's id, not its position: the agent reorders the
|
|
63
|
+
// list between registrations and the answer has to follow the defect.
|
|
64
|
+
objectionId: `defects:untested:${id}`,
|
|
65
|
+
verifier: "defects",
|
|
66
|
+
message: DEFECTS_CONTRACT.objections.untested.message,
|
|
67
|
+
evidence: `${id} (${locate(defect)}): "${String(defect.description ?? "").trim()}"; no planned test cites it in \`defects\`.`,
|
|
68
|
+
suggestion: DEFECTS_CONTRACT.objections.untested.suggestion,
|
|
69
|
+
});
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (citing.some(expectsToFail))
|
|
73
|
+
continue;
|
|
74
|
+
objections.push({
|
|
75
|
+
objectionId: `defects:expectedPass:${id}`,
|
|
76
|
+
verifier: "defects",
|
|
77
|
+
message: DEFECTS_CONTRACT.objections.expectedPass.message,
|
|
78
|
+
evidence: `${citing.map((plannedTest) => plannedTest.plannedTestId).join(", ")} cite ${id}; none declares \`expected.outcome: fail\`.`,
|
|
79
|
+
suggestion: DEFECTS_CONTRACT.objections.expectedPass.suggestion,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// One objection per (planned test, unknown id): each is its own misspelling
|
|
83
|
+
// and owes its own answer.
|
|
84
|
+
for (const plannedTest of plannedTests) {
|
|
85
|
+
for (const id of citedDefects(plannedTest)) {
|
|
86
|
+
if (known.has(id))
|
|
87
|
+
continue;
|
|
88
|
+
objections.push({
|
|
89
|
+
objectionId: `defects:unknownId:${plannedTest.plannedTestId}:${id}`,
|
|
90
|
+
verifier: "defects",
|
|
91
|
+
plannedTestId: plannedTest.plannedTestId,
|
|
92
|
+
message: DEFECTS_CONTRACT.objections.unknownId.message,
|
|
93
|
+
evidence: `${plannedTest.plannedTestId} cites "${id}"; this plan declares ${known.size > 0 ? [...known].map((declaredId) => `"${declaredId}"`).join(", ") : "no defects"}.`,
|
|
94
|
+
suggestion: DEFECTS_CONTRACT.objections.unknownId.suggestion,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// A requirement the code does not meet is a defect, and that defect's `file` is
|
|
99
|
+
// the code that contradicts the requirement.
|
|
100
|
+
for (const plannedTest of plannedTests) {
|
|
101
|
+
if (normalizeCategory(plannedTest?.scenario?.category) !== "requirement_conflict")
|
|
102
|
+
continue;
|
|
103
|
+
if (citedDefects(plannedTest).length > 0)
|
|
104
|
+
continue;
|
|
105
|
+
const plannedTestId = idKey(plannedTest?.plannedTestId) || "plan";
|
|
106
|
+
objections.push({
|
|
107
|
+
objectionId: `defects:conflictWithoutDefect:${plannedTestId}`,
|
|
108
|
+
verifier: "defects",
|
|
109
|
+
plannedTestId: plannedTest?.plannedTestId,
|
|
110
|
+
message: DEFECTS_CONTRACT.objections.conflictWithoutDefect.message,
|
|
111
|
+
evidence: `${plannedTestId} is planned as \`requirement_conflict\` and cites no defect in \`defects\`.`,
|
|
112
|
+
suggestion: DEFECTS_CONTRACT.objections.conflictWithoutDefect.suggestion,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return objections;
|
|
116
|
+
},
|
|
117
|
+
};
|