Skip to content

Commit e3bb287

Browse files
committed
i
1 parent 327bb8a commit e3bb287

10 files changed

Lines changed: 266 additions & 96 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
<!-- END: docs-autogen-note -->
1515

1616
<!-- BEGIN: test-status -->
17-
1/22 12:56:40 - Latest Status
17+
1/22 14:04:42 - Latest Status
1818
- Tests 1339/1339 - 100%
1919
- Lint 0 errors / 0 warnings
2020
- Type-check 0 errors / 0 warnings

RULES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Every task must be tracked in `TODO.md` using the prescribed workflow to ensure
2828
Code should be clean, minimalist, dynamic, and coherent in a self-documenting way that only requires minimal comments, with full descriptions reserved for that files's relevant .md file in /docs.
2929

3030

31-
## RULE 3: Test - /test
31+
## RULE 3: Test - /test SEE ALSO: /docs/test.md
3232

3333
Core principle: Test real implementations, not mocks. Tests align with Polychron's core goal to maximize dynamism and evolution.
3434

docs/test.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,23 @@ npm run test
138138
npm run test:watch
139139
```
140140

141+
- Debug runs (optional, heavy, and opt-in):
142+
143+
The repository includes diagnostic scripts that are intentionally kept separate from the default test flow to avoid slowing down everyday runs and to reduce noisy output. To enable debug-only diagnostics that may perform heavy instrumentation (for example, `scripts/debug-unit-coverage.js`), set the `DEBUG_UNIT_COVERAGE` environment variable or use the cross-platform helper script:
144+
145+
```bash
146+
# Linux / macOS (env directly)
147+
DEBUG_UNIT_COVERAGE=1 npm run test
148+
149+
# Cross-platform helper (works on Windows too):
150+
npm run test:debug
151+
```
152+
153+
Notes:
154+
- The debug script `scripts/debug-unit-coverage.js` now guards itself and will no-op unless `DEBUG_UNIT_COVERAGE` is set. This prevents accidental long runs when invoked directly.
155+
- Prefer `npm run test:debug` for a portable way to execute the full test pipeline with diagnostics enabled.
156+
- Consider running debug diagnostics on demand or in nightly CI jobs rather than on every PR to keep CI fast and deterministic.
157+
141158
- Docs maintenance:
142159

143160
```bash

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"test:loop": "node scripts/tests-loop.js",
3737
"test:quick": "node scripts/run-with-log.js test.log npm run test:raw && npm run docs:status",
3838
"test:composer-contract": "vitest --run test/composer.integration.test.ts test/composer.registry.strict.test.ts --reporter=dot",
39+
"test:debug": "node scripts/test-debug.js",
3940
"lint": "node scripts/run-with-log.js lint.log npm run lint:raw",
4041
"lint:raw": "eslint src/**/*.{js,ts}",
4142
"type-check": "node scripts/run-with-log.js type-check.log npm run type-check:raw",

scripts/debug-unit-coverage.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ import { initializePlayEngine } from '../src/play.js';
22

33
console.log('debug-unit-coverage: starting');
44

