@turri-dev/cli 0.1.3-next.04c4f71 → 0.1.3-next.162a2c3
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/dist/config.js +3 -2
- package/dist/extension.js +45 -14
- package/dist/loop-cli.js +1 -1
- package/dist/pi-launch.js +5 -0
- package/dist/question-dialog.js +5 -1
- package/dist/regenerate/conformance-agents.js +220 -11
- package/dist/regenerate/conformance.js +87 -33
- package/dist/regenerate/debugger.js +27 -3
- package/dist/regenerate/environment-error.js +17 -0
- package/dist/regenerate/loop.js +22 -4
- package/dist/regenerate/prompt.js +3 -1
- package/dist/regenerate/regenerate.js +24 -6
- package/dist/regenerate/requires.js +55 -2
- package/dist/regenerate/stop-report.js +13 -0
- package/dist/regenerate/test-scripts.js +22 -1
- package/dist/regenerate/unit-review.js +11 -0
- package/dist/regenerate-cli.js +1 -1
- package/dist/resume-line.js +4 -0
- package/dist/spec-tools.js +8 -44
- package/docs/config.md +3 -2
- package/docs/headless.md +2 -2
- package/docs/regenerating.md +53 -10
- package/package.json +3 -3
|
@@ -9,14 +9,14 @@ import { abandonWork, diffSinceSnapshot, moduleDirOf, recordHistory, startWork,
|
|
|
9
9
|
import { createWorkLedger, describeAccounts, SUBMIT_WORK_NOTE, unanswered, unansweredPrompt } from "./submit-work.js";
|
|
10
10
|
import { CONFORMANCE_COMPACTION } from "./compaction.js";
|
|
11
11
|
import { NOTES_TURN, notesForSession, passedOutcome, readNotes, reflectOnReview, withNotes } from "./notes.js";
|
|
12
|
-
import { labelsPrompt, labelsPromptFor, misplacedConformanceTests, misplacedTestsPrompt, missingTestLabels } from "./test-kinds.js";
|
|
12
|
+
import { acceptanceLabel, acceptanceText, labelsPrompt, labelsPromptFor, misplacedConformanceTests, misplacedTestsPrompt, missingTestLabels } from "./test-kinds.js";
|
|
13
13
|
import { createScriptTool, findTestScripts, projectRootFor, requireTestScripts, runConformanceScript, runUnitScript, failureSignature, STUCK_ROUNDS, scriptFailure, staticChecksOnly, withTestingScripts, } from "./test-scripts.js";
|
|
14
14
|
import { createWebTools, WEB_TOOLS } from "./web.js";
|
|
15
15
|
import { createMyDiffTool } from "./self-check.js";
|
|
16
16
|
import { createServiceTools } from "./services.js";
|
|
17
|
-
import { missingToolchainError, toolchainError } from "./environment-error.js";
|
|
17
|
+
import { missingToolchainError, timeLimitError, toolchainError } from "./environment-error.js";
|
|
18
18
|
import { workspaceBlock } from "./workspace.js";
|
|
19
|
-
import { codeFixPrompt, conflictKey, createCodeFixer, answerAsExplorer, createAskExplorerTool, exploreInvocation, judgeFailure, SPEC_CONFLICT_RAISES, ENVIRONMENT_RAISES, environmentCheckPrompt, createTestReviewer, testReviewRequest, reviewFeedbackPrompt, testsFixPrompt, sortedRefs, } from "./conformance-agents.js";
|
|
19
|
+
import { codeFixPrompt, conflictKey, escalatedFixPrompt, createCodeFixer, answerAsExplorer, createAskExplorerTool, exploreInvocation, judgeFailure, SPEC_CONFLICT_RAISES, ENVIRONMENT_RAISES, environmentCheckPrompt, createTestReviewer, testReviewRequest, reviewFeedbackPrompt, testsFixPrompt, sortedRefs, } from "./conformance-agents.js";
|
|
20
20
|
import { resolveRequiresChain } from "./requires.js";
|
|
21
21
|
import { notesToRead, recordNote } from "./notes.js";
|
|
22
22
|
import { reportSpecConflict } from "./activity.js";
|
|
@@ -157,9 +157,11 @@ export async function renderConformanceTests(options) {
|
|
|
157
157
|
// may read the code but writes only tests. An explorer reads the code first and writes down how the
|
|
158
158
|
// software is invoked, which is what the tests are written against.
|
|
159
159
|
const label = `${moduleName} ${options.diff !== undefined ? "spec change" : frid}`;
|
|
160
|
-
const specContext = { moduleName, text: specText(specifications, moduleName, frids, required, options.change ?? options.diff, sectionModules(tree, moduleName)) };
|
|
161
160
|
// For a spec change: the functionalities it touches, whose acceptance tests are labelled, checked and reviewed.
|
|
162
161
|
const touched = forChange ? touchedFunctionalities(tree, frids, options.diff) : [];
|
|
162
|
+
// The acceptance tests the run's tests carry out: every functionality the change touches, or the one under test.
|
|
163
|
+
const carriedOut = forChange ? touched : [{ frid: frid, acceptanceTests: specifications[ACCEPTANCE_TESTS] ?? [] }];
|
|
164
|
+
const specContext = { moduleName, text: specText(specifications, moduleName, frids, required, carriedOut, options.change ?? options.diff, sectionModules(tree, moduleName)) };
|
|
163
165
|
// Any functionality of the codebase - this module's, and those of the modules it requires. A
|
|
164
166
|
// functionality's run may raise a spec conflict only with that functionality; a spec diff's, between any two.
|
|
165
167
|
const own = (forChange ? frids : frids.slice(0, frids.indexOf(frid) + 1)).map((id) => ({ module: moduleName, frid: id }));
|
|
@@ -194,6 +196,11 @@ export async function renderConformanceTests(options) {
|
|
|
194
196
|
let environmentRaises = 0;
|
|
195
197
|
/** How many fixes in a row - the code's or the tests' - changed no file: an agent that will not, or cannot, act on the verdict. */
|
|
196
198
|
let idleFixes = 0;
|
|
199
|
+
/** The last fix was the tests', for a failure judged theirs: should the run fail the same way again, the judge weighs escalating it. */
|
|
200
|
+
let testsFix;
|
|
201
|
+
/** A failure escalated to the code: should the run still fail the same way after the code's fix, the run stops. */
|
|
202
|
+
let escalated;
|
|
203
|
+
let escalation;
|
|
197
204
|
/**
|
|
198
205
|
* One attempt at the failing run, written into the notes' pitfalls - what the next judge reads
|
|
199
206
|
* first - as cause and effect: what failed, how it was judged, and what was changed; the next
|
|
@@ -218,11 +225,15 @@ export async function renderConformanceTests(options) {
|
|
|
218
225
|
};
|
|
219
226
|
stage(`exploring the code for how it is invoked${options.model ? ` (${options.model})` : ""}`);
|
|
220
227
|
// The explorer reads the conformance testing script too: it is how the tests are built and run, and so how they reach the software.
|
|
228
|
+
// The prepare script, where there is one, runs on the build before it - and only once it passes does the suite run.
|
|
221
229
|
const testingScripts = [
|
|
230
|
+
...(scripts.prepare !== undefined
|
|
231
|
+
? [{ path: relative(root, scripts.prepare), runs: "<script> <copy of the build>, before every run of the conformance testing script", content: readFileSync(scripts.prepare, "utf8"), prepare: true }]
|
|
232
|
+
: []),
|
|
222
233
|
{ path: relative(root, scripts.conformance), runs: "<script> <copy of the build> <copy of the whole conformance suite>", content: readFileSync(scripts.conformance, "utf8") },
|
|
223
234
|
];
|
|
224
235
|
// The acceptance tests the explorer's plan has to carry out, step by step.
|
|
225
|
-
const toCarryOut =
|
|
236
|
+
const toCarryOut = carriedOut
|
|
226
237
|
.map((item) => ({ module: moduleName, frid: item.frid, count: item.acceptanceTests.length }))
|
|
227
238
|
.filter((item) => item.count > 0);
|
|
228
239
|
let invocation = await exploreInvocation({ buildDir, spec: specContext, scope, testingScripts, acceptance: toCarryOut, label, model: options.model, onDelta: options.onDelta });
|
|
@@ -314,6 +325,7 @@ export async function renderConformanceTests(options) {
|
|
|
314
325
|
],
|
|
315
326
|
toolNames: ["run_unit_tests", "run_conformance_tests", ...fixerServices.names, "update_notes"],
|
|
316
327
|
checkEnvironment: { run: () => runUnitScript(buildDir, scripts.unit), script: scripts.unit },
|
|
328
|
+
testingScripts: { scripts, root },
|
|
317
329
|
});
|
|
318
330
|
fixerConformanceRuns.newTurn();
|
|
319
331
|
return ask(fixer, withNotes(text, buildDir), options.onDelta);
|
|
@@ -414,6 +426,15 @@ export async function renderConformanceTests(options) {
|
|
|
414
426
|
abandonWork(start, moduleName, "the conformance testing script could not run");
|
|
415
427
|
throw lacking;
|
|
416
428
|
}
|
|
429
|
+
// A failure escalated to the code, still the same after the code's fix: neither agent could fix it, and the run stops
|
|
430
|
+
// with why the judge found it the code's. Anything else it fails on is a new failure, judged as any other.
|
|
431
|
+
if (escalated !== undefined && recheck.exitCode !== 0 && failureSignature(recheck) === escalated.signature) {
|
|
432
|
+
escalation = { reason: escalated.reason, evidence: escalated.evidence, error: failureLine(recheck), failure: truncateOutput(recheck.output, undefined).slice(-4000) };
|
|
433
|
+
stage("the failure escalated to the code is still there after the coding agent's fix - stopping");
|
|
434
|
+
recordNote(conformanceDir, "pitfalls", `stopped after ${rounds} attempts: failed with ${failureLine(recheck)}, escalated to the code - ${escalated.reason.replace(/\s+/g, " ").slice(0, 240)} - and the code's fix did not change it`, `${label} conformance`);
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
escalated = undefined;
|
|
417
438
|
if (recheck.exitCode !== 0) {
|
|
418
439
|
// Nothing changing: the same failure, taken the same way, round after round. Once, the explorer
|
|
419
440
|
// is asked what the tests lack - most often how they reach the software; after that, it stops.
|
|
@@ -480,19 +501,6 @@ export async function renderConformanceTests(options) {
|
|
|
480
501
|
idleFixes = 0;
|
|
481
502
|
continue;
|
|
482
503
|
}
|
|
483
|
-
// A run out of time is not a question of test or code: the tests' environment is the conformance agent's.
|
|
484
|
-
if (recheck.timedOut === true) {
|
|
485
|
-
const moved = await unmoved();
|
|
486
|
-
if (moved === "stuck")
|
|
487
|
-
break;
|
|
488
|
-
if (moved === "asked")
|
|
489
|
-
continue;
|
|
490
|
-
stage("the conformance run did not get to the tests - back to the conformance agent");
|
|
491
|
-
const fixed = await changing(() => askConformance(scriptFailure(recheck, "conformance testing"), options.onDelta));
|
|
492
|
-
response = fixed.result;
|
|
493
|
-
recordAttempt(rounds, recheck, `not a question of code or tests (${recheck.timedOut === true ? "the time limit" : `exit ${recheck.exitCode}`})`, { who: "conformance agent", files: fixed.files });
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
496
504
|
stage(`conformance tests failing - working out whether the code, the tests or the spec are wrong${judgeModel ? ` (${judgeModel})` : ""}`);
|
|
497
505
|
// A fix that changed no file leaves the next run as this one: IDLE_FIXES of them in a row, and nothing will change.
|
|
498
506
|
const idle = (files) => {
|
|
@@ -504,6 +512,12 @@ export async function renderConformanceTests(options) {
|
|
|
504
512
|
recordNote(conformanceDir, "pitfalls", `stuck after ${rounds} attempts: failed with ${failureLine(recheck)}, and ${IDLE_FIXES} fixes in a row changed nothing`, `${label} conformance`);
|
|
505
513
|
return true;
|
|
506
514
|
};
|
|
515
|
+
// The tests were fixed for this very failure, and it is still here: the judge weighs whether it is the code's.
|
|
516
|
+
const signature = failureSignature(recheck);
|
|
517
|
+
const escalating = testsFix !== undefined && testsFix.signature === signature ? { reason: testsFix.reason, changed: testsFix.files } : undefined;
|
|
518
|
+
testsFix = undefined;
|
|
519
|
+
if (escalating !== undefined)
|
|
520
|
+
stage("the tests' fix did not make the failure go away - the judge weighs whether it is the code's");
|
|
507
521
|
const verdict = await judgeFailure({
|
|
508
522
|
buildDir,
|
|
509
523
|
conformanceDir,
|
|
@@ -513,11 +527,22 @@ export async function renderConformanceTests(options) {
|
|
|
513
527
|
scope,
|
|
514
528
|
// The tests' notes - every attempt so far - and the code's: it weighs the one against the other.
|
|
515
529
|
notes: [notesToRead(conformanceDir), notesToRead(buildDir)].filter((part) => part !== "").join("\n\n"),
|
|
530
|
+
// The script itself, so the judge checks the explorer's account against it rather than repeating it.
|
|
531
|
+
testingScripts,
|
|
532
|
+
...(escalating !== undefined ? { escalation: escalating } : {}),
|
|
516
533
|
label,
|
|
517
534
|
model: judgeModel,
|
|
518
535
|
onDelta: options.onDelta,
|
|
519
536
|
});
|
|
520
537
|
const reason = verdict.reason.split("\n")[0].slice(0, 160);
|
|
538
|
+
// Stopped by the time limit while it was still getting on: no agent can give it more time. The pulse stops now -
|
|
539
|
+
// rolled back, no retry - and the person is told how to raise the limit.
|
|
540
|
+
if (verdict.culprit === "environment" && recheck.timedOut === true) {
|
|
541
|
+
stage(`the conformance run needs more than the ${Math.round(recheck.timeoutMs / 1000)}s a test run may take - stopping`);
|
|
542
|
+
if (start !== undefined)
|
|
543
|
+
abandonWork(start, moduleName, "the conformance testing script needs more time than a test run may take");
|
|
544
|
+
throw timeLimitError(recheck, { moduleName, script: scripts.conformance, kind: "conformance", reason: verdict.reason });
|
|
545
|
+
}
|
|
521
546
|
// The environment - what the run is given - is the person's to fix: once judged so twice in a row, the run stops.
|
|
522
547
|
if (verdict.culprit === "environment") {
|
|
523
548
|
environmentRaises += 1;
|
|
@@ -585,15 +610,34 @@ export async function renderConformanceTests(options) {
|
|
|
585
610
|
recordAttempt(rounds, recheck, `a spec conflict between functionalities ${key}: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
|
|
586
611
|
}
|
|
587
612
|
else if (verdict.culprit === "code") {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
613
|
+
if (escalating !== undefined) {
|
|
614
|
+
// Escalated: the coding agent is handed the script's error, the tests as they stand and the judge's evidence.
|
|
615
|
+
// Its fix is the last attempt at this failure - should the run fail the same way after it, the run stops.
|
|
616
|
+
const evidence = verdict.evidence ?? [];
|
|
617
|
+
reportActivity(` escalated to the code: ${reason}`);
|
|
618
|
+
stage("the judge finds the failure the code's - escalated to the coding agent");
|
|
619
|
+
const tests = listFiles(conformanceDir).map((path) => ({ path, content: readFileSync(join(conformanceDir, path), "utf8") }));
|
|
620
|
+
const fixed = await changing(() => askFixer(escalatedFixPrompt({ run: recheck, reason: verdict.reason, evidence, tests })));
|
|
621
|
+
recordAttempt(rounds, recheck, `escalated to the code: ${verdict.reason}`, { who: "coding agent", files: fixed.files });
|
|
622
|
+
if (fixed.files.length === 0) {
|
|
623
|
+
escalation = { reason: verdict.reason, evidence, error: failureLine(recheck), failure: truncateOutput(recheck.output, undefined).slice(-4000), changedNothing: true };
|
|
624
|
+
stage("the coding agent changed nothing for the failure escalated to it - stopping");
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
idleFixes = 0;
|
|
628
|
+
escalated = { signature, reason: verdict.reason, evidence };
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
reportActivity(` the code: ${reason}`);
|
|
632
|
+
stage("the code is wrong - the coding agent fixes it");
|
|
633
|
+
// One fixer for the run, starting each fix from what the ones before it tried: its work is compacted as
|
|
634
|
+
// it gets longer, so a session carried through the run does not re-read every earlier fix and suite output
|
|
635
|
+
// whole.
|
|
636
|
+
const fixed = await changing(() => askFixer(codeFixPrompt({ run: recheck, reason: verdict.reason })));
|
|
637
|
+
recordAttempt(rounds, recheck, `the code wrong: ${verdict.reason}`, { who: "coding agent", files: fixed.files });
|
|
638
|
+
if (idle(fixed.files))
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
597
641
|
// The fix has to build and pass its own unit tests before the suite runs again: a build
|
|
598
642
|
// error is the coding agent's to fix, not a judge's to work out from the suite's output.
|
|
599
643
|
for (let again = 0; again < FIX_UNIT_CHECKS; again += 1) {
|
|
@@ -613,7 +657,11 @@ export async function renderConformanceTests(options) {
|
|
|
613
657
|
const fixed = await changing(() => askConformance(testsFixPrompt({ run: recheck, reason: verdict.reason }), options.onDelta));
|
|
614
658
|
response = fixed.result;
|
|
615
659
|
recordAttempt(rounds, recheck, `the tests wrong: ${verdict.reason}`, { who: "conformance agent", files: fixed.files });
|
|
616
|
-
|
|
660
|
+
testsFix = { signature, reason: verdict.reason, files: fixed.files };
|
|
661
|
+
// A fix that changed nothing does not stop the run before the judge has weighed escalating the failure.
|
|
662
|
+
if (escalating === undefined)
|
|
663
|
+
idleFixes = fixed.files.length === 0 ? idleFixes + 1 : 0;
|
|
664
|
+
else if (idle(fixed.files))
|
|
617
665
|
break;
|
|
618
666
|
}
|
|
619
667
|
continue;
|
|
@@ -646,7 +694,7 @@ export async function renderConformanceTests(options) {
|
|
|
646
694
|
buildDir,
|
|
647
695
|
conformanceDir,
|
|
648
696
|
spec: specContext,
|
|
649
|
-
functionalities:
|
|
697
|
+
functionalities: carriedOut,
|
|
650
698
|
forChange,
|
|
651
699
|
invocation,
|
|
652
700
|
label,
|
|
@@ -772,6 +820,7 @@ export async function renderConformanceTests(options) {
|
|
|
772
820
|
...(specConflict !== undefined ? { specConflict } : {}),
|
|
773
821
|
...(stuck !== undefined ? { stuck } : {}),
|
|
774
822
|
...(environment !== undefined ? { environment } : {}),
|
|
823
|
+
...(escalation !== undefined ? { escalation } : {}),
|
|
775
824
|
...(unlabelled !== undefined ? { unlabelled } : {}),
|
|
776
825
|
...(reviewWaived !== undefined ? { reviewWaived } : {}),
|
|
777
826
|
...(verified && green !== undefined ? { green } : {}),
|
|
@@ -905,9 +954,10 @@ export function failureLine(run) {
|
|
|
905
954
|
}
|
|
906
955
|
/**
|
|
907
956
|
* The spec as the explorer, the judge and the coding agent are shown it - its functional specs
|
|
908
|
-
* numbered with their ids, so a spec conflict can name them
|
|
957
|
+
* numbered with their ids, so a spec conflict can name them, and the acceptance tests the run carries
|
|
958
|
+
* out labelled as their tests are, so a verdict can quote them.
|
|
909
959
|
*/
|
|
910
|
-
function specText(specifications, moduleName, frids, required, diff, modules) {
|
|
960
|
+
function specText(specifications, moduleName, frids, required, acceptance, diff, modules) {
|
|
911
961
|
// Every functionality in the codebase, each with its module: the required modules' first, then this one's.
|
|
912
962
|
const numbered = (key, items) => key === FUNCTIONAL_REQUIREMENTS
|
|
913
963
|
? [
|
|
@@ -915,10 +965,14 @@ function specText(specifications, moduleName, frids, required, diff, modules) {
|
|
|
915
965
|
]
|
|
916
966
|
: items;
|
|
917
967
|
// Each section in its tag, as every agent is shown it (see specPrimer).
|
|
918
|
-
const sections = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS
|
|
968
|
+
const sections = [DEFINITIONS, NON_FUNCTIONAL_REQUIREMENTS, TEST_REQUIREMENTS, FUNCTIONAL_REQUIREMENTS]
|
|
919
969
|
.map((key) => [key, specifications[key] ?? []])
|
|
920
970
|
.filter(([, items]) => items.length > 0)
|
|
921
|
-
.map(([key, items]) => (key === FUNCTIONAL_REQUIREMENTS
|
|
971
|
+
.map(([key, items]) => (key === FUNCTIONAL_REQUIREMENTS ? section(key, numbered(key, items)) : moduleSection(key, items, moduleName, modules)));
|
|
972
|
+
// Not only the last functionality's: a spec change's run carries out the acceptance tests of every one it touches.
|
|
973
|
+
const tests = acceptance.flatMap((item) => item.acceptanceTests.map((test, index) => `${acceptanceLabel(item.frid, index + 1)}: ${acceptanceText(test)}`));
|
|
974
|
+
if (tests.length > 0)
|
|
975
|
+
sections.push(section(ACCEPTANCE_TESTS, tests));
|
|
922
976
|
if (diff !== undefined)
|
|
923
977
|
sections.push(specChangeBlock(diff));
|
|
924
978
|
return sections.join("\n\n");
|
|
@@ -4,6 +4,7 @@ import { Type } from "typebox";
|
|
|
4
4
|
import { ask, BEGIN, createExecutor } from "../pi.js";
|
|
5
5
|
import { confinedTools } from "./confine.js";
|
|
6
6
|
import { createWebTools, WEB_TOOLS } from "./web.js";
|
|
7
|
+
import { runTiming } from "./test-scripts.js";
|
|
7
8
|
/**
|
|
8
9
|
* A fresh look at a failure the coding agent keeps hitting.
|
|
9
10
|
*
|
|
@@ -24,9 +25,12 @@ export function diagnosisText(diagnosis) {
|
|
|
24
25
|
}
|
|
25
26
|
function debugPrompt(input) {
|
|
26
27
|
const script = input.script !== undefined && existsSync(input.script) ? readFileSync(input.script, "utf8") : undefined;
|
|
28
|
+
const timedOut = input.run?.timedOut === true;
|
|
27
29
|
return [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
timedOut
|
|
31
|
+
? `The unit testing script of the codebase in ${input.buildDir} did not finish: the time limit stopped it. You look at it with fresh eyes.`
|
|
32
|
+
: `The unit tests of the codebase in ${input.buildDir} have failed the same way ${input.times} times in a row. The coding agent`,
|
|
33
|
+
...(timedOut ? [] : ["keeps changing things and the failure does not move: it has not seen the cause. You look at it with fresh eyes."]),
|
|
30
34
|
"",
|
|
31
35
|
"Find the root cause - the one thing that makes this failure happen - and the change that fixes it. Read the failing",
|
|
32
36
|
"run below, the tests it names, the code they exercise, the build files (pyproject.toml, package.json, go.mod, pom.xml,",
|
|
@@ -37,6 +41,21 @@ function debugPrompt(input) {
|
|
|
37
41
|
"",
|
|
38
42
|
"You change nothing - you only read. When you know, call submit_diagnosis: the root cause, with the evidence for it,",
|
|
39
43
|
"and the fix - which files, and exactly what to change in them.",
|
|
44
|
+
...(timedOut
|
|
45
|
+
? [
|
|
46
|
+
"",
|
|
47
|
+
"<timed out>",
|
|
48
|
+
"The run's output ends where the time limit stopped it: the test running then, or the step after the last one it",
|
|
49
|
+
"printed, is what did not finish. Find what kept it:",
|
|
50
|
+
"- something hangs - a test or the code waits on input, keeps a server in the foreground, never exits, loops, blocks",
|
|
51
|
+
" on a lock or a network call that never answers: the root cause and the fix, as for any failure;",
|
|
52
|
+
"- nothing hangs - the output shows the run making steady progress right up to the limit (installing, building, test",
|
|
53
|
+
" after test passing) and it needs more time than a test run may take: set too_slow, and say in root_cause from the",
|
|
54
|
+
" output how far it got and what took the time. No agent can give it more time: the work stops, and the person is",
|
|
55
|
+
" told to raise the limit. Judge from the output and the files, never from the time alone.",
|
|
56
|
+
"</timed out>",
|
|
57
|
+
]
|
|
58
|
+
: []),
|
|
40
59
|
"",
|
|
41
60
|
"<what the change is meant to do>",
|
|
42
61
|
input.task.trim(),
|
|
@@ -44,6 +63,7 @@ function debugPrompt(input) {
|
|
|
44
63
|
...(script !== undefined ? ["", `<testing script path="${input.script}">`, script.trimEnd(), "</testing script>"] : []),
|
|
45
64
|
"",
|
|
46
65
|
"<failing run>",
|
|
66
|
+
...(input.run !== undefined ? [runTiming(input.run), ""] : []),
|
|
47
67
|
input.failure.trimEnd().slice(-8000),
|
|
48
68
|
"</failing run>",
|
|
49
69
|
].join("\n");
|
|
@@ -58,10 +78,14 @@ export async function debugFailure(input) {
|
|
|
58
78
|
parameters: Type.Object({
|
|
59
79
|
root_cause: Type.String({ description: "The one thing that makes the failure happen, and the evidence for it: the lines of the run, the code, the docs" }),
|
|
60
80
|
fix: Type.String({ description: "The change that fixes it: which files, and exactly what to change in them" }),
|
|
81
|
+
too_slow: Type.Optional(Type.Boolean({
|
|
82
|
+
description: "Only for a run the time limit stopped: true when nothing hangs - it was making progress up to the limit and needs more time than a test run may take",
|
|
83
|
+
})),
|
|
61
84
|
}),
|
|
62
85
|
async execute(_id, params) {
|
|
63
86
|
const given = params;
|
|
64
|
-
|
|
87
|
+
// Too slow is a verdict on a run the limit stopped, and on no other.
|
|
88
|
+
found = { rootCause: given.root_cause, fix: given.fix, ...(given.too_slow === true && input.run?.timedOut === true ? { tooSlow: true } : {}) };
|
|
65
89
|
return { content: [{ type: "text", text: "Recorded." }], details: {}, terminate: true };
|
|
66
90
|
},
|
|
67
91
|
});
|
|
@@ -224,6 +224,23 @@ export function toolchainError(missing, details) {
|
|
|
224
224
|
source: "harness",
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* A testing script the time limit stopped while it was still making progress - judged so by the judge or the debugger,
|
|
229
|
+
* from where its output stops: nothing hangs, the suite needs more time than a test run may take. No agent can give it
|
|
230
|
+
* that, so the pulse stops and the person is told how to.
|
|
231
|
+
*/
|
|
232
|
+
export function timeLimitError(run, details) {
|
|
233
|
+
const limit = Math.round(run.timeoutMs / 1000);
|
|
234
|
+
return new EnvironmentError({
|
|
235
|
+
moduleName: details.moduleName,
|
|
236
|
+
missing: `more time for a test run than its ${limit}s limit`,
|
|
237
|
+
neededBy: `the ${details.kind} testing script, \`${basename(details.script)}\``,
|
|
238
|
+
shownBy: `it ran ${Math.round((run.elapsedMs ?? run.timeoutMs) / 1000)}s and was stopped at the limit, still making progress - ${details.reason.replace(/\s+/g, " ").trim().slice(0, 400)}`,
|
|
239
|
+
fix: `give a test run more time - set TURRI_COMMAND_TIMEOUT, in seconds, above ${limit} (e.g. TURRI_COMMAND_TIMEOUT=${limit * 3}) in the ` +
|
|
240
|
+
"environment turri runs in - or make the suite faster: what it installs or builds on every run, and the tests that take longest.",
|
|
241
|
+
source: "agent",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
227
244
|
/** The harness's run of a testing script, when it could not find its toolchain: the error that stops the pulse. */
|
|
228
245
|
export function missingToolchainError(run, details) {
|
|
229
246
|
const missing = missingToolchain(run);
|
package/dist/regenerate/loop.js
CHANGED
|
@@ -16,7 +16,7 @@ import { modulesRoot } from "./paths.js";
|
|
|
16
16
|
import { budgetFor, choosingWhen, configForSpec, conformanceSwitchedOn } from "../config.js";
|
|
17
17
|
import { describeSpecChange } from "./prompt.js";
|
|
18
18
|
import { resolveTemplateDirs } from "./paths.js";
|
|
19
|
-
import { builtTips, copyRequiredCode, newlyRequired, resolveRequiresChain, startFromScratch } from "./requires.js";
|
|
19
|
+
import { builtTips, copyRequiredCode, importingModules, isImportModule, newlyRequired, resolveRequiresChain, startFromScratch } from "./requires.js";
|
|
20
20
|
import { withTryItLine } from "./delivery.js";
|
|
21
21
|
import { describeSpecDiff, renderSpecDiff, specFilesOf } from "./regenerate.js";
|
|
22
22
|
import { locateDiffLines } from "./diff-view.js";
|
|
@@ -85,6 +85,23 @@ function chainsFor(plainFile) {
|
|
|
85
85
|
const tips = builtTips(plainFile);
|
|
86
86
|
return tips.length > 0 ? tips : [resolveRequiresChain(plainFile, [], undefined, { allowNoFunctionalities: true })];
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* The modules a .plain file is built as: itself - or, an import module, the modules importing it whose code is started.
|
|
90
|
+
* An import module is no codebase; one no started module imports has nothing to build yet - its reqs are built with the
|
|
91
|
+
* first module that imports it.
|
|
92
|
+
*/
|
|
93
|
+
function codebasesOf(plainFile) {
|
|
94
|
+
if (!isImportModule(plainFile))
|
|
95
|
+
return [plainFile];
|
|
96
|
+
return importingModules(plainFile).filter((file) => {
|
|
97
|
+
try {
|
|
98
|
+
return builtTips(file).length > 0 || started(resolveRequiresChain(file, [], undefined, { allowNoFunctionalities: true }).at(-1));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
88
105
|
/** How many functional specs of its own a codebase has built - none when it was only started from the code below it. */
|
|
89
106
|
function ownBuilt(entry) {
|
|
90
107
|
return (readRecord(entry.buildDir)?.[FUNCTIONAL_REQUIREMENTS] ?? []).length;
|
|
@@ -989,9 +1006,10 @@ export function createWorkLoop(options) {
|
|
|
989
1006
|
};
|
|
990
1007
|
const loop = {
|
|
991
1008
|
async bringUp(plainFile, request = {}) {
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
1009
|
+
const files = codebasesOf(resolve(options.cwd, plainFile));
|
|
1010
|
+
for (const file of files)
|
|
1011
|
+
asked.set(file, { ...asked.get(file), ...request });
|
|
1012
|
+
const states = withParseCache(() => files.flatMap((file) => chainsFor(file).flatMap((chain) => toBuild(chain).map((entry) => track(chain.slice(0, chain.indexOf(entry) + 1), file, request)))));
|
|
995
1013
|
// Asked again, a halted codebase is tried again.
|
|
996
1014
|
for (const state of states)
|
|
997
1015
|
state.halted = undefined;
|
|
@@ -278,8 +278,10 @@ function itemText(serialized) {
|
|
|
278
278
|
return item;
|
|
279
279
|
const { markdown, [ACCEPTANCE_TESTS]: tests } = (item ?? {});
|
|
280
280
|
const lines = [markdown ?? JSON.stringify(item)];
|
|
281
|
+
// Each test is an item of the tree, its text in its markdown - as spec-steps.ts `itemText` reads it.
|
|
282
|
+
const testText = (test) => (typeof test === "string" ? test : (test?.markdown ?? JSON.stringify(test)));
|
|
281
283
|
if (Array.isArray(tests) && tests.length > 0)
|
|
282
|
-
lines.push(" <acceptance tests>", ...tests.map((test) => ` ${
|
|
284
|
+
lines.push(" <acceptance tests>", ...tests.map((test) => ` ${testText(test).replace(/\n/g, "\n ")}`), " </acceptance tests>");
|
|
283
285
|
return lines.join("\n");
|
|
284
286
|
}
|
|
285
287
|
const indent = (text, by) => text.split("\n").map((line) => `${by}${line}`).join("\n");
|
|
@@ -20,7 +20,7 @@ import { createWebTools, WEB_TOOLS } from "./web.js";
|
|
|
20
20
|
import { createCanItFailTool, createMyDiffTool, selfCheckNote } from "./self-check.js";
|
|
21
21
|
import { createServiceTools } from "./services.js";
|
|
22
22
|
import { createUnitGate } from "./unit-gate.js";
|
|
23
|
-
import { toolchainError } from "./environment-error.js";
|
|
23
|
+
import { timeLimitError, toolchainError } from "./environment-error.js";
|
|
24
24
|
import { workspaceBlock } from "./workspace.js";
|
|
25
25
|
import { ATTEMPTS, describeStop } from "./stop-report.js";
|
|
26
26
|
import { summarizeDiff } from "./job-panel.js";
|
|
@@ -333,6 +333,8 @@ export async function renderSpecDiff(options) {
|
|
|
333
333
|
let draftAsks = 0;
|
|
334
334
|
/** What the unit testing script needs and does not find, last run. */
|
|
335
335
|
let missing;
|
|
336
|
+
/** The unit testing script needs more time than a test run may take - the debugger's finding: the pulse stops on it. */
|
|
337
|
+
let slow;
|
|
336
338
|
/** The last round failed on the script's static checks alone: the unit tests passed. */
|
|
337
339
|
let staticOnly = false;
|
|
338
340
|
const testRemovals = [];
|
|
@@ -423,24 +425,35 @@ export async function renderSpecDiff(options) {
|
|
|
423
425
|
seenFailures.set(key, seen);
|
|
424
426
|
let prompt = verdict.prompt;
|
|
425
427
|
const circling = loop.stuck;
|
|
426
|
-
|
|
428
|
+
// A run the time limit stopped is looked at at once: whether something hangs, or the suite only needs more time.
|
|
429
|
+
const timedOut = verdict.run.timedOut === true;
|
|
430
|
+
if (circling || timedOut || (key !== undefined && seen >= STUCK_ROUNDS)) {
|
|
427
431
|
if (key !== undefined)
|
|
428
432
|
seenFailures.delete(key);
|
|
429
433
|
const times = circling ? Math.max(seen, loop.failing) : seen;
|
|
430
434
|
loop.reset();
|
|
431
435
|
stage(circling
|
|
432
436
|
? `the coding agent is going round in circles - ${times} failing runs - a debugger looks at it with fresh eyes`
|
|
433
|
-
:
|
|
437
|
+
: timedOut && seen < STUCK_ROUNDS
|
|
438
|
+
? `the unit testing script did not finish in ${Math.round(verdict.run.timeoutMs / 1000)}s - a debugger works out whether something hangs`
|
|
439
|
+
: `the same failure ${seen} times - a debugger looks at it with fresh eyes`);
|
|
434
440
|
const found = await debugFailure({
|
|
435
441
|
buildDir,
|
|
436
442
|
failure: verdict.run.output,
|
|
437
443
|
times,
|
|
438
444
|
task: steeredChange() ?? codingText ?? changeText ?? options.diff ?? "",
|
|
439
445
|
script: verdict.script,
|
|
446
|
+
run: verdict.run,
|
|
440
447
|
label: `${moduleName} spec change`,
|
|
441
448
|
...(debuggerModel !== undefined ? { model: debuggerModel } : {}),
|
|
442
449
|
onDelta: options.onDelta,
|
|
443
450
|
}).catch(() => undefined);
|
|
451
|
+
// Nothing hangs: the suite needs more time than a test run may take, and no agent can give it that.
|
|
452
|
+
if (found?.tooSlow === true) {
|
|
453
|
+
stage(`the unit testing script needs more than the ${Math.round(verdict.run.timeoutMs / 1000)}s a test run may take - stopping`);
|
|
454
|
+
slow = timeLimitError(verdict.run, { moduleName, script: verdict.script, kind: "unit", reason: found.rootCause });
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
444
457
|
if (found !== undefined) {
|
|
445
458
|
const cause = found.rootCause.split("\n")[0].slice(0, 240);
|
|
446
459
|
reportActivity(` the debugger: ${cause.slice(0, 160)}`);
|
|
@@ -486,6 +499,7 @@ export async function renderSpecDiff(options) {
|
|
|
486
499
|
...(options.cases !== undefined ? { cases: options.cases } : {}),
|
|
487
500
|
label: `${moduleName} spec change`,
|
|
488
501
|
...(scripts.runOn === "copy" ? { unitScript: scripts.unit } : {}),
|
|
502
|
+
testingScript: { path: relative(root, scripts.unit), content: readFileSync(scripts.unit, "utf8") },
|
|
489
503
|
model: options.judgeModel ?? modelSpec(inheritModel(configForSpec(tip.plainFile).judgeModel, modelOnly(options.model))),
|
|
490
504
|
send: async (next) => {
|
|
491
505
|
response = await ask(executor, next, options.onDelta);
|
|
@@ -543,13 +557,17 @@ export async function renderSpecDiff(options) {
|
|
|
543
557
|
stage(unitReviewStopped ? "the unit tests pass but still leave the change untested - undoing the change" : "unit tests NOT passing - undoing the change");
|
|
544
558
|
abandonWork(unit, moduleName, missing !== undefined
|
|
545
559
|
? "the unit testing script could not run"
|
|
546
|
-
:
|
|
547
|
-
? "the
|
|
548
|
-
:
|
|
560
|
+
: slow !== undefined
|
|
561
|
+
? "the unit testing script needs more time than a test run may take"
|
|
562
|
+
: staticOnly
|
|
563
|
+
? "the spec change did not pass its static checks"
|
|
564
|
+
: "the spec change did not pass its unit tests");
|
|
549
565
|
// The environment lacks what the testing script needs: the pulse stops, rolled back, and the person is told.
|
|
550
566
|
if (missing !== undefined) {
|
|
551
567
|
throw toolchainError(missing, { moduleName, script: scripts.unit, kind: "unit" });
|
|
552
568
|
}
|
|
569
|
+
if (slow !== undefined)
|
|
570
|
+
throw slow;
|
|
553
571
|
return { ...base, passed: false, stoppedAt: "unit tests", ...(staticOnly && !unitReviewStopped ? { staticChecks: true } : {}) };
|
|
554
572
|
}
|
|
555
573
|
stage("unit tests pass - landing the change on them");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
-
import { PLAIN_SOURCE_FILE_EXTENSION, plainFileParser, withParseCache } from "../plain/parser.js";
|
|
3
|
+
import { IMPORT_DIRECTIVE, PLAIN_SOURCE_FILE_EXTENSION, plainFileParser, readPlainSourceMetadata, withParseCache } from "../plain/parser.js";
|
|
4
4
|
import { getFrids, getInheritedFunctionalSpecs } from "../plain/spec.js";
|
|
5
5
|
import { buildDirUnder, modulesRoot, projectDirOf } from "./paths.js";
|
|
6
6
|
import { notesPath } from "./notes.js";
|
|
@@ -54,6 +54,59 @@ options = {}) {
|
|
|
54
54
|
visit(resolve(plainFile), []);
|
|
55
55
|
return chain;
|
|
56
56
|
}
|
|
57
|
+
/** The .plain files that import `plainFile` - read from beside it, and, kept in a `template/` folder, from the folder it is in. */
|
|
58
|
+
function importersOf(plainFile) {
|
|
59
|
+
const file = resolve(plainFile);
|
|
60
|
+
const dir = dirname(file);
|
|
61
|
+
const name = basename(file, PLAIN_SOURCE_FILE_EXTENSION);
|
|
62
|
+
const dirs = [dir, ...(basename(dir) === "template" ? [dirname(dir)] : [])];
|
|
63
|
+
return dirs.flatMap((at) => {
|
|
64
|
+
let names;
|
|
65
|
+
try {
|
|
66
|
+
names = readdirSync(at).filter((entry) => entry.endsWith(PLAIN_SOURCE_FILE_EXTENSION));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
return names
|
|
72
|
+
.map((entry) => join(at, entry))
|
|
73
|
+
.filter((path) => {
|
|
74
|
+
if (path === file || !resolveTemplateDirs(path).includes(dir))
|
|
75
|
+
return false;
|
|
76
|
+
try {
|
|
77
|
+
return (readPlainSourceMetadata(readFileSync(path, "utf8")).metadata[IMPORT_DIRECTIVE] ?? []).includes(name);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether a .plain file is an import module - one a .plain file imports, or one kept in a `template/` folder - rather than
|
|
87
|
+
* a module of its own. It is no codebase: its reqs and definitions are built in the modules that import it.
|
|
88
|
+
*/
|
|
89
|
+
export function isImportModule(plainFile) {
|
|
90
|
+
return basename(dirname(resolve(plainFile))) === "template" || importersOf(plainFile).length > 0;
|
|
91
|
+
}
|
|
92
|
+
/** The modules of their own that import an import module, through any import modules between. */
|
|
93
|
+
export function importingModules(plainFile) {
|
|
94
|
+
const found = new Set();
|
|
95
|
+
const seen = new Set();
|
|
96
|
+
const walk = (file) => {
|
|
97
|
+
if (seen.has(file))
|
|
98
|
+
return;
|
|
99
|
+
seen.add(file);
|
|
100
|
+
for (const importer of importersOf(file)) {
|
|
101
|
+
if (isImportModule(importer))
|
|
102
|
+
walk(importer);
|
|
103
|
+
else
|
|
104
|
+
found.add(importer);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
walk(resolve(plainFile));
|
|
108
|
+
return [...found];
|
|
109
|
+
}
|
|
57
110
|
/**
|
|
58
111
|
* The codebase a change to `plainFile`'s module lands in: the furthest-built module whose
|
|
59
112
|
* requires chain includes it, with that chain.
|
|
@@ -89,6 +89,19 @@ export function describeStop(facts) {
|
|
|
89
89
|
`though they were sent back for it again and again. Still missing:\n${facts.conformance.unlabelled.map((line) => ` - ${line}`).join("\n")}`;
|
|
90
90
|
todo = ["If the test reqs leave open where the tests go or how they are built, say so exactly - the folder, the files, the build."];
|
|
91
91
|
}
|
|
92
|
+
else if (facts.at === "conformance" && facts.conformance?.escalation !== undefined) {
|
|
93
|
+
const { error, reason, evidence, changedNothing } = facts.conformance.escalation;
|
|
94
|
+
why =
|
|
95
|
+
`its conformance tests kept failing with ${error}. The conformance agent's fix of the tests did not make it go away, so ` +
|
|
96
|
+
`a judge looked again and found it the code's; the coding agent ${changedNothing === true ? "changed nothing for it" : "fixed the code, and the run still failed the same way"}. ` +
|
|
97
|
+
`Why the judge found it the code's:\n${reason.trim().split("\n").map((line) => ` ${line}`).join("\n")}` +
|
|
98
|
+
(evidence.length > 0 ? `\n On the evidence:\n${evidence.map((item) => ` - ${item.source.trim()}: ${item.shows.trim().replace(/\s+/g, " ")}`).join("\n")}` : "");
|
|
99
|
+
todo = [
|
|
100
|
+
"If the code has to produce something the spec leaves open - a build output, an entry point, a module the conformance testing " +
|
|
101
|
+
"script runs or checks for - say exactly what in the implementation reqs: its path and how it is built.",
|
|
102
|
+
"If the conformance testing script expects something the spec does not ask for, change the script or the test reqs so the two agree.",
|
|
103
|
+
];
|
|
104
|
+
}
|
|
92
105
|
else if (facts.at === "conformance" && facts.conformance?.stuck !== undefined) {
|
|
93
106
|
const { error, times, rounds, changedNothing } = facts.conformance.stuck;
|
|
94
107
|
why =
|
|
@@ -335,7 +335,15 @@ async function runScript(cwd, script, args, logDir, options = {}) {
|
|
|
335
335
|
const noted = withScriptLineNote(text, script);
|
|
336
336
|
// Its failures, counts and end are what the agent is shown; the whole is kept, a read_output away.
|
|
337
337
|
const log = keepTestOutput(logDir, basename(script), noted);
|
|
338
|
-
return {
|
|
338
|
+
return {
|
|
339
|
+
command: shown(script),
|
|
340
|
+
exitCode: timedOut === true ? 124 : exitCode,
|
|
341
|
+
output: shownTestOutput(noted, log),
|
|
342
|
+
timedOut: timedOut === true,
|
|
343
|
+
timeoutMs,
|
|
344
|
+
elapsedMs: Date.now() - started,
|
|
345
|
+
...(log !== undefined ? { log } : {}),
|
|
346
|
+
};
|
|
339
347
|
}
|
|
340
348
|
/** What bash says when a line of the script itself cannot run: a command or file that is not there, or not runnable. */
|
|
341
349
|
const OWN_LINE_FAILURE = /: (No such file or directory|command not found|Permission denied)$/;
|
|
@@ -589,6 +597,19 @@ function loopNotice(loop) {
|
|
|
589
597
|
"the failure with fresh eyes, and you will be given what it finds.",
|
|
590
598
|
].join("\n");
|
|
591
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* How long a run took against its time limit, in a line: for whoever judges it or works out its cause - so a run that
|
|
602
|
+
* ended on its own is never taken for one the limit stopped, nor the other way.
|
|
603
|
+
*/
|
|
604
|
+
export function runTiming(run) {
|
|
605
|
+
const seconds = (ms) => `${(ms / 1000).toFixed(1)}s`;
|
|
606
|
+
const limit = seconds(run.timeoutMs);
|
|
607
|
+
if (run.timedOut === true) {
|
|
608
|
+
return `The time limit stopped this run: it had run ${seconds(run.elapsedMs ?? run.timeoutMs)} of the ${limit} a test run may take, and had not finished.`;
|
|
609
|
+
}
|
|
610
|
+
const took = run.elapsedMs !== undefined ? `after ${seconds(run.elapsedMs)} of the ${limit} a test run may take` : `well within the ${limit} a test run may take`;
|
|
611
|
+
return `This run ended on its own - exit ${run.exitCode}, ${took}. The time limit did not stop it and played no part in how it ended.`;
|
|
612
|
+
}
|
|
592
613
|
/** A run the time limit stopped: failed, and what most often keeps a test run from ending. */
|
|
593
614
|
function timedOutFailure(run, what) {
|
|
594
615
|
const limit = Math.round(run.timeoutMs / 1000);
|
|
@@ -63,6 +63,17 @@ export function unitReviewTask(options) {
|
|
|
63
63
|
options.spec.trim(),
|
|
64
64
|
"</specification>",
|
|
65
65
|
"",
|
|
66
|
+
...(options.testingScript !== undefined
|
|
67
|
+
? [
|
|
68
|
+
"The harness runs the unit tests with this script, and it decides whether they pass. Read it for which tests it",
|
|
69
|
+
"runs and how: a test it never reaches - a file it does not pick up, a folder it does not look in - is not run, and",
|
|
70
|
+
"checks nothing.",
|
|
71
|
+
`<unit testing script path="${options.testingScript.path}">`,
|
|
72
|
+
options.testingScript.content.trimEnd(),
|
|
73
|
+
"</unit testing script>",
|
|
74
|
+
"",
|
|
75
|
+
]
|
|
76
|
+
: []),
|
|
66
77
|
...(options.cases !== undefined ? [...codeReviewCasesBlock(options.cases), ""] : []),
|
|
67
78
|
// A job that touches no functional spec is held only to that: the checks of behaviour are not its.
|
|
68
79
|
...(mechanical ? [] : ["Check that the code does what the specification states for this work:"]),
|