Row 148 round 3, Researcher's candidate (agents/researcher/work/queue-148/, row148.patch 1b63164b, manifest 95ae4736, 27 paths, base18aee8fa1). Dewey approved round 3 (comment 28832) and Filbert approved it with no findings (28835). Mos gave the decision 123 go on public diff 8360122b (mos-0636): no vendored code, no innerHTML or other HTML sinks. The Docs page #/docs lists each project's spec/north-star.md and the Markdown files directly under docs/. Save writes the typed text as one local commit authored by business.human, through a new human-only bus verb project.file.save that carries the opened commit as base and refuses stale-base if the file moved. The preview is a textarea plus a renderer built with createElement and textContent only; raw HTML shows as source text and images never load. CSP is unchanged. Round 3 replaced the backreference HR regex, which overflowed the regexp stack, with an isRule scan. Round 4 added a textContent fallback when paintDoc throws, and the submit handler resets saving in a finally. Mos's rule (ceiling 1 s at 1 MiB, growth x3 per doubling over 1 to 4 MiB): 130/130 and 138/138 cells pass, worst 513 ms at 1 MiB, worst ratio x2.50, control x2.01; the 756-cell grid has 0 timeouts. Mutants 43/44, v21 equivalent. Follow-ups: Vikunja P19 tasks 318 (Save stays off after a non-render paint throw), 319 (dead NUL check in bus.js) and 320 (the 1 MiB + 64 KiB buffer before the human proof, Gitea #1650). Sage's landing gate ran ona4779d2a5plus the candidate: 25 steps, all rc 0 (15 packages, 10 script suites), 22:19:24Z to 22:24:09Z, network sage-r148g_default removed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
138 lines
8.8 KiB
JavaScript
138 lines
8.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } 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';
|
|
// Row 148 (#1642): the human's file list and save. The agent writer's own tests stay in project.test.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-human-file-'));
|
|
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 } = await api.initProject(context);
|
|
const commitAs = (path, bytes, author) => {
|
|
writeFileSync(join(repo, path), bytes);
|
|
git(repo, 'add', '--', path);
|
|
git(repo, '-c', `user.name=${author}`, '-c', '[email protected]', 'commit', '-m', 'Fixture ' + path);
|
|
return git(repo, 'rev-parse', 'HEAD');
|
|
};
|
|
const opened = async path => (await api.readProjectFile({ ...context, path })).commit;
|
|
const save = async (path, content, base) => api.writeHumanFile({ ...context, path, content, base: base ?? await opened(path) });
|
|
return { context, repo, root, commitAs, opened, save };
|
|
}
|
|
const state = repo => ({ head: git(repo, 'rev-parse', 'HEAD'), status: git(repo, 'status', '--porcelain') });
|
|
|
|
test('the file list names the spec and Markdown directly under docs, as committed at HEAD', async t => {
|
|
const { context, repo, commitAs } = await fixture(t);
|
|
commitAs('docs/b-notes.md', '# B\n', 'jason');
|
|
mkdirSync(join(repo, 'docs/sub')); commitAs('docs/sub/deep.md', '# Deep\n', 'jason');
|
|
commitAs('docs/plain.txt', 'not markdown\n', 'jason');
|
|
writeFileSync(join(repo, 'docs/uncommitted.md'), '# Not committed\n');
|
|
const listed = await api.listProjectFiles(context);
|
|
assert.deepEqual(listed.files, [{ path: 'docs/README.md' }, { path: 'docs/b-notes.md' }, { path: 'spec/north-star.md' }]);
|
|
assert.equal(listed.head, git(repo, 'rev-parse', 'HEAD'));
|
|
assert.deepEqual(Object.keys(listed).sort(), ['files', 'head']);
|
|
await assert.rejects(api.listProjectFiles({ ...context, project: 'other' }), { code: 'project-not-declared' });
|
|
await assert.rejects(api.listProjectFiles({ ...context, path: 'docs/b-notes.md' }), { code: 'project-read-path' });
|
|
});
|
|
|
|
test('a save writes the typed text as one commit by the business human and touches nothing else', async t => {
|
|
const { context, repo, opened, save } = await fixture(t);
|
|
const text = '# North star\n\nLine one\r\nLine two with <b>html</b> and é.\n\n\n';
|
|
const before = state(repo).head, base = await opened('spec/north-star.md');
|
|
const saved = await save('spec/north-star.md', text, base);
|
|
assert.equal(saved.path, 'spec/north-star.md');
|
|
assert.equal(git(repo, 'rev-parse', 'HEAD'), saved.commit);
|
|
assert.equal(git(repo, 'rev-list', '--count', `${before}..HEAD`), '1');
|
|
assert.equal(git(repo, 'show', '-s', '--format=%an|%ae|%s', saved.commit), 'jason|[email protected]|Edit spec/north-star.md');
|
|
assert.deepEqual(git(repo, 'show', '--format=', '--name-only', saved.commit).split('\n'), ['spec/north-star.md']);
|
|
assert.equal(readFileSync(join(repo, 'spec/north-star.md'), 'utf8'), text);
|
|
assert.equal(git(repo, 'status', '--porcelain'), '');
|
|
const read = await api.readProjectFile({ ...context, path: 'spec/north-star.md' });
|
|
assert.equal(read.content, text); assert.equal(read.commit, saved.commit);
|
|
assert.equal((await api.readSpec(context)).confirmed, true);
|
|
const doc = await save('docs/README.md', '# Docs, edited\n');
|
|
assert.equal(git(repo, 'show', '-s', '--format=%an|%s', doc.commit), 'jason|Edit docs/README.md');
|
|
assert.equal(git(repo, 'rev-list', '--count', `${saved.commit}..HEAD`), '1');
|
|
});
|
|
|
|
test('a stale base refuses and writes nothing, whoever made the newer commit', async t => {
|
|
const { repo, commitAs, opened, save } = await fixture(t);
|
|
const base = await opened('spec/north-star.md');
|
|
commitAs('spec/north-star.md', '# Agent edit\n', 'coder.builder');
|
|
const after = state(repo);
|
|
await assert.rejects(save('spec/north-star.md', '# Mine\n', base), { code: 'stale-base' });
|
|
assert.deepEqual(state(repo), after);
|
|
assert.equal(readFileSync(join(repo, 'spec/north-star.md'), 'utf8'), '# Agent edit\n');
|
|
// A commit that only touched another file is not newer for this file.
|
|
const docBase = await opened('docs/README.md');
|
|
commitAs('docs/other.md', '# Other\n', 'jason');
|
|
assert.equal((await save('docs/README.md', '# Still mine\n', docBase)).path, 'docs/README.md');
|
|
// The head commit is not the file's last commit: a base taken from HEAD refuses.
|
|
const head = git(repo, 'rev-parse', 'HEAD');
|
|
await assert.rejects(save('spec/north-star.md', '# Wrong base\n', head), { code: 'stale-base' });
|
|
// The current last commit is accepted.
|
|
assert.equal((await save('spec/north-star.md', '# Reloaded\n')).path, 'spec/north-star.md');
|
|
});
|
|
|
|
test('only the spec and existing docs files are writable', async t => {
|
|
const { context, repo, opened, commitAs } = await fixture(t);
|
|
const base = await opened('spec/north-star.md');
|
|
mkdirSync(join(repo, 'docs/sub')); commitAs('docs/sub/deep.md', '# Deep\n', 'jason');
|
|
const after = state(repo);
|
|
for (const path of ['project.json', 'requirements/.gitkeep', 'decisions/x.json', 'templates/document.md', 'spec/other.md',
|
|
'docs/plain.txt', 'docs/sub/deep.md', '../outside.md', 'docs/../project.json', '/etc/passwd', 'docs/.hidden.md', 'docs/a\\b.md', '', null, 7])
|
|
await assert.rejects(api.writeHumanFile({ ...context, path, content: '# X\n', base }), { code: 'project-record-path' }, String(path));
|
|
await assert.rejects(api.writeHumanFile({ ...context, path: 'docs/new.md', content: '# New\n', base }), { code: 'project-read-missing' });
|
|
assert.deepEqual(state(repo), after);
|
|
});
|
|
|
|
test('content and base are checked before anything is written', async t => {
|
|
const { context, repo, opened } = await fixture(t);
|
|
const base = await opened('spec/north-star.md'), after = state(repo);
|
|
const write = (content, b) => api.writeHumanFile({ ...context, path: 'spec/north-star.md', content, base: b });
|
|
for (const content of ['', 'a\0b', 'lone \ud800 surrogate', 'x'.repeat(1024 * 1024 + 1), 'é'.repeat(600 * 1024), null, 5, { a: 1 }])
|
|
await assert.rejects(write(content, base), { code: 'project-record-invalid' });
|
|
for (const b of [undefined, '', 'HEAD', base.slice(1), base.toUpperCase(), `${base}0`, 5])
|
|
await assert.rejects(write('# Fine\n', b), { code: 'project-record-invalid' });
|
|
assert.deepEqual(state(repo), after);
|
|
const limit = await write('x'.repeat(1024 * 1024), base);
|
|
assert.match(limit.commit, /^[0-9a-f]{40}$/);
|
|
});
|
|
|
|
test('an unchanged save, a dirty tree and a busy writer refuse without a commit', async t => {
|
|
const { repo, opened, save } = await fixture(t);
|
|
const text = readFileSync(join(repo, 'spec/north-star.md'), 'utf8'), base = await opened('spec/north-star.md');
|
|
const after = state(repo);
|
|
await assert.rejects(save('spec/north-star.md', text, base), { code: 'project-record-unchanged' });
|
|
writeFileSync(join(repo, 'docs/README.md'), '# Pending human edit\n');
|
|
await assert.rejects(save('spec/north-star.md', '# New\n', base), { code: 'project-dirty' });
|
|
git(repo, 'checkout', '--', 'docs/README.md');
|
|
writeFileSync(join(repo, '.git/mosaic-record.lock'), 'another writer');
|
|
await assert.rejects(save('spec/north-star.md', '# New\n', base), { code: 'project-busy' });
|
|
rmSync(join(repo, '.git/mosaic-record.lock'));
|
|
assert.deepEqual(state(repo), after);
|
|
});
|
|
|
|
test('a failed commit restores the exact prior bytes, including a byte-order mark, and leaves history alone', async t => {
|
|
const { context, repo, root, commitAs, opened } = await fixture(t);
|
|
const original = Buffer.concat([Buffer.from([0xef, 0xbb, 0xbf]), Buffer.from('# With BOM\r\n')]);
|
|
commitAs('docs/bom.md', original, 'jason');
|
|
const base = await opened('docs/bom.md'), after = state(repo);
|
|
const realGit = execFileSync('sh', ['-c', 'command -v git'], { encoding: 'utf8' }).trim();
|
|
const bin = join(root, 'bin'); mkdirSync(bin);
|
|
writeFileSync(join(bin, 'git'), `#!/bin/sh\nfor arg do [ "$arg" = commit ] && exit 71; done\nexec '${realGit}' "$@"\n`);
|
|
chmodSync(join(bin, 'git'), 0o755);
|
|
const saved = process.env.PATH;
|
|
try {
|
|
process.env.PATH = bin + ':' + saved;
|
|
await assert.rejects(api.writeHumanFile({ ...context, path: 'docs/bom.md', content: '# Lost\n', base }), { code: 'project-git' });
|
|
} finally { process.env.PATH = saved; }
|
|
assert.deepEqual(state(repo), after);
|
|
assert.ok(readFileSync(join(repo, 'docs/bom.md')).equals(original));
|
|
});
|