@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
|
-
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
package/dist/core/feed/feed.js
CHANGED
|
@@ -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();
|