#24 the HTML-obfuscation injection patterns had overlapping unbounded runs plus a required closing quote, so a non-closing input backtracked O(N^2) (~28.7s at the 512KB cap); quantifiers bounded, pathological input now 4ms. #53 mcp-live-inspect buffered MCP-server stdout via readline with no cap, so a hostile stdio server could exhaust memory / throw an uncaught RangeError; replaced with manual line buffering capped at 4MB that rejects pending RPCs and destroys stdout. #31 vsix-fetch's same-host redirect follower had no depth cap (loop hang); added depth>=5 cap mirroring the sibling fetcher. #25/#51 mcp-description-cache and skill-registry wrote JSON via bare writeFileSync (non-atomic: concurrent load-modify-save loses updates, a torn read silently yields an empty registry); both now write a temp file then renameSync. Suite 1931/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
39 lines
1.9 KiB
JavaScript
39 lines
1.9 KiB
JavaScript
// skill-registry-atomic.test.mjs — #51 (v7.8.3): saveRegistry must write
|
|
// atomically (temp file + rename). A torn write would parse-fail in
|
|
// loadRegistry and silently reset to an empty registry (permanent data loss).
|
|
// Zero external dependencies: node:test + node:assert only.
|
|
|
|
import { describe, it } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, chmodSync, readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { tmpdir } from 'node:os';
|
|
import { loadRegistry, saveRegistry, registryPath } from '../../scanners/lib/skill-registry.mjs';
|
|
|
|
describe('skill-registry — atomic saveRegistry (v7.8.3 #51)', () => {
|
|
it('replaces the registry file via temp+rename (write survives a read-only target)', () => {
|
|
const root = mkdtempSync(join(tmpdir(), 'llmsec-registry-atomic-'));
|
|
try {
|
|
const registry = loadRegistry(root);
|
|
registry.entries['f'.repeat(64)] = { name: 'first', last_scanned: new Date().toISOString() };
|
|
const filePath = saveRegistry(registry, root);
|
|
assert.equal(filePath, registryPath(root));
|
|
|
|
// A direct writeFileSync to a read-only file throws EACCES. An atomic
|
|
// temp+rename replaces the file because only directory write permission
|
|
// is required.
|
|
chmodSync(filePath, 0o444);
|
|
registry.entries['a'.repeat(64)] = { name: 'second', last_scanned: new Date().toISOString() };
|
|
saveRegistry(registry, root);
|
|
|
|
const raw = JSON.parse(readFileSync(filePath, 'utf8'));
|
|
assert.equal(raw.entry_count, 2, 'expected atomic rename to replace the read-only file');
|
|
assert.ok(raw.entries['a'.repeat(64)], 'second entry persisted');
|
|
|
|
const leftovers = readdirSync(join(root, 'reports')).filter((f) => f !== 'skill-registry.json');
|
|
assert.deepEqual(leftovers, [], 'no temp files left behind');
|
|
} finally {
|
|
try { rmSync(root, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
}
|
|
});
|
|
});
|