Files
stack/packages/project/tests/init-failure.test.mjs
jason.woltjeandClaude Opus 5.5 56639c99b0 project: init cleans up after a failure and refuses a nested records repo (row 153, #1647)
Row 153 round 1, Rocko's candidate on 686fc1cb6. Filbert approved
(comment 28727). projectRepo lives in packages/project/src/paths.mjs, so
business.mjs is unchanged (sage-0469). Sage's landing gate ran on this
commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-15 19:32:58 -05:00

92 lines
4.8 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { chmodSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readdirSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
import { checkProject, initProject, projectRepo, writeRecord } from '../src/index.mjs';
function fixture(t) {
const root = mkdtempSync(join(tmpdir(), 'project-init-failure-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
return { root, dataRoot: join(root, 'data'), project: 'stack', business: {
id: 'acme', human: 'jason', projects: { stack: { root: join(root, 'code'), repo: join(root, 'records') } },
} };
}
async function failingCommit(c, operation, sideEffect = ':') {
const realGit = execFileSync('sh', ['-c', 'command -v git'], { encoding: 'utf8' }).trim();
const bin = join(c.root, 'bin');
mkdirSync(bin);
writeFileSync(join(bin, 'git'), `#!/bin/sh\nfor arg do if [ "$arg" = commit ]; then ${sideEffect}; exit 71; fi; done\nexec '${realGit}' "$@"\n`);
chmodSync(join(bin, 'git'), 0o755);
const saved = process.env.PATH;
try { process.env.PATH = bin + ':' + saved; await operation(); }
finally { process.env.PATH = saved; }
}
for (const existing of [false, true]) test(`failed init cleans its layout and permits retry (pre-existing=${existing})`, async t => {
const c = fixture(t), repo = projectRepo(c);
if (existing) mkdirSync(repo);
const inode = existing ? lstatSync(repo).ino : null;
await failingCommit(c, () => assert.rejects(initProject(c), { code: 'project-git' }));
if (existing) {
assert.equal(lstatSync(repo).ino, inode);
assert.deepEqual(readdirSync(repo), []);
} else assert.equal(existsSync(repo), false);
const result = await initProject(c);
assert.match(result.commit, /^[0-9a-f]{40}$/);
assert.equal((await checkProject(c)).ok, true);
});
test('project check distinguishes a complete but uncommitted layout', async t => {
const c = fixture(t), { repo } = await initProject(c);
execFileSync('git', ['-C', repo, 'update-ref', '-d', 'refs/heads/main']);
await assert.rejects(checkProject(c), { code: 'project-no-commit' });
});
test('record repo refuses every declared code root including a sibling project', async t => {
const c = fixture(t);
c.business.projects.other = { root: join(c.root, 'other-code') };
c.business.projects.stack.repo = join(c.business.projects.other.root, 'records');
assert.throws(() => projectRepo(c), { code: 'project-path' });
await assert.rejects(initProject(c), { code: 'project-path' });
assert.equal(existsSync(c.business.projects.other.root), false);
c.business.projects.stack.repo = c.business.projects.other.root;
assert.throws(() => projectRepo(c), { code: 'project-path' });
c.business.projects.stack.repo = c.root;
assert.throws(() => projectRepo(c), { code: 'project-path' });
});
test('agent record writer refuses a human definition without changing history or files', async t => {
const c = fixture(t), { repo, commit } = await initProject(c);
await assert.rejects(writeRecord({ ...c, path: 'docs/human.md', record: '# Human',
author: { definition: 'human', instance: c.business.human } }), { code: 'project-author' });
assert.equal(existsSync(join(repo, 'docs/human.md')), false);
assert.equal(execFileSync('git', ['-C', repo, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim(), commit);
assert.equal(execFileSync('git', ['-C', repo, 'status', '--porcelain'], { encoding: 'utf8' }), '');
});
test('failed init preserves an unrelated file created while its commit runs', async t => {
const c = fixture(t), repo = projectRepo(c), keep = join(repo, 'human-note.txt');
await failingCommit(c, () => assert.rejects(initProject(c), { code: 'project-git' }),
`printf keep > '${keep}'`);
assert.deepEqual(readdirSync(repo), ['human-note.txt']);
assert.equal(readFileSync(keep, 'utf8'), 'keep');
});
test('failed init preserves an unrelated file inside a newly created layout directory', async t => {
const c = fixture(t), repo = projectRepo(c), keep = join(repo, 'docs/human-note.md');
await failingCommit(c, () => assert.rejects(initProject(c), { code: 'project-git' }),
`printf keep > '${keep}'`);
assert.deepEqual(readdirSync(repo), ['docs']);
assert.deepEqual(readdirSync(join(repo, 'docs')), ['human-note.md']);
assert.equal(readFileSync(keep, 'utf8'), 'keep');
});
test('failed init refuses to delete a replacement of its owned metadata file', async t => {
const c = fixture(t), repo = projectRepo(c), file = join(repo, 'project.json');
await failingCommit(c, () => assert.rejects(initProject(c), { code: 'project-init-cleanup' }),
`mv '${file}' '${join(c.root, 'original.json')}' && printf human > '${file}'`);
assert.equal(readFileSync(file, 'utf8'), 'human');
});