@crouton-kit/crouter 0.3.31 → 0.3.32

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.
@@ -200,7 +200,13 @@ export async function createHarness(opts = {}) {
200
200
  }
201
201
  function nodeDirs() {
202
202
  try {
203
- return readdirSync(join(home, 'nodes'));
203
+ // Node ids are minted by newNodeId() (runtime/nodes.ts) as `<base36-ts>-<8
204
+ // hex>`. Scope the scan to that shape so a stray non-node sibling under
205
+ // nodes/ is never miscounted as a new node dir — e.g. a `reports/` dir
206
+ // created for an empty node id (the rare CI flake on full-tier run
207
+ // 27459304455 that broke the spawn counters with
208
+ // `expected exactly 1 new node dir, got [<id>, reports]`).
209
+ return readdirSync(join(home, 'nodes')).filter((d) => /^[0-9a-z]+-[0-9a-f]{8}$/.test(d));
204
210
  }
205
211
  catch {
206
212
  return [];
@@ -73,6 +73,12 @@ export function injectedDocsPath(nodeId) {
73
73
  }
74
74
  /** Create the full directory skeleton for a node. Idempotent. */
75
75
  export function ensureNodeDirs(nodeId) {
76
+ // Refuse an empty nodeId: it would mint stray `nodes/context`, `nodes/job`,
77
+ // `nodes/reports` siblings (non-node entries the test harness's node-dir
78
+ // scan would miscount). A node skeleton must belong to a real node id.
79
+ if (!nodeId) {
80
+ throw new Error('ensureNodeDirs: empty nodeId — refusing to create stray node-skeleton dirs under nodes/');
81
+ }
76
82
  for (const d of [contextDir(nodeId), jobDir(nodeId), reportsDir(nodeId)]) {
77
83
  mkdirSync(d, { recursive: true });
78
84
  }
@@ -0,0 +1,45 @@
1
+ // Regression: a report/skeleton write for an EMPTY node id used to resolve
2
+ // reportsDir('') → `nodes/reports` and silently mint a stray non-node sibling
3
+ // under nodes/. That stray entry broke the full-tier test harness's node-dir
4
+ // counter (`expected exactly 1 new node dir, got [<id>, reports]` — the rare
5
+ // flake on CI run 27459304455). The boundary now refuses an empty node id loudly
6
+ // instead of writing garbage. This locks that in.
7
+ import { test } from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+ import { mkdtempSync, existsSync, rmSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { tmpdir } from 'node:os';
12
+ import { push } from '../feed.js';
13
+ import { ensureNodeDirs, nodesRoot } from '../../canvas/paths.js';
14
+ test('push() rejects an empty nodeId and creates no stray nodes/reports dir', async () => {
15
+ const home = mkdtempSync(join(tmpdir(), 'crtr-emptyguard-'));
16
+ const orig = process.env['CRTR_HOME'];
17
+ process.env['CRTR_HOME'] = home;
18
+ try {
19
+ await assert.rejects(() => push('', { kind: 'update', body: 'orphan report' }), /empty nodeId/);
20
+ assert.equal(existsSync(join(nodesRoot(), 'reports')), false, 'no stray nodes/reports dir was created');
21
+ }
22
+ finally {
23
+ if (orig === undefined)
24
+ delete process.env['CRTR_HOME'];
25
+ else
26
+ process.env['CRTR_HOME'] = orig;
27
+ rmSync(home, { recursive: true, force: true });
28
+ }
29
+ });
30
+ test('ensureNodeDirs() rejects an empty nodeId and creates no stray skeleton dirs', () => {
31
+ const home = mkdtempSync(join(tmpdir(), 'crtr-emptyguard-'));
32
+ const orig = process.env['CRTR_HOME'];
33
+ process.env['CRTR_HOME'] = home;
34
+ try {
35
+ assert.throws(() => ensureNodeDirs(''), /empty nodeId/);
36
+ assert.equal(existsSync(join(nodesRoot(), 'reports')), false, 'no stray nodes/reports dir was created');
37
+ }
38
+ finally {
39
+ if (orig === undefined)
40
+ delete process.env['CRTR_HOME'];
41
+ else
42
+ process.env['CRTR_HOME'] = orig;
43
+ rmSync(home, { recursive: true, force: true });
44
+ }
45
+ });
@@ -70,6 +70,14 @@ function tierFor(kind) {
70
70
  * (c) If kind === 'final', mark the node done.
71
71
  */
72
72
  export async function push(nodeId, opts) {
73
+ // A report MUST belong to a node. An empty nodeId would resolve reportsDir('')
74
+ // to `nodes/reports` and silently mint a stray non-node sibling under nodes/
75
+ // (the latent corruption behind the full-tier flake on CI run 27459304455).
76
+ // Refuse it loudly at the boundary so a bad caller surfaces with a stack
77
+ // instead of writing garbage.
78
+ if (!nodeId) {
79
+ throw new Error(`push: empty nodeId (kind=${opts.kind}) — a report must belong to a node`);
80
+ }
73
81
  const { kind, body } = opts;
74
82
  const from = opts.from ?? nodeId;
75
83
  const now = new Date();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crouton-kit/crouter",
3
- "version": "0.3.31",
3
+ "version": "0.3.32",
4
4
  "description": "crtr — fast access to skills, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",