fix(mosaic): reject unapproved brain skeleton state
This commit is contained in:
@@ -292,6 +292,63 @@ describe('P7 brain provisioning orchestration', (): void => {
|
||||
expect(requests.some((request) => request.args.includes('grant'))).toBe(false);
|
||||
});
|
||||
|
||||
it('blocks before skeleton publication when the existing checkout is dirty', async (): Promise<void> => {
|
||||
const provisioner = await loadProvisioner('MB-REQ-06 dirty checkout publication gate');
|
||||
const root = tempRoot();
|
||||
const input = baseInput(root);
|
||||
mkdirSync(join(input.root, '.git'), { recursive: true });
|
||||
const requests: CommandRequest[] = [];
|
||||
|
||||
const result = await provisioner.provisionBrain(input, {
|
||||
run: (request): CommandResult => {
|
||||
requests.push(request);
|
||||
if (request.program === 'mosaic') {
|
||||
return request.args[2] === 'outside-seat'
|
||||
? {
|
||||
status: 10,
|
||||
stdout: validateResult('refused', 'no-token-for-identity', 'outside-seat'),
|
||||
stderr: 'refused reason=no-token-for-identity',
|
||||
}
|
||||
: { status: 0, stdout: validateResult('ok', 'validation-verified'), stderr: '' };
|
||||
}
|
||||
const command = request.args.join(' ');
|
||||
if (command.includes('ls-remote')) {
|
||||
return {
|
||||
status: 128,
|
||||
stdout: '',
|
||||
stderr: 'credential helper refused reason=no-token-for-identity',
|
||||
};
|
||||
}
|
||||
if (command.includes('rev-parse --is-inside-work-tree')) {
|
||||
return { status: 0, stdout: 'true\n', stderr: '' };
|
||||
}
|
||||
if (command.includes('remote get-url origin')) {
|
||||
return {
|
||||
status: 0,
|
||||
stdout: 'https://git.example.invalid/durable-owner/mosaic-brain.git\n',
|
||||
stderr: '',
|
||||
};
|
||||
}
|
||||
if (command.includes('branch --show-current')) {
|
||||
return { status: 0, stdout: 'main\n', stderr: '' };
|
||||
}
|
||||
if (command.includes('status --porcelain')) {
|
||||
return { status: 0, stdout: '?? .gitignore\n', stderr: '' };
|
||||
}
|
||||
return { status: 99, stdout: '', stderr: 'unexpected publication command' };
|
||||
},
|
||||
fetch: ownerFetch(),
|
||||
absentControlName: (): string => 'generated-absent-control',
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
status: 'blocked',
|
||||
reasonCode: 'brain-postcondition-failed',
|
||||
});
|
||||
expect(requests.some((request) => request.args.includes('commit'))).toBe(false);
|
||||
expect(requests.some((request) => request.args.includes('push'))).toBe(false);
|
||||
});
|
||||
|
||||
it('clones, seeds, resolves owner, migrates, pushes on each write, and archives source only after reachability', async (): Promise<void> => {
|
||||
const provisioner = await loadProvisioner('MB-REQ-07 complete migration transaction');
|
||||
const root = tempRoot();
|
||||
|
||||
@@ -175,6 +175,8 @@ export async function provisionBrain(
|
||||
finding.code === 'brain-not-git-repository' ||
|
||||
finding.code === 'brain-remote-mismatch' ||
|
||||
finding.code === 'brain-branch-mismatch' ||
|
||||
finding.code === 'brain-uncommitted-state' ||
|
||||
finding.code === 'brain-git-state-indeterminate' ||
|
||||
finding.code.startsWith('brain-write-access-'),
|
||||
);
|
||||
if (blockingCloneFindings.length > 0) {
|
||||
|
||||
@@ -359,6 +359,16 @@ describe('R6 — brain layout refuses secret material', (): void => {
|
||||
);
|
||||
});
|
||||
|
||||
it('refuses existing noncanonical gitignore content instead of publishing it', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-06 unapproved gitignore content');
|
||||
const root = tempRoot();
|
||||
const ignore = join(root, '.gitignore');
|
||||
writeFileSync(ignore, '# token=fixture-value\n*.token\n');
|
||||
|
||||
expect(() => sut.createBrainSkeleton(root)).toThrow(/brain-layout-ignore-content-unsafe/);
|
||||
expect(readFileSync(ignore, 'utf8')).toBe('# token=fixture-value\n*.token\n');
|
||||
});
|
||||
|
||||
it('refuses a symlinked layout directory without writing a tracked placeholder outside the brain', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-06 no-follow brain layout');
|
||||
const root = tempRoot();
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
openSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
unlinkSync,
|
||||
writeFileSync,
|
||||
@@ -400,21 +399,22 @@ export function createBrainSkeleton(root: string): {
|
||||
throw new Error('brain-layout-ignore-unsafe');
|
||||
}
|
||||
}
|
||||
const existing = existsSync(ignorePath)
|
||||
? readFileSync(ignorePath, 'utf8')
|
||||
.split(/\r?\n/)
|
||||
.filter((line: string): boolean => line.length > 0)
|
||||
: [];
|
||||
const merged = [...existing];
|
||||
for (const rule of GITIGNORE_RULES) {
|
||||
if (!merged.includes(rule)) merged.push(rule);
|
||||
}
|
||||
const content = `${merged.join('\n')}\n`;
|
||||
if (!existsSync(ignorePath) || readFileSync(ignorePath, 'utf8') !== content) {
|
||||
const content = `${GITIGNORE_RULES.join('\n')}\n`;
|
||||
if (existsSync(ignorePath)) {
|
||||
const snapshot = stableSourceSnapshot(ignorePath);
|
||||
if (!snapshot.content.equals(Buffer.from(content, 'utf8'))) {
|
||||
throw new Error('brain-layout-ignore-content-unsafe');
|
||||
}
|
||||
} else {
|
||||
const temporary = `${ignorePath}.tmp-${process.pid}-${randomUUID()}`;
|
||||
writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o644, flag: 'wx' });
|
||||
syncFile(temporary);
|
||||
renameSync(temporary, ignorePath);
|
||||
try {
|
||||
writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o644, flag: 'wx' });
|
||||
syncFile(temporary);
|
||||
linkSync(temporary, ignorePath);
|
||||
syncFile(ignorePath);
|
||||
} finally {
|
||||
rmSync(temporary, { force: true });
|
||||
}
|
||||
created.push(ignorePath);
|
||||
publicationEntries.push({ path: ignorePath, content: new TextEncoder().encode(content) });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user