unitbob 0.1.6 → 0.1.7

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.
@@ -7,6 +7,11 @@ export const HELPER_FILE = 'unitbob_helper.rb';
7
7
  // the project's own .rspec (stray --require lines, extra stdout formatters)
8
8
  // out of guardrail runs, whose JSON output must stay parseable.
9
9
  export const OPTIONS_FILE = 'rspec.opts';
10
+ // RSpec writes its JSON report here via `--out`, not to stdout: the app under
11
+ // test can print to stdout during a run (Rails logging, deprecation notices, a
12
+ // stray `puts`), which corrupts a stdout-parsed JSON document. A dedicated file
13
+ // isolates the report from anything else the process emits.
14
+ export const RESULT_FILE = 'rspec_result.json';
10
15
  // The boot file the generated suite requires (spec 29). Connector-owned and
11
16
  // versioned with it — never scaffolded into the project, never part of the
12
17
  // suite digest (that covers `spec_rb` bytes only). Delegates to the project's
@@ -1,7 +1,7 @@
1
- import { existsSync, statSync } from 'node:fs';
1
+ import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { runProcess } from "../proc.js";
4
- import { GUARDRAILS_DIR, OPTIONS_FILE, SUITE_FILE } from "../files/guardrails.js";
4
+ import { GUARDRAILS_DIR, OPTIONS_FILE, RESULT_FILE, SUITE_FILE } from "../files/guardrails.js";
5
5
  export const RSPEC_TIMEOUT_MS = 10 * 60 * 1000;
6
6
  // Spec 26: the Unitbob file runs in a defined order with a fixed seed so a
7
7
  // green→red flip can never come from run-order nondeterminism. It does not inherit
@@ -11,11 +11,14 @@ export const RSPEC_SEED = '1';
11
11
  // never the project's full suite — under RAILS_ENV=test with a fixed order/seed.
12
12
  // --options points at the materialized empty file so the project's own .rspec
13
13
  // (a --require of a helper we replaced, an extra stdout formatter) can neither
14
- // break the boot nor corrupt the JSON output.
14
+ // break the boot nor corrupt the JSON output. The JSON report goes to `--out`
15
+ // (a file), not stdout, so the app's own stdout writes during the run can never
16
+ // corrupt it.
15
17
  export async function runRspecSuite(projectRoot) {
16
18
  const suitePath = join(GUARDRAILS_DIR, SUITE_FILE);
17
19
  const optionsPath = join(GUARDRAILS_DIR, OPTIONS_FILE);
18
- return invokeRspec(projectRoot, [
20
+ const resultPath = join(GUARDRAILS_DIR, RESULT_FILE);
21
+ const { result, command, args } = await invokeRspec(projectRoot, [
19
22
  suitePath,
20
23
  '--options',
21
24
  optionsPath,
@@ -25,7 +28,21 @@ export async function runRspecSuite(projectRoot) {
25
28
  RSPEC_SEED,
26
29
  '--format',
27
30
  'json',
31
+ '--out',
32
+ resultPath,
28
33
  ]);
34
+ return { ...result, command, args, jsonReport: readReport(join(projectRoot, resultPath)) };
35
+ }
36
+ // Read the `--out` report file back verbatim. A missing or unreadable file is a
37
+ // clean empty string, not a throw: the caller falls back to stdout/stderr and
38
+ // reports a suite error, exactly as it would for unparseable output.
39
+ function readReport(path) {
40
+ try {
41
+ return existsSync(path) ? readFileSync(path, 'utf8') : '';
42
+ }
43
+ catch {
44
+ return '';
45
+ }
29
46
  }
30
47
  // Prefer the project's own `bin/rspec`; fall back to `bundle exec rspec`. Every
31
48
  // run sets RAILS_ENV=test so guardrails execute against the Rails test
@@ -40,7 +57,7 @@ async function invokeRspec(projectRoot, rspecArgs) {
40
57
  timeoutMs: RSPEC_TIMEOUT_MS,
41
58
  env: { ...process.env, RAILS_ENV: 'test', UNITBOB_REPO_ROOT: projectRoot },
42
59
  });
43
- return { ...result, command, args };
60
+ return { result, command, args };
44
61
  }
45
62
  function executable(path) {
46
63
  try {
package/dist/verbs/run.js CHANGED
@@ -32,6 +32,7 @@ export async function run(config, _args, deps) {
32
32
  code: null,
33
33
  command: 'rspec',
34
34
  args: [],
35
+ jsonReport: '',
35
36
  }));
36
37
  const payload = rawRunPayload(suite.suite_digest, result);
37
38
  const summary = await d.postRun(payload);
@@ -39,8 +40,11 @@ export async function run(config, _args, deps) {
39
40
  if (summary.map_url)
40
41
  d.stdout.write(`${summary.map_url}\n`);
41
42
  }
43
+ // The RSpec JSON report comes from the `--out` file (`jsonReport`), which the app
44
+ // under test cannot pollute. Stdout is only a fallback for a runner double that
45
+ // emits its report there; anything unparseable is a genuine suite error.
42
46
  function rawRunPayload(suiteDigest, result) {
43
- const parsed = parseJsonObject(result.stdout);
47
+ const parsed = parseJsonObject(result.jsonReport) ?? parseJsonObject(result.stdout);
44
48
  if (parsed)
45
49
  return { suite_digest: suiteDigest, rspec_json: boundFailures(parsed) };
46
50
  return {
@@ -1,5 +1,5 @@
1
1
  export function repoUrl(config) {
2
- return `${config.server}/repos/${config.repoId}`;
2
+ return `${config.server}/repos/${config.repoId}#map`;
3
3
  }
4
4
  export async function show(config) {
5
5
  process.stdout.write(`${repoUrl(config)}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unitbob",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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": {