unitbob 0.4.5 → 0.5.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/dist/files/guardrails.js +19 -9
- package/dist/files/suiteBuild.js +16 -1
- package/dist/files/suiteBuildUpload.js +71 -0
- package/dist/files/workerPlan.js +20 -3
- package/dist/runner/bdd.js +75 -12
- package/dist/runner/bootcheck.js +47 -9
- package/dist/runner/precheck.js +104 -36
- package/dist/runner/provision.js +326 -26
- package/dist/runner/pytest.js +25 -20
- package/dist/runner/rspec.js +19 -11
- package/dist/runner/toolchain.js +122 -0
- package/dist/runner/vitest.js +64 -30
- package/dist/surfaces/routeInventory.js +15 -3
- package/dist/verbs/codexInstall.js +51 -16
- package/dist/verbs/putSuiteBuild.js +32 -75
- package/dist/verbs/run.js +15 -6
- package/dist/verbs/runLocal.js +19 -7
- package/dist/verbs/suitePrepare.js +84 -7
- package/dist/verbs/validateBuild.js +140 -456
- package/dist/wire.js +21 -3
- package/package.json +1 -1
- package/plugin/codex/agents/suite-repair-worker.toml +1 -1
- package/plugin/codex/agents/suite-reviewer.toml +157 -0
- package/plugin/codex/agents/suite-worker.toml +9 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { collectBuildProblems
|
|
1
|
+
import { readHostSuiteOutputsPerBranch, readSuiteBuildRequest, } from "../files/suiteBuild.js";
|
|
2
|
+
import { collectBuildProblems } from "./validateBuild.js";
|
|
3
|
+
import { PUBLISHED, uploadItem, withReview } from "../files/suiteBuildUpload.js";
|
|
3
4
|
import { Wire } from "../wire.js";
|
|
4
5
|
// Read the task and the host's answers, verify each branch parses and carries a
|
|
5
6
|
// safe-path artifact envelope, then upload both peer branches in one batch
|
|
@@ -28,7 +29,6 @@ export async function putSuiteBuild(config, _args = [], deps) {
|
|
|
28
29
|
stdout: process.stdout,
|
|
29
30
|
...deps,
|
|
30
31
|
};
|
|
31
|
-
const digestFor = new Map(request.branches.map((branch) => [branch.suite_kind, branch.source_digest]));
|
|
32
32
|
const items = [];
|
|
33
33
|
const blocked = unreadable.map((entry) => ({
|
|
34
34
|
suite_kind: entry.suite_kind,
|
|
@@ -39,26 +39,19 @@ export async function putSuiteBuild(config, _args = [], deps) {
|
|
|
39
39
|
// skipped by going straight to the upload — but reported the way every other
|
|
40
40
|
// local failure here is reported: against the branch it belongs to.
|
|
41
41
|
//
|
|
42
|
+
// Since spec 42 that check is exactly one question, and it is about a branch
|
|
43
|
+
// the answer has *no* entry for: everything else it used to ask is now asked
|
|
44
|
+
// of the server, by a dry run, before this command runs at all. So its
|
|
45
|
+
// problems can never land on a branch this loop visits, and they are reported
|
|
46
|
+
// below rather than inside it.
|
|
47
|
+
//
|
|
42
48
|
// An earlier draft threw and stopped the command, which quietly undid spec
|
|
43
|
-
// 32-5 Phase 4: one
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
// batch. That also bounds what a false positive in a local check can cost —
|
|
47
|
-
// one branch, with the peer still going up and the server still the authority.
|
|
48
|
-
const problemsFor = new Map();
|
|
49
|
-
for (const problem of collectBuildProblems(request, outputs, unreadable)) {
|
|
50
|
-
problemsFor.set(problem.branch, [...(problemsFor.get(problem.branch) ?? []), problem.message]);
|
|
51
|
-
}
|
|
49
|
+
// 32-5 Phase 4: one behavioral problem would have left a finished structural
|
|
50
|
+
// suite unpublished. Every problem here is named against a branch, so it
|
|
51
|
+
// blocks that branch and never the batch.
|
|
52
52
|
for (const output of outputs) {
|
|
53
|
-
const failed = problemsFor.get(output.suite_kind);
|
|
54
|
-
problemsFor.delete(output.suite_kind);
|
|
55
|
-
if (failed) {
|
|
56
|
-
blocked.push({ suite_kind: output.suite_kind, status: BLOCKED_STATUS, error: formatBranchProblems(failed) });
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
const sourceDigest = digestFor.get(output.suite_kind) ?? '';
|
|
60
53
|
if (output.build_error) {
|
|
61
|
-
items.push(
|
|
54
|
+
items.push(uploadItem(request, output, undefined));
|
|
62
55
|
continue;
|
|
63
56
|
}
|
|
64
57
|
let testMetadata = output.test_metadata;
|
|
@@ -71,23 +64,14 @@ export async function putSuiteBuild(config, _args = [], deps) {
|
|
|
71
64
|
continue;
|
|
72
65
|
}
|
|
73
66
|
}
|
|
74
|
-
items.push(
|
|
75
|
-
suite_kind: output.suite_kind,
|
|
76
|
-
source_digest: sourceDigest,
|
|
77
|
-
artifacts: {
|
|
78
|
-
suite_file: output.suite_file,
|
|
79
|
-
runner_manifest: output.runner_manifest,
|
|
80
|
-
test_metadata: testMetadata,
|
|
81
|
-
},
|
|
82
|
-
});
|
|
67
|
+
items.push(uploadItem(request, output, testMetadata));
|
|
83
68
|
}
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
// upload and nothing to roll back, so it costs its peer nothing — but it is
|
|
69
|
+
// A branch the request asked for and the answer never mentions. It has nothing
|
|
70
|
+
// to upload and nothing to roll back, so it costs its peer nothing — but it is
|
|
87
71
|
// exactly the branch that used to leave no trace anywhere, and the one line it
|
|
88
72
|
// prints here is the whole point of noticing it (spec 32-6, a2time 2026-08-04).
|
|
89
|
-
for (const
|
|
90
|
-
blocked.push({ suite_kind:
|
|
73
|
+
for (const problem of collectBuildProblems(request, outputs, unreadable)) {
|
|
74
|
+
blocked.push({ suite_kind: problem.branch, status: BLOCKED_STATUS, error: problem.message });
|
|
91
75
|
}
|
|
92
76
|
// Every branch is blocked, so there is nothing to upload. Asking the server to
|
|
93
77
|
// publish an empty batch would turn a local, already-explained problem into a
|
|
@@ -103,46 +87,6 @@ export async function putSuiteBuild(config, _args = [], deps) {
|
|
|
103
87
|
// malformed. Not a server status — it never reaches the server — but it travels
|
|
104
88
|
// as one so a single rule decides what counts as published (see `PUBLISHED`).
|
|
105
89
|
const BLOCKED_STATUS = 'not_ready';
|
|
106
|
-
// The behavioral branch's uploaded metadata, with the independent review and the
|
|
107
|
-
// connector's own run evidence folded in.
|
|
108
|
-
//
|
|
109
|
-
// Throws for anything that leaves this branch unpublishable — a missing review,
|
|
110
|
-
// one bound to a different candidate, a defect the review called not_supplied.
|
|
111
|
-
// The caller turns that into one unpublished branch rather than a failed
|
|
112
|
-
// command: a blocked review is a fact about the behavioral suite, and the
|
|
113
|
-
// structural peer next to it is finished and correct. Sinking the whole upload
|
|
114
|
-
// with it forced the one workaround this contract exists to prevent — hand-editing
|
|
115
|
-
// the answer down to a single branch, which loses the peer candidate for real.
|
|
116
|
-
function withReview(config, request, output) {
|
|
117
|
-
const review = readBehavioralReview(config.projectRoot, output);
|
|
118
|
-
const probe = review.known_defect_probe;
|
|
119
|
-
const qualityReview = review.bdd_quality_review;
|
|
120
|
-
if (!qualityReview || typeof qualityReview !== 'object') {
|
|
121
|
-
throw new Error('The separate behavioral review must contain a bdd_quality_review object.');
|
|
122
|
-
}
|
|
123
|
-
if (request.known_defect_context.status === 'supplied' && probe?.status === 'not_supplied') {
|
|
124
|
-
throw new Error('A known defect was supplied to suite-prepare, but the behavioral review marked it not_supplied.');
|
|
125
|
-
}
|
|
126
|
-
return {
|
|
127
|
-
...output.test_metadata,
|
|
128
|
-
bdd_quality_review: {
|
|
129
|
-
...qualityReview,
|
|
130
|
-
candidate_digest: review.candidate_digest,
|
|
131
|
-
},
|
|
132
|
-
...(review.selection_review ? { selection_review: review.selection_review } : {}),
|
|
133
|
-
known_defect_probe: review.known_defect_probe,
|
|
134
|
-
known_defect_context: request.known_defect_context,
|
|
135
|
-
candidate_run: review.candidate_run,
|
|
136
|
-
...(review.fixed_candidate_run ? { fixed_candidate_run: review.fixed_candidate_run } : {}),
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
// The three outcomes that leave a branch published and current: a new version, an
|
|
140
|
-
// identical version already stored, or a reactivated one. Each returns the
|
|
141
|
-
// identity to run. Everything else — a rejected branch, a branch the host could
|
|
142
|
-
// not build, or a status this connector has never seen — fails closed and is
|
|
143
|
-
// never run, so a newer server can never trick an older connector into running
|
|
144
|
-
// something it does not understand.
|
|
145
|
-
const PUBLISHED = new Set(['created', 'unchanged', 'restored']);
|
|
146
90
|
export function classifyPublication(results) {
|
|
147
91
|
const split = { digests: [], unpublished: [] };
|
|
148
92
|
for (const result of results) {
|
|
@@ -176,7 +120,20 @@ function printResult(result) {
|
|
|
176
120
|
.join(', ')
|
|
177
121
|
: '';
|
|
178
122
|
const digest = result.suite_digest ? ` (${result.suite_digest})` : '';
|
|
179
|
-
return `${result.suite_kind}: ${result.status}${digest}${tallies ? ` — ${tallies}` : ''}
|
|
123
|
+
return (`${result.suite_kind}: ${result.status}${digest}${tallies ? ` — ${tallies}` : ''}.` + printDowngrades(result));
|
|
124
|
+
}
|
|
125
|
+
// Spec 42, §7. A capability every one of whose Scenarios the review objected to
|
|
126
|
+
// is stored `unguarded` by the publish. The run is standing right here when that
|
|
127
|
+
// is decided, so it is told here, in the server's own words — finding it on the
|
|
128
|
+
// map afterwards is how a run finishes believing it published a guarantee it did
|
|
129
|
+
// not.
|
|
130
|
+
function printDowngrades(result) {
|
|
131
|
+
const downgraded = result.unguarded_by_review ?? [];
|
|
132
|
+
if (downgraded.length === 0)
|
|
133
|
+
return '';
|
|
134
|
+
return (`\n ${downgraded.length} capability(ies) published unguarded, because the review objected to every ` +
|
|
135
|
+
'Scenario guarding them:\n' +
|
|
136
|
+
downgraded.map((entry) => ` - ${entry.capability_id}: ${entry.reason}`).join('\n'));
|
|
180
137
|
}
|
|
181
138
|
// The server's own words when it sent any; otherwise the best true thing that can
|
|
182
139
|
// be said. A status this connector does not know is quoted rather than guessed
|
package/dist/verbs/run.js
CHANGED
|
@@ -27,9 +27,12 @@ function resolve(config, deps) {
|
|
|
27
27
|
return {
|
|
28
28
|
getSuites: () => wire.getSuites(),
|
|
29
29
|
postRunsBatch: (runs) => wire.postRunsBatch(runs),
|
|
30
|
+
// The whole envelope, support files and all: a branch is a set of files
|
|
31
|
+
// since spec 42, §6, and picking `path` and `content` out of it here was
|
|
32
|
+
// where the rest of them used to be lost.
|
|
30
33
|
materializeStructural: (projectRoot, item) => materializeGuardrails(projectRoot, {
|
|
31
34
|
suite_digest: item.suite_digest,
|
|
32
|
-
suite_file:
|
|
35
|
+
suite_file: item.suite_file,
|
|
33
36
|
runner_manifest: item.runner_manifest,
|
|
34
37
|
}),
|
|
35
38
|
materializeBehavioral: (projectRoot, item) => materializeBehavioral(projectRoot, item.suite_file, item.runner_manifest.runner).mainPath,
|
|
@@ -98,7 +101,7 @@ async function buildRunPayload(config, d, item) {
|
|
|
98
101
|
}
|
|
99
102
|
else {
|
|
100
103
|
d.materializeStructural(config.projectRoot, item);
|
|
101
|
-
result = await d.runStructural(config.projectRoot, runner, item.suite_file
|
|
104
|
+
result = await d.runStructural(config.projectRoot, runner, artifactPaths(item.suite_file));
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
catch (err) {
|
|
@@ -121,18 +124,24 @@ async function buildRunPayload(config, d, item) {
|
|
|
121
124
|
// Exported for `run-local`, which runs these same strategies against the files
|
|
122
125
|
// the host just wrote rather than against a published suite. One dispatch table,
|
|
123
126
|
// so the command the loop iterates on is the command that runs after publishing.
|
|
124
|
-
export function runStructuralByRunner(projectRoot, runner,
|
|
127
|
+
export function runStructuralByRunner(projectRoot, runner, suitePaths) {
|
|
125
128
|
switch (runner) {
|
|
126
129
|
case 'rspec':
|
|
127
|
-
return runRspecSuite(projectRoot,
|
|
130
|
+
return runRspecSuite(projectRoot, suitePaths);
|
|
128
131
|
case 'vitest':
|
|
129
|
-
return runVitestSuite(projectRoot,
|
|
132
|
+
return runVitestSuite(projectRoot, suitePaths);
|
|
130
133
|
case 'pytest':
|
|
131
|
-
return runPytestSuite(projectRoot,
|
|
134
|
+
return runPytestSuite(projectRoot, suitePaths);
|
|
132
135
|
default:
|
|
133
136
|
return Promise.reject(new Error(`Unsupported runner "${runner}" — rebuild the suite.`));
|
|
134
137
|
}
|
|
135
138
|
}
|
|
139
|
+
// Every file of the branch, in the order the envelope carries them. A structural
|
|
140
|
+
// branch is one file per assignment since spec 42, §6, and running only the main
|
|
141
|
+
// one would execute a fraction of what the map says is guarded.
|
|
142
|
+
function artifactPaths(file) {
|
|
143
|
+
return [file.path, ...(file.support_files ?? []).map((entry) => entry.path)];
|
|
144
|
+
}
|
|
136
145
|
function suiteError(suiteDigest, message) {
|
|
137
146
|
return {
|
|
138
147
|
suite_digest: suiteDigest,
|
package/dist/verbs/runLocal.js
CHANGED
|
@@ -102,10 +102,10 @@ async function runOneBranch(config, d, suiteKind, output) {
|
|
|
102
102
|
return null;
|
|
103
103
|
}
|
|
104
104
|
let runner;
|
|
105
|
-
let
|
|
105
|
+
let suitePaths;
|
|
106
106
|
try {
|
|
107
107
|
runner = branchRunner(output);
|
|
108
|
-
|
|
108
|
+
suitePaths = artifactPathsOf(output);
|
|
109
109
|
}
|
|
110
110
|
catch (err) {
|
|
111
111
|
d.stdout.write(`Cannot run this branch: ${err.message}\n`);
|
|
@@ -120,8 +120,8 @@ async function runOneBranch(config, d, suiteKind, output) {
|
|
|
120
120
|
try {
|
|
121
121
|
result =
|
|
122
122
|
suiteKind === 'behavioral'
|
|
123
|
-
? await d.runBehavioral(config.projectRoot, runner,
|
|
124
|
-
: await d.runStructural(config.projectRoot, runner,
|
|
123
|
+
? await d.runBehavioral(config.projectRoot, runner, suitePaths[0])
|
|
124
|
+
: await d.runStructural(config.projectRoot, runner, suitePaths);
|
|
125
125
|
}
|
|
126
126
|
catch (err) {
|
|
127
127
|
d.stdout.write(`The runner could not start: ${err.message}\n`);
|
|
@@ -161,14 +161,26 @@ function outputTail(result) {
|
|
|
161
161
|
const joined = bits.join('\n');
|
|
162
162
|
return joined.length > OUTPUT_TAIL_CHARS ? joined.slice(-OUTPUT_TAIL_CHARS) : joined;
|
|
163
163
|
}
|
|
164
|
-
// The suite blob's own project-relative
|
|
165
|
-
|
|
164
|
+
// The suite blob's own project-relative paths, exactly as the runners expect
|
|
165
|
+
// them: the main file first, then every other file of the branch. The main file
|
|
166
|
+
// stopped being the whole suite in spec 42, §6 — a branch is one file per
|
|
167
|
+
// assignment now — and running it alone would exercise a fraction of what the
|
|
168
|
+
// answer claims to guard.
|
|
169
|
+
//
|
|
170
|
+
// The behavioral runners are given the main `.feature` and find the rest
|
|
171
|
+
// themselves: all three are pointed at the directory (`bdd.ts`), which is why a
|
|
172
|
+
// multi-file behavioral branch already worked before this spec.
|
|
173
|
+
function artifactPathsOf(output) {
|
|
166
174
|
const file = output.suite_file;
|
|
167
175
|
const path = file?.path;
|
|
168
176
|
if (typeof path !== 'string' || !path) {
|
|
169
177
|
throw new Error('this branch names no suite file to run.');
|
|
170
178
|
}
|
|
171
|
-
|
|
179
|
+
const support = Array.isArray(file?.support_files) ? file.support_files : [];
|
|
180
|
+
const rest = support
|
|
181
|
+
.map((entry) => entry?.path)
|
|
182
|
+
.filter((candidate) => typeof candidate === 'string' && candidate.length > 0);
|
|
183
|
+
return [path, ...rest];
|
|
172
184
|
}
|
|
173
185
|
function branchRoot(config, suiteKind) {
|
|
174
186
|
const request = readSuiteBuildRequest(config.projectRoot);
|
|
@@ -2,10 +2,11 @@ import { clearRunState } from "../runner/failureDigest.js";
|
|
|
2
2
|
import { materializeHelper } from "../files/guardrails.js";
|
|
3
3
|
import { materializeBehavioralWorld } from "../files/behavioral.js";
|
|
4
4
|
import { recipeNameFor, writeSuiteBuildRequest, } from "../files/suiteBuild.js";
|
|
5
|
+
import { bddStepLoading } from "../runner/bdd.js";
|
|
5
6
|
import { bootCheck, SIGNAL_STRENGTH } from "../runner/bootcheck.js";
|
|
6
|
-
import { anyStackPrecheck, detectBddRunner, detectStructuralRunner } from "../runner/precheck.js";
|
|
7
|
+
import { anyStackPrecheck, detectBddRunner, detectStructuralRunner, runnerReadyPrecheck } from "../runner/precheck.js";
|
|
7
8
|
import { selectRunnerEnvelope, withInstalledRunnerVersion } from "../runner/manifest.js";
|
|
8
|
-
import { ensureRunner } from "../runner/provision.js";
|
|
9
|
+
import { ensureRunner, ensureStructuralRunner } from "../runner/provision.js";
|
|
9
10
|
import { probeBehavioralWorld } from "../runner/worldProbe.js";
|
|
10
11
|
import { Wire } from "../wire.js";
|
|
11
12
|
// The complete envelope for one branch, or null when this machine cannot
|
|
@@ -46,7 +47,8 @@ function runnerEnvelopeFor(packet, runner, projectRoot) {
|
|
|
46
47
|
: envelope;
|
|
47
48
|
}
|
|
48
49
|
// Confirm at least one supported stack is present, materialize the Ruby boot
|
|
49
|
-
// helper
|
|
50
|
+
// helper an RSpec suite would need — only in a Ruby project — then fetch both
|
|
51
|
+
// peer assignments
|
|
50
52
|
// (spec 32) and each branch's recipe, and write the host's task to
|
|
51
53
|
// `.unitbob/suite-build/request.json`. No model is called and no source is read
|
|
52
54
|
// here — that is the host's job, framed by the two generation recipes. An
|
|
@@ -60,8 +62,10 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
60
62
|
getRecipe: (name) => wire.getRecipe(name),
|
|
61
63
|
getSuitePacketsBatch: () => wire.getSuitePacketsBatch(),
|
|
62
64
|
precheck: anyStackPrecheck,
|
|
65
|
+
confirmRunner: (projectRoot, runner) => runnerReadyPrecheck(projectRoot, runner),
|
|
63
66
|
bootCheck: (projectRoot, runner) => bootCheck(projectRoot, runner),
|
|
64
67
|
ensureRunner: deps?.ensureRunner ?? ensureRunner,
|
|
68
|
+
ensureStructuralRunner: deps?.ensureStructuralRunner ?? ensureStructuralRunner,
|
|
65
69
|
worldProbe: deps?.worldProbe ?? probeBehavioralWorld,
|
|
66
70
|
runnerEnvelope: runnerEnvelopeFor,
|
|
67
71
|
stdout: process.stdout,
|
|
@@ -70,7 +74,36 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
70
74
|
const check = actual.precheck(config.projectRoot);
|
|
71
75
|
if (!check.ok)
|
|
72
76
|
throw new Error(check.message ?? 'Unsupported runtime.');
|
|
73
|
-
|
|
77
|
+
// Ruby only. This wrote `unitbob_helper.rb` and `rspec.opts` into every
|
|
78
|
+
// project it touched, so a Flask app and a NestJS app each came away with a
|
|
79
|
+
// Ruby file they never asked for and cannot run — the product leaving another
|
|
80
|
+
// stack's litter in someone's repository.
|
|
81
|
+
if (detectStructuralRunner(config.projectRoot) === 'rspec')
|
|
82
|
+
materializeHelper(config.projectRoot);
|
|
83
|
+
// The stack is known; now make it runnable. A vibecoder who has never
|
|
84
|
+
// installed a test runner is the ordinary customer, not an edge case, so the
|
|
85
|
+
// runner (and, where the language allows it, the application's own
|
|
86
|
+
// dependencies) is installed under `.unitbob/` rather than reported as a
|
|
87
|
+
// reason they cannot use the product. Nothing in their project is written to.
|
|
88
|
+
//
|
|
89
|
+
// A project that already has its runner is left completely alone — see
|
|
90
|
+
// `ensureStructuralRunner` — so this costs nothing on a set-up machine.
|
|
91
|
+
const setupNotices = [];
|
|
92
|
+
if (check.runner) {
|
|
93
|
+
const provisioned = await actual.ensureStructuralRunner(config.projectRoot, check.runner);
|
|
94
|
+
if (provisioned.status === 'fixable') {
|
|
95
|
+
const steps = provisioned.checklist?.length ? `\n - ${provisioned.checklist.join('\n - ')}` : '';
|
|
96
|
+
throw new Error(`The ${check.runner} runner could not be installed under .unitbob/, and nothing can run without it: ` +
|
|
97
|
+
`${provisioned.message ?? 'provisioning failed'}${steps}\nNothing was written and nothing was uploaded.`);
|
|
98
|
+
}
|
|
99
|
+
setupNotices.push(...(provisioned.checklist ?? []));
|
|
100
|
+
// Confirm rather than assume. Provisioning reporting success and the runner
|
|
101
|
+
// actually being startable are two different facts, and this is the cheap
|
|
102
|
+
// one to check before a whole generation is built on it.
|
|
103
|
+
const ready = actual.confirmRunner(config.projectRoot, check.runner);
|
|
104
|
+
if (!ready.ok)
|
|
105
|
+
throw new Error(ready.message ?? `The ${check.runner} runner is not available.`);
|
|
106
|
+
}
|
|
74
107
|
// Spec 32-6. Before anything is fetched or written, find out whether the suite
|
|
75
108
|
// would get off the ground at all. It runs here, after the boot helper exists
|
|
76
109
|
// and before the network, so a project whose suite cannot start costs one
|
|
@@ -124,6 +157,11 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
124
157
|
const manifest = actual.runnerEnvelope(packet, runner, config.projectRoot);
|
|
125
158
|
if (!manifest)
|
|
126
159
|
return { packet, runner, branch: null };
|
|
160
|
+
// Spec 43, §3.2. The rule for which step files this runner loads travels
|
|
161
|
+
// with the branch that will be written against it, so nobody has to read
|
|
162
|
+
// the connector's own source to find it out — which is exactly what two
|
|
163
|
+
// coordinators did.
|
|
164
|
+
const stepLoading = packet.suite_kind === 'behavioral' && runner ? bddStepLoading(runner) : null;
|
|
127
165
|
return {
|
|
128
166
|
packet,
|
|
129
167
|
runner,
|
|
@@ -134,6 +172,7 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
134
172
|
recipe: await actual.getRecipe(recipeNameFor(packet)),
|
|
135
173
|
assignment: packet.assignment,
|
|
136
174
|
runner_manifest: manifest,
|
|
175
|
+
...(stepLoading ? { step_loading: stepLoading } : {}),
|
|
137
176
|
},
|
|
138
177
|
};
|
|
139
178
|
}));
|
|
@@ -168,8 +207,27 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
168
207
|
`finish says so in its own entry rather than being left out of the array. Run each locally with \`unitbob run-local\` (the same ` +
|
|
169
208
|
`runner that runs after publishing, so you never have to guess the command), repair broken harness steps while application failures remain red, ` +
|
|
170
209
|
`then run ${nextCommand}.\n`);
|
|
210
|
+
// Printed as well as written, because a rule nobody reads is a rule nobody
|
|
211
|
+
// follows — and this one is silent when broken: a step file the runner does
|
|
212
|
+
// not collect produces no error, only a green run over no scenarios.
|
|
213
|
+
for (const { packet, runner, branch } of prepared) {
|
|
214
|
+
if (!branch || packet.suite_kind !== 'behavioral' || !runner)
|
|
215
|
+
continue;
|
|
216
|
+
actual.stdout.write(branch.step_loading
|
|
217
|
+
? stepLoadingNotice(runner, branch.step_loading)
|
|
218
|
+
// Said rather than left blank. This connector has no strategy for that
|
|
219
|
+
// runner, so it does not know which files it loads — and silence here
|
|
220
|
+
// reads as "any name will do", which is the failure this whole notice
|
|
221
|
+
// exists to prevent.
|
|
222
|
+
: `\nBehavioral steps run under "${runner}", and this connector does not know how that runner ` +
|
|
223
|
+
'finds its step files — it has no strategy of that name. Nothing here tells you what to call ' +
|
|
224
|
+
'them, and this connector will not be able to run the branch either.\n');
|
|
225
|
+
}
|
|
171
226
|
// A fixable runner blocker is not a failure: the structural suite still builds this run. Tell the
|
|
172
227
|
// vibecoder the one command that unblocks the behavioral peer, then re-run suite-prepare.
|
|
228
|
+
if (setupNotices.length > 0) {
|
|
229
|
+
actual.stdout.write('\nOne setup step is worth knowing about before you generate:\n - ' + setupNotices.join('\n - ') + '\n');
|
|
230
|
+
}
|
|
173
231
|
if (fixableNotices.length > 0) {
|
|
174
232
|
actual.stdout.write('\nBehavioral suite skipped this run — its runner or connector-owned World profile is not ready. ' +
|
|
175
233
|
'This is a fixable setup step, not a build failure, and it does not affect the structural suite:\n' +
|
|
@@ -186,6 +244,24 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
186
244
|
'\n');
|
|
187
245
|
}
|
|
188
246
|
}
|
|
247
|
+
// The runner's own rule for which step files it will load, in the words of the
|
|
248
|
+
// side that loads them (spec 43, §3.2). The same object is in `request.json`, on
|
|
249
|
+
// the behavioral branch; this is the copy the coordinator sees without opening a
|
|
250
|
+
// file.
|
|
251
|
+
//
|
|
252
|
+
// A null pattern is printed as a null pattern. A runner whose rule is not one
|
|
253
|
+
// pattern says what it does know and admits the rest — inventing a pattern here
|
|
254
|
+
// would recreate, in the connector this time, exactly the retelling this
|
|
255
|
+
// replaced.
|
|
256
|
+
function stepLoadingNotice(runner, loading) {
|
|
257
|
+
const rule = loading.step_files
|
|
258
|
+
? `it loads \`${loading.step_files}\` from \`.unitbob/behavioral/step_definitions/\` — put the capability id ` +
|
|
259
|
+
'where the `*` is, and a file named anything else is not loaded at all'
|
|
260
|
+
: 'its rule for which files it loads is not one pattern, and this connector will not state one for it';
|
|
261
|
+
return (`\nBehavioral steps run under "${runner}", and ${rule}. What else has to be true of a step file there:\n - ` +
|
|
262
|
+
loading.requirements.join('\n - ') +
|
|
263
|
+
'\nThis is also in `request.json`, on the behavioral branch, as `step_loading`.\n');
|
|
264
|
+
}
|
|
189
265
|
// What the boot check found, in the vibecoder's terms. Printed on every run,
|
|
190
266
|
// including the quiet ones: "we looked and it starts" and "we could not look"
|
|
191
267
|
// are both worth a line, and a check nobody hears about is a check nobody
|
|
@@ -224,9 +300,10 @@ function bootFinding(boot, runner) {
|
|
|
224
300
|
// the one the vibecoder can paste into a search.
|
|
225
301
|
const next = boot.cause === 'defect_in_code'
|
|
226
302
|
? 'Fix that, then run `unitbob suite-prepare` again.'
|
|
227
|
-
:
|
|
228
|
-
'
|
|
229
|
-
'`pip install -r requirements.txt`),
|
|
303
|
+
: 'Unitbob installs the runner, and your declared dependencies with it, into `.unitbob/runners/` — ' +
|
|
304
|
+
'it never writes to your project. Something outside that file is still missing here. Run the ' +
|
|
305
|
+
'install your project needs (`bundle install`, `npm install`, `pip install -r requirements.txt`), ' +
|
|
306
|
+
'then run `unitbob suite-prepare` again.';
|
|
230
307
|
return (`${headline}\n\n` +
|
|
231
308
|
` ${boot.message}\n\n` +
|
|
232
309
|
`${boot.detail}\n\n` +
|