Files
stack/packages/project/tests/read.test.mjs
T
jason.woltjeandClaude Opus 5.5 3190ad762b bus, project: file-backed north-star specs with spec.read and record.write (row 138, #1632)
Row 138 round 2, Rocko's candidate e3f9a400 (43 paths) on 26a0b0fab.
Dewey approved (comment 28754) after round 1 changes (comment 28752):
D1-D5 mutants killed, the 17 touched test files run singly passed
124/124. Rocko's own run: 753 tests and 59/59 mutants killed.

The bus gains the project adapter through runtime.mjs, the way it
already injects the organization, so broker.mjs takes no new imports
and refuses with project-not-configured when no adapter is set. The
project socket timeout is 120s. Roles gain record.write. The schema
moves from 3e to 3f; the live migration follows MIGRATION.md and
needs Mos's window.

Sage's landing gate ran on c842686d5 plus the candidate: 25 steps,
all rc 0 (15 packages, 10 script suites), network sage-r138_default
removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 20:49:44 -05:00

125 lines
7.3 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, writeFileSync, readFileSync, symlinkSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
import * as api from '../src/index.mjs';
const git = (repo, ...args) => execFileSync('git', ['-c', 'core.hooksPath=/dev/null', '-C', repo, ...args], { encoding: 'utf8' }).trim();
async function fixture(t) {
const root = mkdtempSync(join(tmpdir(), 'project-spec-read-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const context = { business: { id: 'demo', human: 'jason', projects: {
stack: { root: join(root, 'code'), repo: join(root, 'records') },
} }, project: 'stack', dataRoot: root };
const { repo, commit } = await api.initProject(context);
const save = (path, bytes, author = 'jason') => {
writeFileSync(join(repo, path), bytes);
git(repo, 'add', '--', path);
git(repo, '-c', `user.name=${author}`, '-c', '[email protected]', 'commit', '-m', 'Save ' + path);
return git(repo, 'rev-parse', 'HEAD');
};
return { context, repo, commit, save, root };
}
test('spec reads exact committed content and its path commit/log, not dirty bytes or a later docs commit', async t => {
const { context, repo, save, commit } = await fixture(t);
const spec = save('spec/north-star.md', '# Spec\n\nKeep this.\n\n');
const docs = save('docs/notes.md', '# Docs\n');
writeFileSync(join(repo, 'spec/north-star.md'), '# Unsaved human edit\n');
const result = await api.readSpec(context);
assert.equal(result.content, '# Spec\n\nKeep this.\n\n');
assert.equal(result.repo, repo);
assert.equal(result.path, 'spec/north-star.md');
assert.equal(result.head, docs); assert.equal(result.revision, docs); assert.equal(result.commit, spec);
assert.equal(result.confirmed, true);
assert.deepEqual(result.author, { name: 'jason', email: '[email protected]' });
assert.deepEqual(result.log.map(r => r.commit), [spec, commit]);
assert.equal(result.truncated, false);
assert.equal(readFileSync(join(repo, 'spec/north-star.md'), 'utf8'), '# Unsaved human edit\n');
assert.equal(git(repo, 'rev-parse', 'HEAD'), docs);
for (const row of result.log) assert.equal(new Date(row.at).toISOString(), row.at);
const historical = await api.readSpec({ ...context, commit });
assert.equal(historical.commit, commit);
assert.deepEqual(historical.log.map(r => r.commit), [commit]);
assert.equal(historical.content, readFileSync(new URL('../templates/north-star.md', import.meta.url), 'utf8'));
});
test('latest agent spec edit is visible but never confirmed or replaced by an earlier human version', async t => {
const { context, save } = await fixture(t);
const commit = save('spec/north-star.md', '# Agent\n', 'coder.builder');
const result = await api.readSpec(context);
assert.equal(result.commit, commit); assert.equal(result.confirmed, false);
assert.equal(result.author.name, 'coder.builder');
});
test('shared file reader exposes committed docs and refuses path/revision injection or another history', async t => {
const { context, repo, save } = await fixture(t);
const commit = save('docs/design.md', '# Design\n');
assert.equal((await api.readProjectFile({ ...context, path: 'docs/design.md' })).commit, commit);
for (const path of ['../outside.md', 'spec/other.md', '/spec/north-star.md', 'docs/a/b.md', 'requirements/R.json'])
await assert.rejects(api.readProjectFile({ ...context, path }), { code: 'project-read-path' });
for (const commit of ['HEAD', '--help', 'a'.repeat(40), 'abc', git(repo, 'write-tree')])
await assert.rejects(api.readSpec({ ...context, commit }), { code: 'project-read-commit' });
const orphan = git(repo, '-c', 'user.name=jason', '-c', '[email protected]',
'commit-tree', git(repo, 'write-tree'), '-m', 'Unreachable');
await assert.rejects(api.readSpec({ ...context, commit: orphan }), { code: 'project-read-commit' });
});
test('reader rejects missing paths, links, invalid UTF-8, NUL and oversized committed blobs', async t => {
const { context, repo, save } = await fixture(t);
await assert.rejects(api.readProjectFile({ ...context, path: 'docs/missing.md' }), { code: 'project-read-missing' });
for (const bytes of [Buffer.from([0xff]), 'x\0y', 'x'.repeat(1024 * 1024 + 1)]) {
save('spec/north-star.md', bytes);
await assert.rejects(api.readSpec(context), { code: 'project-read-content' });
}
rmSync(join(repo, 'spec/north-star.md'));
symlinkSync('../docs/README.md', join(repo, 'spec/north-star.md'));
git(repo, 'add', '--', 'spec/north-star.md');
git(repo, '-c', 'user.name=jason', '-c', '[email protected]', 'commit', '-m', 'Linked spec');
await assert.rejects(api.readSpec(context), { code: 'project-read-content' });
});
test('reader refuses mismatched committed project identity, unsafe config and unknown argument keys', async t => {
const { context, repo, save } = await fixture(t);
await assert.rejects(api.readSpec({ ...context, path: 'docs/README.md' }), { code: 'project-read-path' });
await assert.rejects(api.readSpec({ ...context, project: 'other' }), { code: 'project-not-declared' });
const metadata = JSON.parse(readFileSync(join(repo, 'project.json'))); metadata.business = 'other';
save('project.json', JSON.stringify(metadata));
await assert.rejects(api.readSpec(context), { code: 'project-invalid' });
git(repo, 'remote', 'add', 'origin', 'https://fixture.invalid/no-network');
await assert.rejects(api.readSpec(context), { code: 'project-remote' });
});
test('path history is bounded at 100 and explicitly marks truncation', async t => {
const { context, save } = await fixture(t);
for (let i = 0; i < 101; i++) save('spec/north-star.md', `# Spec ${i}\n`);
const result = await api.readSpec(context);
assert.equal(result.log.length, 100);
assert.equal(result.truncated, true);
assert.equal(result.log[0].commit, result.commit);
});
test('committed UTF-8 BOM, Unicode and trailing newlines are returned without normalization', async t => {
const { context, save } = await fixture(t);
const value = '\uFEFF# North star 🧭\r\n\r\n';
save('spec/north-star.md', Buffer.from(value));
assert.equal((await api.readSpec(context)).content, value);
});
test('D4: spec log refuses a commit subject larger than its 256 KiB read bound', async t => {
const root = mkdtempSync(join(tmpdir(), 'spec-log-bound-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const business = { id: 'demo', human: 'jason', projects: { stack: { root: join(root, 'code'), repo: join(root, 'records') } } };
const { repo } = await api.initProject({ business, project: 'stack', dataRoot: root });
const message = join(root, 'commit-message.txt');
writeFileSync(message, 'X'.repeat(256 * 1024 + 1) + '\n');
writeFileSync(join(repo, 'spec/north-star.md'), '# Small spec\n');
execFileSync('git', ['-C', repo, 'add', '--', 'spec/north-star.md']);
execFileSync('git', ['-c', 'core.hooksPath=/dev/null', '-c', 'commit.gpgsign=false',
'-c', 'user.name=jason', '-c', '[email protected]', '-C', repo, 'commit', '-F', message],
{ stdio: ['ignore', 'ignore', 'pipe'] });
await assert.rejects(api.readSpec({ business, project: 'stack', dataRoot: root }), { code: 'project-git' });
});