172 lines
11 KiB
JavaScript
172 lines
11 KiB
JavaScript
// Row 288: real bare-repository plumbing and remote effects. All text and credentials are fixtures.
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { writeFileSync, existsSync, renameSync, mkdirSync, rmSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { git } from './helpers.mjs';
|
|
import { sync, push, withLibraryWriteGuard } from '../src/sync.mjs';
|
|
import { servedCommit, withLibraryLock, readSyncState, SYNC_STATE_FILE } from '../src/mirror.mjs';
|
|
import { publishLibraryWrite } from '../src/sync.mjs';
|
|
import { checkMapping } from '../src/mapping.mjs';
|
|
import { writeLibraryDoc, saveLibraryFile, revertLibraryCommit } from '../src/write.mjs';
|
|
import { world, PATH, EDITOR } from './write-fixture.mjs';
|
|
|
|
test('C2-02: write/delete each publish one parent, seat ident, fixed trailer, index and lastPush', async t => {
|
|
const f = await world(t), parent = f.remoteHead();
|
|
const saved = await writeLibraryDoc(f.args(), f.opts);
|
|
assert.deepEqual(saved, { project: 'books', path: PATH, commit: f.remoteHead() });
|
|
assert.equal(git(f.remote, 'show', `${saved.commit}:${PATH}`), 'after');
|
|
assert.equal(git(f.remote, 'show', '-s', '--format=%P', saved.commit), parent);
|
|
assert.equal(git(f.remote, 'show', '-s', '--format=%an <%ae>|%cn <%ce>', saved.commit),
|
|
'Bean Fixture <[email protected]>|Bean Fixture <[email protected]>');
|
|
assert.equal(git(f.remote, 'show', '-s', '--format=%B', saved.commit), `Mosaic: write ${PATH}\n\nMosaic-Agent: acme/bean`);
|
|
assert.equal(await servedCommit(f.mirror), saved.commit);
|
|
assert.equal((await f.state()).lastPush.commit, saved.commit);
|
|
assert.equal(existsSync(join(f.mirror, 'index')), false);
|
|
const gone = await writeLibraryDoc(f.args({ content: undefined }), { ...f.opts, op: 'delete' });
|
|
assert.equal(git(f.remote, 'show', '-s', '--format=%P', gone.commit), saved.commit);
|
|
assert.equal(git(f.remote, 'ls-tree', '-r', gone.commit, '--', PATH), '');
|
|
assert.equal(git(f.remote, 'show', '-s', '--format=%B', gone.commit), `Mosaic: delete ${PATH}\n\nMosaic-Agent: acme/bean`);
|
|
});
|
|
|
|
test('C2-02 M12: each path refusal leaves every ref and remote unchanged', async t => {
|
|
const f = await world(t);
|
|
const paths = ['Book One/Chapters/Chapter 01.md', 'Book One/Playground/What If.md', 'Inbox Note.md',
|
|
'Book One/Notes/X.canvas', 'Book One/Notes/X.base', 'Book One/Notes/X.png', 'Second World/Chapters/01.md',
|
|
'/Book One/Notes/X.md', 'Book One/Notes/../X.md', 'Book One/./Notes/X.md', 'Book One//Notes/X.md',
|
|
'Book One/Notes/X\\Y.md', 'Book One/Notes/X\nY.md', 'Book One/Notes/Link.md',
|
|
'Book One/Notes/agent.md', `Book One/Notes/${'é'.repeat(512)}.md`];
|
|
for (const path of paths) {
|
|
const before = f.refs(), head = f.remoteHead();
|
|
await assert.rejects(writeLibraryDoc(f.args({ path, base: null }), f.opts), { code: 'library-write-story-path' }, path);
|
|
assert.equal(f.refs(), before); assert.equal(f.remoteHead(), head);
|
|
}
|
|
});
|
|
|
|
test('C2-02 Q4: delete requires an existing 100644 blob; null base never deletes a missing path', async t => {
|
|
const f = await world(t);
|
|
for (const path of ['Book One/Notes/Missing.md', 'Book One/Notes/Executable.md', 'Book One/Notes/Link.md', 'Book One/Notes/Submodule.md']) {
|
|
const before = f.refs();
|
|
await assert.rejects(writeLibraryDoc(f.args({ path, base: null, content: undefined }), { ...f.opts, op: 'delete' }),
|
|
{ code: 'library-write-story-path' });
|
|
assert.equal(f.refs(), before);
|
|
}
|
|
});
|
|
|
|
test('C2-02: stale base and failed push keep remote/served/tracking refs unchanged', async t => {
|
|
const f = await world(t), refs = f.refs(), head = f.remoteHead();
|
|
await assert.rejects(writeLibraryDoc(f.args({ base: 'a'.repeat(40) }), f.opts), { code: 'stale-base' });
|
|
assert.equal(f.refs(), refs); assert.equal(f.remoteHead(), head);
|
|
f.reject();
|
|
await assert.rejects(writeLibraryDoc(f.args(), f.opts), { code: 'library-push-failed' });
|
|
assert.equal(f.refs(), refs); assert.equal(f.remoteHead(), head);
|
|
});
|
|
|
|
test('C2-02: a moved unrelated blob retries once on the new parent, changed target refuses', async t => {
|
|
const f = await world(t);
|
|
const args = f.args(); let raced;
|
|
f.race(() => { f.external('Inbox Note.md', 'another\n'); raced = f.remoteHead(); });
|
|
const saved = await writeLibraryDoc(args, f.opts);
|
|
assert.equal(git(f.remote, 'show', '-s', '--format=%P', saved.commit), raced);
|
|
assert.equal(git(f.remote, 'show', `${saved.commit}:Inbox Note.md`), 'another');
|
|
const g = await world(t), before = g.refs();
|
|
g.race(() => g.external(PATH, 'raced target\n'));
|
|
await assert.rejects(writeLibraryDoc(g.args(), g.opts), { code: 'stale-base' });
|
|
assert.equal(g.refs(), before); assert.equal(git(g.remote, 'show', `main:${PATH}`), 'raced target');
|
|
});
|
|
|
|
test('C2-02: create with null base; human revert restores parent and does not require seat credentials', async t => {
|
|
const f = await world(t);
|
|
const made = await writeLibraryDoc(f.args({ path: 'Book One/Notes/New.md', base: null, content: 'new\n' }), f.opts);
|
|
const options = { credential: { ...f.credential, kind: 'human' }, command: f.command };
|
|
const reverted = await revertLibraryCommit({ ...f.context, commit: made.commit }, options);
|
|
assert.deepEqual(reverted, { project: 'books', path: 'Book One/Notes/New.md', reverted: made.commit, commit: f.remoteHead() });
|
|
assert.equal(git(f.remote, 'ls-tree', '-r', reverted.commit, '--', reverted.path), '');
|
|
assert.equal(git(f.remote, 'show', '-s', '--format=%an <%ae>|%cn <%ce>', reverted.commit),
|
|
'Human Fixture <[email protected]>|Human Fixture <[email protected]>');
|
|
assert.equal(git(f.remote, 'show', '-s', '--format=%(trailers:key=Mosaic-Agent,valueonly)', reverted.commit), '');
|
|
});
|
|
|
|
test('C2-02: revert requires reachable business trailer and refuses a later path change', async t => {
|
|
const f = await world(t), options = { credential: { ...f.credential, kind: 'human' }, command: f.command };
|
|
await assert.rejects(revertLibraryCommit({ ...f.context, commit: f.commit }, options), { code: 'library-revert-not-agent' });
|
|
const saved = await writeLibraryDoc(f.args(), f.opts);
|
|
await saveLibraryFile({ ...f.context, path: PATH, base: git(f.mirror, 'rev-parse', `served:${PATH}`), content: 'human later\n' }, options);
|
|
await assert.rejects(revertLibraryCommit({ ...f.context, commit: saved.commit }, options), { code: 'stale-base' });
|
|
});
|
|
|
|
test('C2-02: human save reaches story, preserves UTF-8 bytes and uses editor without a trailer', async t => {
|
|
const f = await world(t), path = 'Book One/Chapters/Chapter 01.md';
|
|
const content = '# Her text\n' + 'é'.repeat(40000) + '\n\n';
|
|
const saved = await saveLibraryFile({ ...f.context, path, base: git(f.mirror, 'rev-parse', `served:${path}`), content },
|
|
{ credential: { ...f.credential, kind: 'human' }, command: f.command });
|
|
assert.equal(git(f.remote, 'show', '-s', '--format=%an <%ae>', saved.commit), 'Human Fixture <[email protected]>');
|
|
// git helper trims; compare the stored blob byte count separately.
|
|
assert.equal(Number(git(f.remote, 'cat-file', '-s', `${saved.commit}:${path}`)), Buffer.byteLength(content));
|
|
});
|
|
|
|
test('C2-02: invalid content/editor/human credential refuse before any git command', async t => {
|
|
const f = await world(t), before = f.refs();
|
|
for (const content of ['nul\0', '\ud800', 'x'.repeat(1024 * 1024 + 1)])
|
|
await assert.rejects(writeLibraryDoc(f.args({ content }), f.opts), { code: 'invalid-request' });
|
|
const args = { ...f.context, path: PATH, base: null, content: 'x' };
|
|
writeFileSync(join(f.mirror, 'unexpected'), 'any local git inspection must refuse\n');
|
|
delete f.entry.editor;
|
|
await assert.rejects(saveLibraryFile(args, { credential: { ...f.credential, kind: 'human' }, command: f.command }), { code: 'library-mapping-invalid' });
|
|
await assert.rejects(revertLibraryCommit({ ...f.context, commit: f.commit }, { credential: { ...f.credential, kind: 'human' }, command: f.command }), { code: 'library-mapping-invalid' });
|
|
f.entry.editor = EDITOR;
|
|
const n = f.commands.length;
|
|
await assert.rejects(saveLibraryFile(args, { credential: { kind: 'human', user: 'human', file: '/absent' }, command: f.command }), { code: 'library-human-missing' });
|
|
await assert.rejects(revertLibraryCommit({ ...f.context, commit: f.commit }, { credential: { kind: 'human', user: 'human', file: '/absent' }, command: f.command }), { code: 'library-human-missing' });
|
|
assert.equal(f.commands.length, n); assert.equal(f.refs(), before);
|
|
});
|
|
|
|
|
|
test('C2-02 guard: only the exact owning token permits a push race fetch; sync refuses busy', async t => {
|
|
const f = await world(t), commit = f.remoteHead();
|
|
f.external('Inbox Note.md', 'remote moved\n');
|
|
await withLibraryWriteGuard(f.mirror, async token => {
|
|
await assert.rejects(sync(f.context, { credential: f.credential, command: f.command }), { code: 'library-sync-busy' });
|
|
for (const writeToken of [null, {}, Object.freeze({})])
|
|
await assert.rejects(push(f.context, { commit, credential: f.credential, command: f.command, writeToken }), { code: 'library-sync-busy' });
|
|
const raced = await push(f.context, { commit, credential: f.credential, command: f.command, writeToken: token });
|
|
assert.equal(raced.raced, true); assert.equal(raced.tip, f.remoteHead());
|
|
});
|
|
});
|
|
|
|
|
|
test('C2-02 publication F1: failed state rename restores served to match prior state', async t => {
|
|
const f = await world(t), previous = await servedCommit(f.mirror);
|
|
const commit = git(f.mirror, 'commit-tree', git(f.mirror, 'rev-parse', `${previous}^{tree}`), '-p', previous, '-m', 'candidate');
|
|
const statePath = join(f.mirror, SYNC_STATE_FILE), backup = join(f.root, 'state-backup');
|
|
await withLibraryLock(f.mirror, async lock => {
|
|
// A filesystem failure after the locked state read, before state publication. The original
|
|
// state is saved outside the mirror and restored after the failed rename.
|
|
const read = async options => {
|
|
const state = await lock.readSyncState(options);
|
|
renameSync(statePath, backup); mkdirSync(statePath, { mode: 0o700 });
|
|
return state;
|
|
};
|
|
try {
|
|
await assert.rejects(publishLibraryWrite(f.mirror, { mapping: checkMapping(f.entry), commit, previous, readSyncState: read }),
|
|
{ code: 'outcome-unknown' });
|
|
assert.equal(git(f.mirror, 'rev-parse', 'served'), previous);
|
|
} finally { rmSync(statePath, { recursive: true, force: true }); renameSync(backup, statePath); }
|
|
});
|
|
assert.equal((await readSyncState(f.mirror, { branch: 'main' })).served, previous);
|
|
});
|
|
|
|
|
|
test('C2-02 E1: a dotted business id never matches another business trailer', async t => {
|
|
const f = await world(t, { business: 'acme.co' });
|
|
writeFileSync(join(f.repo, PATH), 'foreign seat change\n');
|
|
git(f.repo, 'add', '-A'); git(f.repo, 'commit', '-qm', 'Foreign\n\nMosaic-Agent: acme-co/bean');
|
|
const foreign = git(f.repo, 'rev-parse', 'HEAD');
|
|
git(f.repo, 'push', '-q', f.remote, 'HEAD:main');
|
|
await sync(f.context, { credential: f.credential, command: f.command });
|
|
const before = f.refs();
|
|
await assert.rejects(revertLibraryCommit({ ...f.context, commit: foreign },
|
|
{ credential: { ...f.credential, kind: 'human' }, command: f.command }), { code: 'library-revert-not-agent' });
|
|
assert.equal(f.refs(), before); assert.equal(f.remoteHead(), foreign);
|
|
});
|