unitbob 0.7.7 → 0.7.8

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.
@@ -94,6 +94,8 @@ const BDD_STRATEGIES = {
94
94
  'over nothing.',
95
95
  '`conftest.py` is picked up by pytest itself whatever else sits beside it, so shared fixtures ' +
96
96
  'belong there and there is no load-order trap to work around.',
97
+ 'The project root is on `sys.path`, as it is for the structural branch: `from app import …` ' +
98
+ 'works in `conftest.py` and in step files, with no `sys.path` insert of your own.',
97
99
  ],
98
100
  },
99
101
  },
@@ -175,13 +177,16 @@ async function runPytestBdd(projectRoot, mainPath) {
175
177
  writeFileSync(join(projectRoot, PYTEST_BDD_PLUGIN_FILE), PYTEST_BDD_PLUGIN);
176
178
  const command = await pickPython(projectRoot);
177
179
  const stepsDir = join(BEHAVIORAL_ROOT, STEP_DEFINITIONS);
178
- const isVenvPytest = command.endsWith('/pytest');
180
+ // `python -m pytest`, as the structural branch runs it: `-m` puts the working
181
+ // directory — the project root — on `sys.path`, so the shared conftest's
182
+ // `from app import …` resolves. The `pytest` script does not do that, and on
183
+ // microblog the conftest died on that import before the first Scenario, with
184
+ // no report to read (spec 51-1).
185
+ //
179
186
  // `--rootdir .`, not the absolute root: the working directory is the project
180
187
  // root in every place, and an absolute host path would name a directory that
181
188
  // does not exist wherever the run actually happens.
182
- const args = isVenvPytest
183
- ? ['-c', PYTEST_INI_FILE, '-p', 'no:cacheprovider', '-p', pluginModule(), stepsDir, '--rootdir', '.']
184
- : ['-m', 'pytest', '-c', PYTEST_INI_FILE, '-p', 'no:cacheprovider', '-p', pluginModule(), stepsDir, '--rootdir', '.'];
189
+ const args = ['-m', 'pytest', '-c', PYTEST_INI_FILE, '-p', 'no:cacheprovider', '-p', pluginModule(), stepsDir, '--rootdir', '.'];
185
190
  const survivor = clearReport(join(projectRoot, PYTEST_BDD_REPORT));
186
191
  const run = await runInProject(projectRoot, command, args, {
187
192
  timeoutMs: BDD_TIMEOUT_MS,
@@ -212,10 +217,14 @@ function finalize(run, projectRoot, reportRel, survivor) {
212
217
  report: readFreshReport(join(projectRoot, reportRel), survivor),
213
218
  };
214
219
  }
220
+ // The sidecar's `pytest` is the sign that suite-prepare installed pytest-bdd
221
+ // there; a venv always has a `python` beside it, and that is what runs. Judging
222
+ // by `python` alone would send an unprovisioned venv into "No module named
223
+ // pytest" instead of the message that names the fix.
215
224
  async function pickPython(projectRoot) {
216
- const sidecarVenvPytest = `${BEHAVIORAL_ROOT}/.venv/bin/pytest`;
217
- if (executable(commandFileOnHost(projectRoot, sidecarVenvPytest))) {
218
- return sidecarVenvPytest;
225
+ const sidecarVenvBin = `${BEHAVIORAL_ROOT}/.venv/bin`;
226
+ if (executable(commandFileOnHost(projectRoot, `${sidecarVenvBin}/pytest`))) {
227
+ return `${sidecarVenvBin}/python`;
219
228
  }
220
229
  throw missingRunner('pytest-bdd');
221
230
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unitbob",
3
- "version": "0.7.7",
3
+ "version": "0.7.8",
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.7.7 run-local <branch>` and inspect the machine
24
+ `npx -y --loglevel=error unitbob@0.7.8 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
@@ -47,7 +47,10 @@ Write strict JSON, and nothing else, to
47
47
  "known_defect_probe": { "status": "not_supplied" },
48
48
  "selection_review": {
49
49
  "plan_digest": "<exact plan_digest from the request>",
50
- "capability_reviews": [{ "capability_id": "billing", "verdict": "pass" }]
50
+ "capability_reviews": [
51
+ { "capability_id": "billing", "verdict": "pass" },
52
+ { "capability_id": "reporting", "verdict": "pass" }
53
+ ]
51
54
  }
52
55
  }
53
56
  ```
@@ -58,11 +61,19 @@ Nesting it one level deeper cost a run its publish; so did inventing values for
58
61
  top-level keys", which dropped it entirely. Copy the digest, do not compute it.
59
62
 
60
63
  Write `selection_review` only when the request carries a `plan_digest`, and give
61
- it one entry per assigned capability. Its verdicts are `pass` or
62
- `does_not_pass` there is no `pass_with_reservation` at capability level and
63
- `does_not_pass` owes a non-empty `reviewer_objection_text` naming the lost
64
- promise, the unjustified merge, or the dishonest deferral. Selection objections
65
- are recorded; they never block the publish and never downgrade a lamp.
64
+ it one entry for **every capability in the request's `behavioral_assignment`**
65
+ the map's whole list, not the `worker_plan`'s. The plan takes a few capabilities
66
+ and leaves the rest for a later build; a capability it left out still gets a
67
+ verdict, about the deferral: `pass` when the candidate's `capabilities` mark it
68
+ `unguarded` with an honest reason, `does_not_pass` when a promise was dropped or
69
+ merged away. In the example above, `billing` was planned and `reporting` was
70
+ deferred, and both are there. On soul, 2026-09-11, the reviewer wrote one entry
71
+ per plan item and the publish was refused for every capability it left out.
72
+ Its verdicts are `pass` or `does_not_pass` — there is no `pass_with_reservation`
73
+ at capability level — and `does_not_pass` owes a non-empty
74
+ `reviewer_objection_text` naming the lost promise, the unjustified merge, or the
75
+ dishonest deferral. Selection objections are recorded; they never block the
76
+ publish and never downgrade a lamp.
66
77
 
67
78
  Omit `candidate_run`, `known_defect_context` and any runner report: the connector
68
79
  owns those and adds them itself.
@@ -100,12 +111,21 @@ and no answer that consists of writing nothing.
100
111
  it never excuses a "loads successfully" assertion for a promised record, state
101
112
  change, message, or side effect.
102
113
 
103
- `public_surfaces` lists the addresses you verified the `When` implementation
104
- actually drives, and must equal that Scenario's `surface_coverage` in the
105
- candidate's metadata. If the two disagree, that is a finding say it in a
106
- reservation or an objection rather than adjusting your list to match.
114
+ `public_surfaces` is your check of the worker's claim, not a list of your
115
+ own. Read the steps behind the Scenario and confirm every address in its
116
+ `surface_coverage` is really driven by some step; if every one is, copy that
117
+ list into `public_surfaces` verbatim the two must be equal, and the server
118
+ refuses a Scenario where they are not. If the manifest names an address **no
119
+ step drives at all**, the verdict is `does_not_pass`, naming the address
120
+ (a2time, 2026-08-17: six Scenarios claimed addresses their steps never
121
+ touched). An address a step drives that the manifest does **not** name never
122
+ goes into the field: a `When` reaching another capability's address goes in
123
+ `reservation` (below); a `Then` re-reading state, a `Given` arriving, an `After`
124
+ leaving go nowhere. A claimed address that a `Then` drives rather than the
125
+ `When` is real — it stays in the list; say which step drove it in `reservation`
126
+ if it matters. On microblog, 2026-09-11, that finding was right and the list
127
+ was shortened to show it, and the publish was refused for the shortened list.
107
128
 
108
- The `When`, and only the `When` — the same rule the worker wrote its list by.
109
129
  What a `Given` does to arrive (sign in, create the table the Scenario needs) and
110
130
  what an `After` does to leave are not the behaviour under test, so an address
111
131
  they touch is not missing from `surface_coverage` and not a finding. On soul,
@@ -152,7 +172,10 @@ shared test database and proves nothing about the candidate that was bound.
152
172
 
153
173
  **Do not rewrite anybody's verdict, including on a second pass.** If you find
154
174
  yourself weighing whether an objection is worth the trouble, the answer is that
155
- it costs this run nothing at all.
175
+ it costs this run nothing at all. A second pass that arrives with a
176
+ `validate-build` refusal corrects the field it names — `public_surfaces` brought
177
+ back to the manifest, a missing `selection_review` entry added — and leaves
178
+ every verdict as it was.
156
179
 
157
180
  **Do not widen the review.** Whether a capability deserved more Scenarios is the
158
181
  `selection_review` question and is answered per capability; everything else about