@danypops/papyrus 0.27.11 → 0.27.12

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/ops.ts +22 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.27.11",
3
+ "version": "0.27.12",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
package/src/ops.ts CHANGED
@@ -529,14 +529,17 @@ export function runGates(db: Db, artifactId: string, options: GateRunOptions = {
529
529
  }
530
530
  }
531
531
  case "command": {
532
- const { execSync } = require_("node:child_process");
533
- try {
534
- const output = execSync(gate.target, { encoding: "utf-8", timeout: GATE_COMMAND_TIMEOUT_MS, stdio: ["pipe", "pipe", "pipe"], ...(cwd ? { cwd } : {}) }).trim();
535
- const passed = gate.expect ? output.includes(gate.expect) : true;
536
- return { gate, passed, output: output.slice(0, GATE_OUTPUT_LIMIT) };
537
- } catch (e) {
538
- return { gate, passed: false, output: e instanceof Error ? e.message.slice(0, GATE_OUTPUT_LIMIT) : "command failed" };
539
- }
532
+ // spawnSync + manual stdout/stderr concatenation, not execSync: execSync's return value is
533
+ // stdout only. Many real commands (bun test's own per-test lines and its pass/fail summary
534
+ // among them) write their actual output to stderr, so an execSync-based match against
535
+ // gate.expect saw only the first line of a banner and never the result -- every such gate
536
+ // failed regardless of whether the command actually passed.
537
+ const { spawnSync } = require_("node:child_process");
538
+ const result = spawnSync(gate.target, { shell: true, encoding: "utf-8", timeout: GATE_COMMAND_TIMEOUT_MS, ...(cwd ? { cwd } : {}) });
539
+ if (result.error) return { gate, passed: false, output: result.error.message.slice(0, GATE_OUTPUT_LIMIT) };
540
+ const combined = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
541
+ const passed = result.status === 0 && (gate.expect ? combined.includes(gate.expect) : true);
542
+ return { gate, passed, output: combined.slice(0, GATE_OUTPUT_LIMIT) || (result.status === 0 ? "ok" : `command exited with code ${result.status}`) };
540
543
  }
541
544
  case "test": {
542
545
  const { execSync } = require_("node:child_process");
@@ -564,7 +567,7 @@ export function runGates(db: Db, artifactId: string, options: GateRunOptions = {
564
567
  * indefinitely after Papyrus considers the gate "timed out". Spawning detached (its own
565
568
  * process group) and killing the negated pid on our own timer reaches the whole tree.
566
569
  */
567
- function executeGateCommand(command: string, timeout: number, cwd?: string): Promise<{ passed: boolean; output: string }> {
570
+ function executeGateCommand(command: string, timeout: number, cwd?: string): Promise<{ passed: boolean; output: string; matchable: string }> {
568
571
  // `spawn(..., { shell: true, detached: true })` instead of the `exec()` convenience wrapper:
569
572
  // `detached` (needed to make the shell the leader of its own process group, so the negated pid
570
573
  // below reaches every descendant, not just the shell) is not part of Node's `exec()`/
@@ -587,17 +590,21 @@ function executeGateCommand(command: string, timeout: number, cwd?: string): Pro
587
590
  child.stdout?.on("data", append);
588
591
  child.stderr?.on("data", append);
589
592
 
590
- const finish = (result: { passed: boolean; output: string }): void => {
593
+ const finish = (result: { passed: boolean; output: string; matchable: string }): void => {
591
594
  if (settled) return;
592
595
  settled = true;
593
596
  clearTimeout(timer);
594
597
  resolve(result);
595
598
  };
596
599
 
597
- child.on("error", (error) => finish({ passed: false, output: error.message.slice(0, GATE_OUTPUT_LIMIT) }));
600
+ child.on("error", (error) => finish({ passed: false, output: error.message.slice(0, GATE_OUTPUT_LIMIT), matchable: error.message }));
598
601
  child.on("close", (code) => {
599
- const output = buffered.trim().slice(0, GATE_OUTPUT_LIMIT);
600
- finish({ passed: code === 0, output: output || (code === 0 ? "ok" : `command exited with code ${code}`) });
602
+ // `matchable` carries the full (GATE_MAX_BUFFER_BYTES-bounded) buffer so the caller's
603
+ // gate.expect substring check sees the whole run, not just the first GATE_OUTPUT_LIMIT
604
+ // characters -- a real bun test run's pass/fail summary is its last line, not its first.
605
+ const full = buffered.trim();
606
+ const output = full.slice(0, GATE_OUTPUT_LIMIT);
607
+ finish({ passed: code === 0, output: output || (code === 0 ? "ok" : `command exited with code ${code}`), matchable: full });
601
608
  });
602
609
 
603
610
  const timer = setTimeout(() => {
@@ -609,7 +616,7 @@ function executeGateCommand(command: string, timeout: number, cwd?: string): Pro
609
616
  child.kill("SIGKILL");
610
617
  }
611
618
  }
612
- finish({ passed: false, output: `gate command timed out after ${timeout}ms` });
619
+ finish({ passed: false, output: `gate command timed out after ${timeout}ms`, matchable: "" });
613
620
  }, timeout);
614
621
  });
615
622
  }
@@ -651,7 +658,7 @@ export async function runGatesAsync(db: Db, artifactId: string, options: GateRun
651
658
  const executed = await executeGateCommand(command, timeout, options.cwd);
652
659
  results.push({
653
660
  gate,
654
- passed: executed.passed && (gate.expect ? executed.output.includes(gate.expect) : true),
661
+ passed: executed.passed && (gate.expect ? executed.matchable.includes(gate.expect) : true),
655
662
  output: executed.output,
656
663
  });
657
664
  } else {