5+
// Only run this heavy diagnostic when explicitly enabled via env var to avoid accidental slow test runs
6+
if (!process.env.DEBUG_UNIT_COVERAGE) {
7+
console.log('debug-unit-coverage: disabled. Set DEBUG_UNIT_COVERAGE=1 to enable (or use "npm run test:debug").');
8+
process.exit(0);
9+
}
10+
511
function walkTree(node, path = [], results = []) {
612
if (!node) return results;
713
const keys = Object.keys(node.children || {});

scripts/docs.js

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import readLogSafe from './utils/readLogSafe.js';
99
import formatDate from './utils/formatDate.js';
1010
import splitByCodeFences from './utils/splitByCodeFences.js';
1111
import normalizeCodeForComparison from './utils/normalizeCodeForComparison.js';
12+
import { getFailuresFromLog } from './utils/getFailuresFromLog.js';
1213

1314
const projectRoot = process.cwd();
1415
const srcDir = path.join(projectRoot, 'src');
@@ -214,6 +215,83 @@ function updateRootReadmeStatus() {
214215
}
215216

216217

218+
export { getFailuresFromLog };
219+
220+
/*
221+
* Scan root-level TODO*.md files and synchronize the "Test Failures" section with current log
222+
* - Marks entries as fixed (checked) when the failure no longer appears in the log
223+
* - Ensures ongoing failures remain unchecked
224+
* - Appends any new failures that are not already present
225+
*/
226+
function updateTodosStatus() {
227+
const failures = getFailuresFromLog();
228+
const todoFiles = fs.readdirSync(projectRoot).filter(f => /^TODO(?:[-_].+)?\.md$/i.test(f));
229+
if (todoFiles.length === 0) return;
230+
231+
for (const todo of todoFiles) {
232+
const p = path.join(projectRoot, todo);
233+
let content = fs.readFileSync(p, 'utf8');
234+
const reSection = /(##\s*Test Failures[\s\S]*?)(?=\n##\s|$)/i;
235+
const m = content.match(reSection);
236+
if (!m) continue;
237+
238+
const section = m[1];
239+
const lines = section.split(/\r?\n/);
240+
const updated = [...lines];
241+
let changed = false;
242+
243+
// Update existing items
244+
for (let i = 0; i < lines.length; i++) {
245+
const line = lines[i];
246+
if (!/^- \[[ x]?\]/.test(line)) continue;
247+
const body = line.replace(/^\s*- \[[ x]?\]\s*/, '');
248+
const parts = body.split('—').map(s => s.trim());
249+
const locPart = parts[0] || '';
250+
const descPart = parts[1] || '';
251+
252+
const matched = failures.find(f => (f.file && locPart.includes(f.file)) || (descPart && f.desc && descPart.includes(f.desc)));
253+
if (matched) {
254+
if (/^- \[x\]/i.test(line)) {
255+
updated[i] = line.replace(/^- \[x\]/i, '- [ ]'); changed = true;
256+
}
257+
if (matched.msg && !line.includes(matched.msg)) { updated[i] = updated[i] + ` — ${matched.msg}`; changed = true; }
258+
} else {
259+
if (!/^- \[x\]/i.test(line)) {
260+
const date = formatDate();
261+
updated[i] = line.replace(/^- \[\s\]/i, '- [x]') + ` (fixed ${date})`; changed = true;
262+
}
263+
}
264+
}
265+
266+
// Append any new failures
267+
const existingKeys = new Set(lines.filter(l => /^- \[/.test(l)).map(l => l.replace(/\s*\(fixed.*?\)\s*$/, '').trim()));
268+
const toAppend = [];
269+
for (const f of failures) {
270+
const candidate = `- [ ] ${f.loc || f.file}${f.desc}${f.msg ? ` — ${f.msg}` : ''}`;
271+
if (!Array.from(existingKeys).some(k => k.includes(f.desc) || k.includes(f.file))) {
272+
toAppend.push(candidate);
273+
}
274+
}
275+
276+
if (toAppend.length) {
277+
const insertPos = m.index + section.length;
278+
const before = content.slice(0, insertPos);
279+
const after = content.slice(insertPos);
280+
content = before + '\n' + toAppend.join('\n') + '\n' + after;
281+
changed = true;
282+
}
283+
284+
if (changed) {
285+
// replace the old section text with the new updated block
286+
const newSection = updated.join('\n');
287+
content = content.replace(m[1], newSection);
288+
fs.writeFileSync(p, content, 'utf8');
289+
console.log(`Updated: ${todo} (synchronized with test.log)`);
290+
}
291+
}
292+
}
293+
294+
217295
function enforceLinksInText(text, isReadme=false) {
218296
let out = text;
219297
const codePrefix = isReadme ? 'src' : '../src';
@@ -478,7 +556,14 @@ if (cmd === 'fix') fixAll(verbose);
478556
else if (cmd === 'watch') watchAll();
479557
else if (cmd === 'check') checkAll();
480558
else if (cmd === 'index') generateIndex();
481-
else if (cmd === 'status') updateRootReadmeStatus();
559+
else if (cmd === 'status') {
560+
updateRootReadmeStatus();
561+
try {
562+
updateTodosStatus();
563+
} catch (e) {
564+
console.error('Error updating TODO status:', e && e.message ? e.message : e);
565+
}
566+
}
482567
else {
483568
console.error('Usage: node scripts/docs.js [fix|watch|check|index|status] [--verbose]');
484569
process.exit(1);

scripts/new-todo.js

Lines changed: 22 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ function sanitizeName(raw) {
1919
}
2020

2121
import makeTemplate from './utils/TODO-template.js';
22+
import { getFailuresFromLog } from './utils/getFailuresFromLog.js';
2223

2324
const HEADER = makeTemplate(dateStr);
2425

@@ -65,7 +66,7 @@ function usage() {
6566
/**
6667
* CLI entrypoint: create TODO.md with header and initial status block.
6768
*/
68-
function main() {
69+
async function main() {
6970
const args = process.argv.slice(2);
7071

7172
// Determine filename: default TODO.md, or TODO-<name>.md when a simple name is provided.
@@ -101,101 +102,31 @@ function main() {
101102
try {
102103
const logPath = path.join(projectRoot, 'log', 'test.log');
103104
if (fs.existsSync(logPath)) {
104-
const logText = fs.readFileSync(logPath, 'utf8');
105-
const lines = logText.split(/\r?\n/);
106-
// Strip ANSI sequences (colors/formatting) for reliable regex matching
107-
const stripAnsi = s => s.replace(/\x1b\[[0-9;]*m/g, '');
108-
const cleanLines = lines.map(l => stripAnsi(l));
109-
110-
const failures = [];
111-
// First, try to detect explicit FAIL lines (some reporters)
112-
for (let i = 0; i < cleanLines.length; i++) {
113-
const line = cleanLines[i];
114-
const failMatch = line.match(/\bFAIL\b\s+(\S+)\s*>\s*(.+)$/i);
115-
if (failMatch) {
116-
const file = failMatch[1];
117-
const desc = failMatch[2].trim().replace(/\s+/g, ' ');
118-
// look ahead for error message and location markers
119-
let msg = '';
120-
let loc = '';
121-
for (let j = i + 1; j < Math.min(cleanLines.length, i + 30); j++) {
122-
const lraw = cleanLines[j];
123-
const l = lraw.trim();
124-
if (!loc) {
125-
const locMatch = lraw.match(/\s*(\S+:\d+:\d+)/);
126-
if (locMatch) loc = locMatch[1];
127-
}
128-
if (!msg) {
129-
const errMatch = l.match(/^([A-Za-z0-9_]+Error|AssertionError|Error):\s*(.+)$/);
130-
if (errMatch) { msg = errMatch[0]; break; }
131-
}
132-
}
133-
if (!msg) {
134-
for (let j = i + 1; j < Math.min(cleanLines.length, i + 10); j++) {
135-
const l = cleanLines[j].trim();
136-
if (l && !/^stdout|^\[|^·/.test(l)) { msg = l.replace(/\s+/g, ' '); break; }
137-
}
138-
}
139-
failures.push({ file, loc, desc, msg });
140-
}
141-
}
142-
143-
// Second, detect vitest-style stdout/stderr lines that include the test file and description,
144-
// followed shortly by an Error/Assertion message or stack trace.
145-
const testLineRe = /(?:stdout|stderr)\s*\|\s*(\S+)\s*>\s*(.+)$/i;
146-
for (let i = 0; i < cleanLines.length; i++) {
147-
const m = cleanLines[i].match(testLineRe);
148-
if (m) {
149-
const file = m[1];
150-
// description may include multiple '>' segments; use the last segment after '>' as the test name
151-
const segments = m[2].split('>');
152-
const desc = segments.map(s => s.trim()).filter(Boolean).slice(-1)[0] || segments[0].trim();
153-
154-
// look ahead for an Error/TypeError/Assertion message within the next 20 lines
155-
let msg = '';
156-
let loc = '';
157-
for (let j = i + 1; j < Math.min(cleanLines.length, i + 40); j++) {
158-
const lraw = cleanLines[j];
159-
const l = lraw.trim();
160-
if (!l) continue;
161-
if (!loc) {
162-
const locMatch = lraw.match(/(\S+\.ts:\d+:\d+)/);
163-
if (locMatch) loc = locMatch[1];
164-
}
165-
const errMatch = l.match(/^([A-Za-z0-9_]+Error|AssertionError|Error|TypeError):\s*(.+)$/);
166-
if (errMatch) { msg = errMatch[0]; break; }
167-
const inlineErr = l.match(/Error:\s*(.+)/);
168-
if (inlineErr) { msg = `Error: ${inlineErr[1]}`; break; }
169-
if (cleanLines[j].match(testLineRe)) break;
170-
}
171-
172-
if (msg) {
173-
failures.push({ file, loc, desc: desc.replace(/\s+/g, ' '), msg });
105+
try {
106+
const failures = getFailuresFromLog(projectRoot);
107+
108+
if (failures.length > 0) {
109+
const seen = new Set();
110+
const linesToAppend = ['\n\n## Test Failures (from log/test.log)\n'];
111+
for (const f of failures) {
112+
const key = `${f.file}|${f.loc}|${f.desc}|${f.msg}`;
113+
if (seen.has(key)) continue;
114+
seen.add(key);
115+
const locPart = f.loc ? `${f.loc}` : f.file;
116+
const msgPart = f.msg ? ` — ${f.msg}` : '';
117+
linesToAppend.push(`- [ ] ${locPart}${f.desc}${msgPart}`);
174118
}
119+
fs.appendFileSync(todoPath, linesToAppend.join('\n') + '\n', 'utf8');
120+
console.log(`Appended ${seen.size} test failure(s) from log/test.log to ${path.relative(projectRoot, todoPath)}`);
121+
} else {
122+
console.log('No test failures found in log/test.log to append.');
175123
}
176-
}
177-
178-
if (failures.length > 0) {
179-
// Deduplicate
180-
const seen = new Set();
181-
const linesToAppend = ['\n\n## Test Failures (from log/test.log)\n'];
182-
for (const f of failures) {
183-
const key = `${f.file}|${f.loc}|${f.desc}|${f.msg}`;
184-
if (seen.has(key)) continue;
185-
seen.add(key);
186-
const locPart = f.loc ? `${f.loc}` : f.file;
187-
const msgPart = f.msg ? ` — ${f.msg}` : '';
188-
linesToAppend.push(`- [ ] ${locPart}${f.desc}${msgPart}`);
189-
}
190-
fs.appendFileSync(todoPath, linesToAppend.join('\n') + '\n', 'utf8');
191-
console.log(`Appended ${seen.size} test failure(s) from log/test.log to ${path.relative(projectRoot, todoPath)}`);
192-
} else {
193-
console.log('No test failures found in log/test.log to append.');
124+
} catch (e) {
125+
console.error('Failed to detect test failures from log/test.log:', e && e.message ? e.message : e);
194126
}
195127
}
196128
} catch (e) {
197129
console.error('Warning: failed to parse or append test log:', e && e.message ? e.message : e);
198-
}
199-
}
130+
}}
200131

201132
main();

scripts/run-with-log.js

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import { spawn } from 'child_process';
77
import { createWriteStream } from 'fs';
88
import { mkdir } from 'fs/promises';
9+
import stripAnsi from './utils/stripAnsi.js';
10+
import path from 'path';
911

12+
const cwd = process.cwd();
1013
const [, , logFile, ...command] = process.argv;
1114

1215
if (!logFile || command.length === 0) {
@@ -21,17 +24,58 @@ await mkdir('log', { recursive: true });
2124
const logStream = createWriteStream(`log/${logFile}`);
2225
const proc = spawn(command[0], command.slice(1), { shell: true, stdio: 'pipe' });
2326

27+
/**
28+
* Normalize a single line for the persistent log:
29+
* - Strip ANSI escapes
30+
* - Replace absolute repo paths with relative ones
31+
* - Shorten node_modules references
32+
* - Prefix with a timestamp and stream label (STDOUT/STDERR)
33+
*/
34+
function normalizeForLog(line, label = 'STDOUT') {
35+
let s = String(line || '');
36+
s = stripAnsi(s);
37+
// Collapse file:// prefixes that appear in stack traces
38+
s = s.replace(/file:\/\/[\/\\]*/g, '');
39+
// Replace absolute repository-root paths with a short <repo>/ prefix
40+
if (cwd && typeof s === 'string') {
41+
const safeCwd = cwd.replace(/\\/g, '/');
42+
s = s.split(safeCwd).join('<repo>');
43+
}
44+
// Collapse repetitive node_modules paths to a concise token
45+
s = s.replace(/node_modules[\\\/](@?[^\\\/\s]+)[\\\/]?/g, 'node_modules/$1/...');
46+
// Omit timestamps for a cleaner persistent log
47+
return `${label}: ${s}`;
48+
}
49+
50+
function writeNormalized(streamLabel, chunk) {
51+
// Split into lines and write each normalized line
52+
const raw = String(chunk);
53+
const lines = raw.split(/\r?\n/);
54+
for (let i = 0; i < lines.length; i++) {
55+
const l = lines[i];
56+
if (l === '' && i === lines.length - 1) continue; // trailing newline
57+
const normalized = normalizeForLog(l, streamLabel) + '\n';
58+
logStream.write(normalized);
59+
}
60+
}
61+
2462
proc.stdout.on('data', (data) => {
63+
// Mirror colored output to console for developer convenience
2564
process.stdout.write(data);
26-
logStream.write(data);
65+
// Write a cleaned, timestamped copy to the persistent log
66+
writeNormalized('STDOUT', data);
2767
});
2868

2969
proc.stderr.on('data', (data) => {
70+
// Mirror colored output to console
3071
process.stderr.write(data);
31-
logStream.write(data);
72+
// Clean and write to the persistent log, labeled STDERR
73+
writeNormalized('STDERR', data);
3274
});
3375

3476
proc.on('close', (code) => {
77+
const summary = `[${new Date().toISOString()}] PROCESS EXIT: code=${code}\n`;
78+
logStream.write(summary);
3579
logStream.end();
3680
process.exit(code);
3781
});

scripts/test-debug.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/usr/bin/env node
2+
// Historically this was a .cjs shim to handle Windows `npm.cmd` spawn behavior; now ESM with explicit platform handling.
3+
import { spawnSync } from 'child_process';
4+
5+
console.log('test:debug - running tests with DEBUG_UNIT_COVERAGE=1');
6+
7+
const env = { ...process.env, DEBUG_UNIT_COVERAGE: '1' };
8+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
9+
10+
const res = spawnSync(npmCmd, ['run', 'test'], { stdio: 'inherit', env });
11+
if (res.error) {
12+
console.error('test:debug failed to start tests', res.error);
13+
process.exit(1);
14+
}
15+
process.exit(res.status || 0);

0 commit comments

Comments
 (0)