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'); });