unitbob 0.5.0 → 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/runner/bdd.js
CHANGED
|
@@ -22,33 +22,96 @@ export const BDD_RUN_ARTIFACTS = [
|
|
|
22
22
|
PYTEST_BDD_PLUGIN_NAME,
|
|
23
23
|
PYTEST_INI_NAME,
|
|
24
24
|
];
|
|
25
|
+
// One name for the directory every strategy points its loader at, so the
|
|
26
|
+
// descriptors below and the commands below them cannot come to mean different
|
|
27
|
+
// directories.
|
|
28
|
+
const STEP_DEFINITIONS = 'step_definitions';
|
|
25
29
|
const CUCUMBER_REPORT = join(BEHAVIORAL_ROOT, CUCUMBER_REPORT_NAME);
|
|
26
30
|
const PYTEST_BDD_REPORT = join(BEHAVIORAL_ROOT, PYTEST_BDD_REPORT_NAME);
|
|
27
31
|
const PYTEST_BDD_PLUGIN_FILE = join(BEHAVIORAL_ROOT, PYTEST_BDD_PLUGIN_NAME);
|
|
28
32
|
const PYTEST_INI_FILE = join(BEHAVIORAL_ROOT, PYTEST_INI_NAME);
|
|
29
33
|
const PYTEST_INI = '[pytest]\naddopts =\n';
|
|
34
|
+
// Load order is a fact about both Cucumbers and about neither pytest — pytest
|
|
35
|
+
// picks `conftest.py` up itself, so there is no trap to work around there.
|
|
36
|
+
const CUCUMBER_LOAD_ORDER = 'Files load in filename order, and the shared file is not special to the runner — `account_access` ' +
|
|
37
|
+
'loads before `shared`. Open each capability file with an explicit require of the shared one rather ' +
|
|
38
|
+
'than trusting the alphabet.';
|
|
30
39
|
// The connector-owned BDD strategy table (spec 32): the `runner` enum names one
|
|
31
40
|
// of these; the connector never executes a host-provided command string. Each
|
|
32
41
|
// strategy runs the whole behavioral bundle and returns the raw machine-readable
|
|
33
42
|
// report verbatim — the connector does no marker join and no aggregation.
|
|
43
|
+
//
|
|
44
|
+
// A strategy is its command *and* its loading rule. They are one entry so that a
|
|
45
|
+
// fourth runner cannot be added with only half of itself stated.
|
|
46
|
+
const BDD_STRATEGIES = {
|
|
47
|
+
cucumber: {
|
|
48
|
+
run: (projectRoot) => runCucumberRuby(projectRoot),
|
|
49
|
+
loading: {
|
|
50
|
+
step_files: '*.rb',
|
|
51
|
+
requirements: [
|
|
52
|
+
'The connector points `--require` at `step_definitions/`, so every `.rb` file there is loaded. ' +
|
|
53
|
+
'That explicit `--require` also switches off Cucumber\'s automatic loading of `features/support/`: ' +
|
|
54
|
+
'a World or helper parked there is never evaluated, and every step then fails on a bare object.',
|
|
55
|
+
CUCUMBER_LOAD_ORDER,
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
'cucumber-js': {
|
|
60
|
+
run: (projectRoot) => runCucumberJs(projectRoot),
|
|
61
|
+
loading: {
|
|
62
|
+
step_files: '*.js',
|
|
63
|
+
requirements: [
|
|
64
|
+
'Keep `step_definitions/` to CommonJS JavaScript and nothing else. The connector passes the whole ' +
|
|
65
|
+
'directory to `--require`, and cucumber-js `require()`s every file it matches whatever the ' +
|
|
66
|
+
'extension — a stray `.ts`, `.json` or `.md` left there is executed as JavaScript and aborts the ' +
|
|
67
|
+
'entire run with a parse error, before a single scenario.',
|
|
68
|
+
'The connector registers no TypeScript loader, so a `.ts` file cannot compile itself. If you want ' +
|
|
69
|
+
'one, register the compiler from the file that sorts first — and remember the file registering it ' +
|
|
70
|
+
'is itself loaded as plain JavaScript.',
|
|
71
|
+
CUCUMBER_LOAD_ORDER,
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
'pytest-bdd': {
|
|
76
|
+
run: (projectRoot, mainPath) => runPytestBdd(projectRoot, mainPath),
|
|
77
|
+
loading: {
|
|
78
|
+
step_files: 'test_*.py',
|
|
79
|
+
requirements: [
|
|
80
|
+
'pytest collects `step_definitions/` under its own default, which is `test_*.py` and `*_test.py`. ' +
|
|
81
|
+
'The connector writes no `python_files` setting and will not: a file named outside those two — ' +
|
|
82
|
+
'`<capability>_steps.py`, say — is simply not collected. No error, no scenarios, a green run ' +
|
|
83
|
+
'over nothing.',
|
|
84
|
+
'`conftest.py` is picked up by pytest itself whatever else sits beside it, so shared fixtures ' +
|
|
85
|
+
'belong there and there is no load-order trap to work around.',
|
|
86
|
+
],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
34
90
|
export function runBddSuite(projectRoot, runner, mainPath) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
case 'cucumber-js':
|
|
39
|
-
return runCucumberJs(projectRoot);
|
|
40
|
-
case 'pytest-bdd':
|
|
41
|
-
return runPytestBdd(projectRoot, mainPath);
|
|
42
|
-
default:
|
|
43
|
-
return Promise.reject(new Error(`Unsupported BDD runner "${runner}" — rebuild the behavioral suite.`));
|
|
91
|
+
const strategy = strategyFor(runner);
|
|
92
|
+
if (!strategy) {
|
|
93
|
+
return Promise.reject(new Error(`Unsupported BDD runner "${runner}" — rebuild the behavioral suite.`));
|
|
44
94
|
}
|
|
95
|
+
return strategy.run(projectRoot, mainPath);
|
|
96
|
+
}
|
|
97
|
+
// How this runner loads step files, for whoever has to write one. Null for a
|
|
98
|
+
// runner this connector does not run, which is the same answer `runBddSuite`
|
|
99
|
+
// gives it.
|
|
100
|
+
export function bddStepLoading(runner) {
|
|
101
|
+
return strategyFor(runner)?.loading ?? null;
|
|
102
|
+
}
|
|
103
|
+
// `Object.hasOwn` rather than a bare index: the runner name arrives over the
|
|
104
|
+
// wire, and `constructor` would otherwise come back as a truthy strategy with no
|
|
105
|
+
// `run` on it.
|
|
106
|
+
function strategyFor(runner) {
|
|
107
|
+
return Object.hasOwn(BDD_STRATEGIES, runner) ? BDD_STRATEGIES[runner] : null;
|
|
45
108
|
}
|
|
46
109
|
// Ruby: `cucumber` with the built-in message formatter. The features and step
|
|
47
110
|
// definitions both live under the behavioral root; --require points at the step
|
|
48
111
|
// definitions so only the Unitbob bundle loads.
|
|
49
112
|
async function runCucumberRuby(projectRoot) {
|
|
50
113
|
const features = join(BEHAVIORAL_ROOT, 'features');
|
|
51
|
-
const steps = join(BEHAVIORAL_ROOT,
|
|
114
|
+
const steps = join(BEHAVIORAL_ROOT, STEP_DEFINITIONS);
|
|
52
115
|
const sidecarGemfile = join(projectRoot, BEHAVIORAL_ROOT, 'Gemfile');
|
|
53
116
|
if (!existsSync(sidecarGemfile)) {
|
|
54
117
|
throw missingRunner('Cucumber');
|
|
@@ -75,7 +138,7 @@ function missingRunner(name) {
|
|
|
75
138
|
// to a file.
|
|
76
139
|
async function runCucumberJs(projectRoot) {
|
|
77
140
|
const features = join(BEHAVIORAL_ROOT, 'features');
|
|
78
|
-
const steps = join(BEHAVIORAL_ROOT,
|
|
141
|
+
const steps = join(BEHAVIORAL_ROOT, STEP_DEFINITIONS, '**', '*');
|
|
79
142
|
const sidecarBin = join(projectRoot, BEHAVIORAL_ROOT, 'node_modules', '.bin', 'cucumber-js');
|
|
80
143
|
if (!executable(sidecarBin)) {
|
|
81
144
|
throw missingRunner('Cucumber JS');
|
|
@@ -103,7 +166,7 @@ async function runPytestBdd(projectRoot, mainPath) {
|
|
|
103
166
|
writeFileSync(join(projectRoot, PYTEST_INI_FILE), PYTEST_INI);
|
|
104
167
|
writeFileSync(join(projectRoot, PYTEST_BDD_PLUGIN_FILE), PYTEST_BDD_PLUGIN);
|
|
105
168
|
const command = await pickPython(projectRoot);
|
|
106
|
-
const stepsDir = join(BEHAVIORAL_ROOT,
|
|
169
|
+
const stepsDir = join(BEHAVIORAL_ROOT, STEP_DEFINITIONS);
|
|
107
170
|
const isVenvPytest = command.endsWith('/pytest');
|
|
108
171
|
const args = isVenvPytest
|
|
109
172
|
? ['-c', PYTEST_INI_FILE, '-p', 'no:cacheprovider', '-p', pluginModule(), stepsDir, '--rootdir', projectRoot]
|
|
@@ -2,27 +2,62 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
|
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
|
|
5
|
+
// The four named roles, in one place. Everything that counts them counts this
|
|
6
|
+
// list — see the message at the bottom, which used to carry the number as a
|
|
7
|
+
// literal and spent a release saying "Installed 3" beside four names.
|
|
8
|
+
export const AGENT_NAMES = ['suite-worker', 'suite-repair-worker', 'fact-finder', 'suite-reviewer'];
|
|
6
9
|
const bundledAgentsDir = fileURLToPath(new URL('../../plugin/codex/agents/', import.meta.url));
|
|
10
|
+
const LABEL = {
|
|
11
|
+
created: 'created: ',
|
|
12
|
+
updated: 'updated: ',
|
|
13
|
+
current: 'already current:',
|
|
14
|
+
};
|
|
15
|
+
// Install (or refresh) the bounded Codex role definitions in the user's agent
|
|
16
|
+
// directory.
|
|
17
|
+
//
|
|
18
|
+
// This used to refuse when an installed file differed from the bundled one, to
|
|
19
|
+
// protect a definition the user had edited by hand. The case it actually met was
|
|
20
|
+
// the ordinary one: an upgrade from an older release, where every file differs
|
|
21
|
+
// and every install therefore failed. Spec 43, §1.6.
|
|
22
|
+
//
|
|
23
|
+
// The refusal became actively harmful once the workflows started asking a role
|
|
24
|
+
// whether this session can see it. A stale role answers that question exactly
|
|
25
|
+
// like a current one, so an update nobody could apply reads as "fully equipped"
|
|
26
|
+
// — the check would pass and the run would proceed on last release's
|
|
27
|
+
// instructions. So the file is overwritten, and what changed is said out loud
|
|
28
|
+
// rather than left for the user to discover.
|
|
7
29
|
export function installCodexAgents(args, deps = { home: homedir(), stdout: process.stdout }) {
|
|
8
30
|
if (args.length > 0)
|
|
9
31
|
throw new Error('codex-install accepts no arguments.');
|
|
10
32
|
const targetDir = join(deps.home, '.codex', 'agents');
|
|
11
|
-
const files = AGENT_NAMES.map((name) => ({
|
|
12
|
-
source: join(bundledAgentsDir, `${name}.toml`),
|
|
13
|
-
target: join(targetDir, `${name}.toml`),
|
|
14
|
-
}));
|
|
15
|
-
for (const file of files) {
|
|
16
|
-
if (!existsSync(file.target))
|
|
17
|
-
continue;
|
|
18
|
-
if (readFileSync(file.target, 'utf8') === readFileSync(file.source, 'utf8'))
|
|
19
|
-
continue;
|
|
20
|
-
throw new Error(`Refusing to overwrite existing Codex agent definition: ${file.target}`);
|
|
21
|
-
}
|
|
22
33
|
mkdirSync(targetDir, { recursive: true });
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
34
|
+
const byOutcome = { created: [], updated: [], current: [] };
|
|
35
|
+
for (const name of AGENT_NAMES) {
|
|
36
|
+
const source = join(bundledAgentsDir, `${name}.toml`);
|
|
37
|
+
const target = join(targetDir, `${name}.toml`);
|
|
38
|
+
const outcome = outcomeFor(source, target);
|
|
39
|
+
if (outcome !== 'current')
|
|
40
|
+
copyFileSync(source, target);
|
|
41
|
+
byOutcome[outcome].push(`${name}.toml`);
|
|
26
42
|
}
|
|
27
|
-
|
|
43
|
+
const changes = ['created', 'updated', 'current']
|
|
44
|
+
.filter((outcome) => byOutcome[outcome].length > 0)
|
|
45
|
+
.map((outcome) => ` ${LABEL[outcome]} ${byOutcome[outcome].join(', ')}`);
|
|
46
|
+
// The reason is attached only when something actually changed, and it is
|
|
47
|
+
// worded to be true of both ways it can change. "A thread open before these
|
|
48
|
+
// files existed" is true of a first install and false of an upgrade, where the
|
|
49
|
+
// files did exist — and the upgrade is the case that matters most, because a
|
|
50
|
+
// thread holding last release's definition answers a readiness check exactly
|
|
51
|
+
// like a current one.
|
|
52
|
+
const changed = byOutcome.created.length + byOutcome.updated.length > 0;
|
|
53
|
+
const why = changed
|
|
54
|
+
? ' — a thread already open is running the definitions it read when it started, not these'
|
|
55
|
+
: '';
|
|
56
|
+
deps.stdout.write(`${AGENT_NAMES.length} Unitbob Codex agent definitions in ${targetDir}:\n${changes.join('\n')}\n` +
|
|
57
|
+
`Start a new Codex thread before running Unitbob${why}.\n`);
|
|
58
|
+
}
|
|
59
|
+
function outcomeFor(source, target) {
|
|
60
|
+
if (!existsSync(target))
|
|
61
|
+
return 'created';
|
|
62
|
+
return readFileSync(target, 'utf8') === readFileSync(source, 'utf8') ? 'current' : 'updated';
|
|
28
63
|
}
|
|
@@ -2,6 +2,7 @@ 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
7
|
import { anyStackPrecheck, detectBddRunner, detectStructuralRunner, runnerReadyPrecheck } from "../runner/precheck.js";
|
|
7
8
|
import { selectRunnerEnvelope, withInstalledRunnerVersion } from "../runner/manifest.js";
|
|
@@ -156,6 +157,11 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
156
157
|
const manifest = actual.runnerEnvelope(packet, runner, config.projectRoot);
|
|
157
158
|
if (!manifest)
|
|
158
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;
|
|
159
165
|
return {
|
|
160
166
|
packet,
|
|
161
167
|
runner,
|
|
@@ -166,6 +172,7 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
166
172
|
recipe: await actual.getRecipe(recipeNameFor(packet)),
|
|
167
173
|
assignment: packet.assignment,
|
|
168
174
|
runner_manifest: manifest,
|
|
175
|
+
...(stepLoading ? { step_loading: stepLoading } : {}),
|
|
169
176
|
},
|
|
170
177
|
};
|
|
171
178
|
}));
|
|
@@ -200,6 +207,22 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
200
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 ` +
|
|
201
208
|
`runner that runs after publishing, so you never have to guess the command), repair broken harness steps while application failures remain red, ` +
|
|
202
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
|
+
}
|
|
203
226
|
// A fixable runner blocker is not a failure: the structural suite still builds this run. Tell the
|
|
204
227
|
// vibecoder the one command that unblocks the behavioral peer, then re-run suite-prepare.
|
|
205
228
|
if (setupNotices.length > 0) {
|
|
@@ -221,6 +244,24 @@ export async function suitePrepare(config, args = [], deps) {
|
|
|
221
244
|
'\n');
|
|
222
245
|
}
|
|
223
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
|
+
}
|
|
224
265
|
// What the boot check found, in the vibecoder's terms. Printed on every run,
|
|
225
266
|
// including the quiet ones: "we looked and it starts" and "we could not look"
|
|
226
267
|
// are both worth a line, and a check nobody hears about is a check nobody
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unitbob",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Unitbob connector — thin local hands for the Unitbob Rails brain. Owns no domain logic: it runs tools, relays bytes over the wire, and prints what the server returns.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@ markers, or paths. Do not edit production code, host-owned shared files, the
|
|
|
21
21
|
connector-owned harness, or another slice.
|
|
22
22
|
|
|
23
23
|
After every owned edit, run
|
|
24
|
-
`npx -y --loglevel=error unitbob@0.5.
|
|
24
|
+
`npx -y --loglevel=error unitbob@0.5.1 run-local <branch>` and inspect the machine
|
|
25
25
|
report. Look only at examples or scenarios matching your owned paths or case
|
|
26
26
|
markers. Do not require a green exit code from the whole branch: foreign failures
|
|
27
27
|
and an already-confirmed product red do not widen your scope. Repeat the bounded
|