test(#1051): preregister mosaic-brain acceptance contract
This commit is contained in:
@@ -0,0 +1,623 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
/**
|
||||
* Red-first contract checks for stack #1051 / MB-BRAIN-01.
|
||||
*
|
||||
* These checks are committed before the implementation. They bind to the
|
||||
* MC-CRED v1.3 terminal classes and stable reason codes, not to the currently
|
||||
* deployed resolver behavior. Live grant and read/write round-trip tests remain
|
||||
* gated on MC-CRED-01; these fixtures contain no credential values.
|
||||
*/
|
||||
|
||||
interface BrainTarget {
|
||||
readonly estate: string;
|
||||
readonly host: string;
|
||||
readonly owner: string;
|
||||
readonly repo: string;
|
||||
readonly cloneUrl: string;
|
||||
}
|
||||
|
||||
interface CredentialAssessment {
|
||||
readonly outcome: 'ok' | 'refused' | 'error' | 'indeterminate';
|
||||
readonly exitCode: 0 | 10 | 20 | 30;
|
||||
readonly reasonCode: string;
|
||||
readonly diagnostic: string;
|
||||
}
|
||||
|
||||
interface ResolverParityAssessment extends CredentialAssessment {
|
||||
readonly gitReasonCode: string;
|
||||
readonly apiReasonCode: string;
|
||||
}
|
||||
|
||||
interface MigrationOwner {
|
||||
readonly name: string;
|
||||
readonly kind: 'active-lane' | 'durable-team' | 'durable-human' | 'durable-queue';
|
||||
readonly validated: boolean;
|
||||
}
|
||||
|
||||
interface MigrationCandidate {
|
||||
readonly source: string;
|
||||
readonly destination: string;
|
||||
readonly archive: string;
|
||||
readonly kind: 'lane' | 'seat';
|
||||
}
|
||||
|
||||
interface MigrationPlan {
|
||||
readonly status: 'ready' | 'blocked';
|
||||
readonly candidates: readonly MigrationCandidate[];
|
||||
readonly reported: readonly { path: string; reason: string }[];
|
||||
readonly owner: MigrationOwner | null;
|
||||
}
|
||||
|
||||
interface MigrationPublishEvidence {
|
||||
readonly commit: string;
|
||||
readonly remoteHead: string;
|
||||
readonly reachable: boolean;
|
||||
}
|
||||
|
||||
interface MigrationResult {
|
||||
readonly status: 'migrated' | 'reported' | 'failed';
|
||||
readonly migrated: readonly MigrationCandidate[];
|
||||
readonly reported: readonly { path: string; reason: string }[];
|
||||
readonly publish: MigrationPublishEvidence | null;
|
||||
}
|
||||
|
||||
interface BrainDoctorObservation {
|
||||
readonly rootExists: boolean;
|
||||
readonly gitRepository: boolean;
|
||||
readonly remote: string | null;
|
||||
readonly branch: string | null;
|
||||
readonly dirty: boolean | null;
|
||||
readonly access: CredentialAssessment | null;
|
||||
}
|
||||
|
||||
interface BrainDoctorFinding {
|
||||
readonly code: string;
|
||||
readonly repairable: boolean;
|
||||
readonly reasonCode: string | null;
|
||||
}
|
||||
|
||||
interface BrainDoctorAction {
|
||||
readonly program: 'git' | 'mosaic';
|
||||
readonly args: readonly string[];
|
||||
readonly findingCode: string;
|
||||
}
|
||||
|
||||
interface BrainWritePolicy {
|
||||
readonly allowed: boolean;
|
||||
readonly mode: 'append-only' | 'single-writer' | 'seat-writer' | 'refused';
|
||||
readonly reason: string;
|
||||
}
|
||||
|
||||
interface BrainStoreModule {
|
||||
deriveBrainTarget(registrySource: string, targetGitUrl: string): BrainTarget;
|
||||
createBrainSkeleton(root: string): { readonly created: readonly string[] };
|
||||
assessCredentialResult(source: string): CredentialAssessment;
|
||||
assessResolverParity(gitSource: string, apiSource: string): ResolverParityAssessment;
|
||||
discoverBrainMigration(input: {
|
||||
readonly sourceRoot: string;
|
||||
readonly brainRoot: string;
|
||||
readonly seat: string;
|
||||
readonly lane: string;
|
||||
readonly laneActive: boolean;
|
||||
readonly owner?: MigrationOwner;
|
||||
}): MigrationPlan;
|
||||
migrateBrainState(
|
||||
plan: MigrationPlan,
|
||||
publish: (brainRoot: string, paths: readonly string[]) => MigrationPublishEvidence,
|
||||
brainRoot: string,
|
||||
): MigrationResult;
|
||||
evaluateBrainDoctor(
|
||||
observation: BrainDoctorObservation,
|
||||
expectedRemote: string,
|
||||
): readonly BrainDoctorFinding[];
|
||||
planBrainDoctorFix(input: {
|
||||
readonly findings: readonly BrainDoctorFinding[];
|
||||
readonly target: BrainTarget;
|
||||
readonly identity: string;
|
||||
readonly root: string;
|
||||
}): readonly BrainDoctorAction[];
|
||||
classifyBrainWrite(input: {
|
||||
readonly path: string;
|
||||
readonly actor: string;
|
||||
readonly seat: string;
|
||||
readonly boardWriter?: string;
|
||||
}): BrainWritePolicy;
|
||||
}
|
||||
|
||||
const MODULE_PATH = './brain-store.js';
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
async function loadSut(requirement: string): Promise<BrainStoreModule> {
|
||||
try {
|
||||
return (await import(MODULE_PATH)) as BrainStoreModule;
|
||||
} catch (error: unknown) {
|
||||
const detail = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`${requirement}: brain-store implementation is absent (${detail})`);
|
||||
}
|
||||
}
|
||||
|
||||
function tempRoot(): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'mosaic-brain-contract-'));
|
||||
tempRoots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
function registry(): string {
|
||||
return JSON.stringify({
|
||||
version: 1,
|
||||
estates: [
|
||||
{
|
||||
name: 'homelab',
|
||||
readOnlyControlIdentity: 'homelab-read-control',
|
||||
hosts: [
|
||||
{
|
||||
host: 'git.mosaicstack.dev',
|
||||
provider: 'gitea',
|
||||
apiBaseUrl: 'https://git.mosaicstack.dev',
|
||||
tokenPrefix: 'gitea-mosaicstack',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'usc',
|
||||
readOnlyControlIdentity: 'usc-read-control',
|
||||
hosts: [
|
||||
{
|
||||
host: 'git.uscllc.com',
|
||||
provider: 'gitea',
|
||||
apiBaseUrl: 'https://git.uscllc.com',
|
||||
tokenPrefix: 'gitea-usc',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function credentialResult(
|
||||
outcome: CredentialAssessment['outcome'],
|
||||
reasonCode: string,
|
||||
message = 'non-secret diagnostic',
|
||||
): string {
|
||||
const exits = { ok: 0, refused: 10, error: 20, indeterminate: 30 } as const;
|
||||
return JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
operation: 'validate',
|
||||
outcome,
|
||||
exitCode: exits[outcome],
|
||||
retryable: false,
|
||||
subject: {
|
||||
identity: 'external-seat',
|
||||
estate: 'homelab',
|
||||
host: 'git.mosaicstack.dev',
|
||||
repo: 'mosaicstack/mosaic-brain',
|
||||
},
|
||||
mutation: 'none',
|
||||
reason: { code: reasonCode, message },
|
||||
evidence: {
|
||||
providerIdentity: null,
|
||||
repositoryPermission: null,
|
||||
writeDifferential: null,
|
||||
},
|
||||
audit: { journalId: 'opaque', state: 'sealed' },
|
||||
});
|
||||
}
|
||||
|
||||
afterEach((): void => {
|
||||
for (const root of tempRoots.splice(0)) {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe('R2/Q1 — estate and brain discovery have one authority', (): void => {
|
||||
it('derives the estate from the configured target git host and the brain owner from that target URL', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-02 target-host estate derivation');
|
||||
|
||||
expect(
|
||||
sut.deriveBrainTarget(registry(), 'https://git.mosaicstack.dev/mosaicstack/stack.git'),
|
||||
).toEqual({
|
||||
estate: 'homelab',
|
||||
host: 'git.mosaicstack.dev',
|
||||
owner: 'mosaicstack',
|
||||
repo: 'mosaicstack/mosaic-brain',
|
||||
cloneUrl: 'https://git.mosaicstack.dev/mosaicstack/mosaic-brain.git',
|
||||
});
|
||||
});
|
||||
|
||||
it('fails closed for an unmapped target host instead of consulting machine or ambient estate values', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-02 unmapped host fail-closed');
|
||||
const previous = process.env['MOSAIC_ESTATE'];
|
||||
process.env['MOSAIC_ESTATE'] = 'homelab';
|
||||
try {
|
||||
expect(() =>
|
||||
sut.deriveBrainTarget(registry(), 'https://unmapped.example.invalid/acme/stack.git'),
|
||||
).toThrow(/estate-host-unmapped/);
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env['MOSAIC_ESTATE'];
|
||||
else process.env['MOSAIC_ESTATE'] = previous;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('R6 — brain layout refuses secret material', (): void => {
|
||||
it('creates the durable layout and exact required gitignore exclusions', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-06 exact secret exclusions');
|
||||
const root = tempRoot();
|
||||
|
||||
sut.createBrainSkeleton(root);
|
||||
|
||||
for (const directory of ['agents', 'lanes', 'board', 'specs', 'methods', 'archives']) {
|
||||
expect(existsSync(join(root, directory)), directory).toBe(true);
|
||||
}
|
||||
const rules = readFileSync(join(root, '.gitignore'), 'utf8').trim().split('\n');
|
||||
expect(rules).toEqual(['*.token', '*.key', '*.pem', '.env', 'credentials.json']);
|
||||
});
|
||||
|
||||
it('never relays broker reason messages that may contain secret-bearing text', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-06 secret-free error path');
|
||||
const secretMarker = 'DO-NOT-EMIT-CREDENTIAL-MARKER';
|
||||
|
||||
const result = sut.assessCredentialResult(
|
||||
credentialResult('error', 'insecure-credential-source', secretMarker),
|
||||
);
|
||||
|
||||
expect(JSON.stringify(result)).not.toContain(secretMarker);
|
||||
expect(result.diagnostic).toContain('insecure-credential-source');
|
||||
});
|
||||
});
|
||||
|
||||
describe('credential caller contract v1.3 terminal classes', (): void => {
|
||||
it.each([
|
||||
['ok', 0, 'grant-verified'],
|
||||
['refused', 10, 'no-token-for-identity'],
|
||||
['error', 20, 'estate-registry-invalid'],
|
||||
['indeterminate', 30, 'provider-unavailable'],
|
||||
['indeterminate', 30, 'identity-not-found'],
|
||||
['indeterminate', 30, 'credential-rejected'],
|
||||
] as const)(
|
||||
'preserves %s/%i and stable reason %s without parsing prose',
|
||||
async (outcome, exitCode, reasonCode): Promise<void> => {
|
||||
const sut = await loadSut(`AC-MB-01 terminal class ${outcome}/${exitCode}`);
|
||||
|
||||
const result = sut.assessCredentialResult(credentialResult(outcome, reasonCode));
|
||||
|
||||
expect(result.outcome).toBe(outcome);
|
||||
expect(result.exitCode).toBe(exitCode);
|
||||
expect(result.reasonCode).toBe(reasonCode);
|
||||
},
|
||||
);
|
||||
|
||||
it('makes a missing or inconsistent decision field indeterminate rather than success or refusal', async (): Promise<void> => {
|
||||
const sut = await loadSut('AC-MB-01 malformed broker result fail-closed');
|
||||
const malformed = JSON.parse(credentialResult('ok', 'grant-verified')) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
malformed['exitCode'] = 10;
|
||||
|
||||
const result = sut.assessCredentialResult(JSON.stringify(malformed));
|
||||
|
||||
expect(result.outcome).toBe('indeterminate');
|
||||
expect(result.exitCode).toBe(30);
|
||||
expect(result.reasonCode).toBe('unexpected-provider-shape');
|
||||
});
|
||||
});
|
||||
|
||||
describe('R5 — out-of-estate refusal must agree on both resolver axes', (): void => {
|
||||
it('accepts refusal evidence only when Git and API return the same authoritative refusal', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-05 both-axis refusal');
|
||||
const refusal = credentialResult('refused', 'no-token-for-identity');
|
||||
|
||||
const result = sut.assessResolverParity(refusal, refusal);
|
||||
|
||||
expect(result.outcome).toBe('refused');
|
||||
expect(result.exitCode).toBe(10);
|
||||
expect(result.reasonCode).toBe('no-token-for-identity');
|
||||
expect(result.gitReasonCode).toBe('no-token-for-identity');
|
||||
expect(result.apiReasonCode).toBe('no-token-for-identity');
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
credentialResult('refused', 'no-token-for-identity'),
|
||||
credentialResult('ok', 'grant-verified'),
|
||||
],
|
||||
[
|
||||
credentialResult('refused', 'no-token-for-identity'),
|
||||
credentialResult('refused', 'cross-estate-resolution'),
|
||||
],
|
||||
[
|
||||
credentialResult('refused', 'no-token-for-identity'),
|
||||
credentialResult('indeterminate', 'provider-unavailable'),
|
||||
],
|
||||
])('turns axis disagreement into indeterminate failure', async (git, api): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-05 resolver-axis disagreement');
|
||||
|
||||
const result = sut.assessResolverParity(git, api);
|
||||
|
||||
expect(result.outcome).toBe('indeterminate');
|
||||
expect(result.exitCode).toBe(30);
|
||||
expect(result.reasonCode).toBe('permission-evidence-disagrees');
|
||||
});
|
||||
});
|
||||
|
||||
describe('R7 — migration is non-destructive, append-only, and explicit', (): void => {
|
||||
it('detects canonical lane and current-seat state while explicitly reporting unsupported local state', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-07 migration detection/reporting');
|
||||
const root = tempRoot();
|
||||
const sourceRoot = join(root, 'local-memory');
|
||||
const brainRoot = join(root, 'brain');
|
||||
mkdirSync(join(sourceRoot, 'lanes', 'lane-a'), { recursive: true });
|
||||
mkdirSync(join(sourceRoot, 'agents', 'seat-a'), { recursive: true });
|
||||
writeFileSync(join(sourceRoot, 'lanes', 'lane-a', 'finding.md'), 'lane finding\n');
|
||||
writeFileSync(join(sourceRoot, 'agents', 'seat-a', 'STATE.md'), 'seat state\n');
|
||||
writeFileSync(join(sourceRoot, 'orphan-state.md'), 'must be reported\n');
|
||||
|
||||
const plan = sut.discoverBrainMigration({
|
||||
sourceRoot,
|
||||
brainRoot,
|
||||
seat: 'seat-a',
|
||||
lane: 'lane-a',
|
||||
laneActive: true,
|
||||
owner: { name: 'lane-a', kind: 'active-lane', validated: true },
|
||||
});
|
||||
|
||||
expect(plan.status).toBe('ready');
|
||||
expect(plan.candidates.map((candidate) => candidate.kind).sort()).toEqual(['lane', 'seat']);
|
||||
expect(plan.reported).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ path: join(sourceRoot, 'orphan-state.md') }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves the migration gate blocking when no validated owner exists', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-09 named durable owner gate');
|
||||
const root = tempRoot();
|
||||
const sourceRoot = join(root, 'local-memory');
|
||||
mkdirSync(join(sourceRoot, 'lanes', 'closed-lane'), { recursive: true });
|
||||
writeFileSync(join(sourceRoot, 'lanes', 'closed-lane', 'finding.md'), 'finding\n');
|
||||
|
||||
const plan = sut.discoverBrainMigration({
|
||||
sourceRoot,
|
||||
brainRoot: join(root, 'brain'),
|
||||
seat: 'seat-a',
|
||||
lane: 'closed-lane',
|
||||
laneActive: false,
|
||||
});
|
||||
|
||||
expect(plan.status).toBe('blocked');
|
||||
expect(plan.candidates).toHaveLength(0);
|
||||
expect(plan.reported).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ reason: expect.stringMatching(/durable owner/i) }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('publishes collision-safe append-only copies before archiving sources and never overwrites a finding', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-07 append-only publish-before-archive migration');
|
||||
const root = tempRoot();
|
||||
const sourceRoot = join(root, 'local-memory');
|
||||
const brainRoot = join(root, 'brain');
|
||||
mkdirSync(join(sourceRoot, 'lanes', 'lane-a'), { recursive: true });
|
||||
writeFileSync(join(sourceRoot, 'lanes', 'lane-a', 'finding.md'), 'new finding\n');
|
||||
const plan = sut.discoverBrainMigration({
|
||||
sourceRoot,
|
||||
brainRoot,
|
||||
seat: 'seat-a',
|
||||
lane: 'lane-a',
|
||||
laneActive: true,
|
||||
owner: { name: 'lane-a', kind: 'active-lane', validated: true },
|
||||
});
|
||||
const candidate = plan.candidates[0];
|
||||
expect(candidate).toBeDefined();
|
||||
if (candidate === undefined) return;
|
||||
mkdirSync(join(brainRoot, 'lanes', 'lane-a', 'findings', 'imports'), { recursive: true });
|
||||
const preexisting = join(
|
||||
brainRoot,
|
||||
'lanes',
|
||||
'lane-a',
|
||||
'findings',
|
||||
'imports',
|
||||
'existing-finding.md',
|
||||
);
|
||||
writeFileSync(preexisting, 'older independent finding\n');
|
||||
let publishedPaths: readonly string[] = [];
|
||||
|
||||
const result = sut.migrateBrainState(
|
||||
plan,
|
||||
(_publishedRoot, paths): MigrationPublishEvidence => {
|
||||
publishedPaths = paths;
|
||||
return { commit: 'a'.repeat(40), remoteHead: 'a'.repeat(40), reachable: true };
|
||||
},
|
||||
brainRoot,
|
||||
);
|
||||
|
||||
expect(result.status).toBe('migrated');
|
||||
expect(readFileSync(preexisting, 'utf8')).toBe('older independent finding\n');
|
||||
expect(readFileSync(candidate.destination, 'utf8')).toBe('new finding\n');
|
||||
expect(readFileSync(candidate.archive, 'utf8')).toBe('new finding\n');
|
||||
expect(existsSync(candidate.source)).toBe(false);
|
||||
expect(publishedPaths).toContain(candidate.destination);
|
||||
expect(publishedPaths).toContain(candidate.archive);
|
||||
});
|
||||
|
||||
it('retains every source and reports failure when remote reachability is not established', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-07 failed-publish source preservation');
|
||||
const root = tempRoot();
|
||||
const sourceRoot = join(root, 'local-memory');
|
||||
const brainRoot = join(root, 'brain');
|
||||
mkdirSync(join(sourceRoot, 'agents', 'seat-a'), { recursive: true });
|
||||
const source = join(sourceRoot, 'agents', 'seat-a', 'STATE.md');
|
||||
writeFileSync(source, 'seat state\n');
|
||||
const plan = sut.discoverBrainMigration({
|
||||
sourceRoot,
|
||||
brainRoot,
|
||||
seat: 'seat-a',
|
||||
lane: 'lane-a',
|
||||
laneActive: true,
|
||||
owner: { name: 'lane-a', kind: 'active-lane', validated: true },
|
||||
});
|
||||
|
||||
const result = sut.migrateBrainState(
|
||||
plan,
|
||||
(): MigrationPublishEvidence => ({
|
||||
commit: 'a'.repeat(40),
|
||||
remoteHead: 'b'.repeat(40),
|
||||
reachable: false,
|
||||
}),
|
||||
brainRoot,
|
||||
);
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(readFileSync(source, 'utf8')).toBe('seat state\n');
|
||||
expect(result.reported).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ reason: expect.stringMatching(/reachab/i) }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('R8 — doctor diagnoses defects and fixes only through approved seams', (): void => {
|
||||
it('surfaces missing clone, wrong remote, absent write evidence, and uncommitted state as distinct defects', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-08 doctor defect classes');
|
||||
const expected = 'https://git.mosaicstack.dev/mosaicstack/mosaic-brain.git';
|
||||
|
||||
const missing = sut.evaluateBrainDoctor(
|
||||
{
|
||||
rootExists: false,
|
||||
gitRepository: false,
|
||||
remote: null,
|
||||
branch: null,
|
||||
dirty: null,
|
||||
access: null,
|
||||
},
|
||||
expected,
|
||||
);
|
||||
expect(missing.map((finding) => finding.code)).toContain('brain-clone-missing');
|
||||
|
||||
const defects = sut.evaluateBrainDoctor(
|
||||
{
|
||||
rootExists: true,
|
||||
gitRepository: true,
|
||||
remote: 'https://git.uscllc.com/usc/mosaic-brain.git',
|
||||
branch: 'main',
|
||||
dirty: true,
|
||||
access: sut.assessCredentialResult(
|
||||
credentialResult('indeterminate', 'credential-rejected'),
|
||||
),
|
||||
},
|
||||
expected,
|
||||
);
|
||||
expect(defects.map((finding) => finding.code)).toEqual(
|
||||
expect.arrayContaining([
|
||||
'brain-remote-mismatch',
|
||||
'brain-write-access-indeterminate',
|
||||
'brain-uncommitted-state',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('plans clone/remote/grant repair with git and mosaic cred only and never emits a token lookup', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-08 approved doctor fix seam');
|
||||
const target = sut.deriveBrainTarget(
|
||||
registry(),
|
||||
'https://git.mosaicstack.dev/mosaicstack/stack.git',
|
||||
);
|
||||
const findings: BrainDoctorFinding[] = [
|
||||
{ code: 'brain-clone-missing', repairable: true, reasonCode: null },
|
||||
{ code: 'brain-remote-mismatch', repairable: true, reasonCode: null },
|
||||
{ code: 'brain-write-access-refused', repairable: true, reasonCode: 'permission-denied' },
|
||||
{ code: 'brain-uncommitted-state', repairable: false, reasonCode: null },
|
||||
];
|
||||
|
||||
const actions = sut.planBrainDoctorFix({
|
||||
findings,
|
||||
target,
|
||||
identity: 'seat-a',
|
||||
root: '/home/test/.mosaic',
|
||||
});
|
||||
const rendered = JSON.stringify(actions);
|
||||
|
||||
expect(actions.map((action) => action.program)).toEqual(['git', 'git', 'mosaic']);
|
||||
expect(rendered).toContain('cred');
|
||||
expect(rendered).toContain('grant');
|
||||
expect(rendered).toContain('--estate');
|
||||
expect(rendered).toContain('homelab');
|
||||
expect(rendered).not.toMatch(/token|authorization|password/i);
|
||||
expect(actions.some((action) => action.findingCode === 'brain-uncommitted-state')).toBe(false);
|
||||
});
|
||||
|
||||
it('does not reinterpret error or indeterminate evidence as grantable refusal', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-08 non-refusal fail-closed fix planning');
|
||||
const target = sut.deriveBrainTarget(
|
||||
registry(),
|
||||
'https://git.mosaicstack.dev/mosaicstack/stack.git',
|
||||
);
|
||||
|
||||
for (const finding of [
|
||||
{
|
||||
code: 'brain-write-access-error',
|
||||
repairable: false,
|
||||
reasonCode: 'estate-registry-invalid',
|
||||
},
|
||||
{
|
||||
code: 'brain-write-access-indeterminate',
|
||||
repairable: false,
|
||||
reasonCode: 'provider-unavailable',
|
||||
},
|
||||
]) {
|
||||
expect(
|
||||
sut.planBrainDoctorFix({
|
||||
findings: [finding],
|
||||
target,
|
||||
identity: 'seat-a',
|
||||
root: '/home/test/.mosaic',
|
||||
}),
|
||||
).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Q2/Q3 doctrine — writes and retention are structurally constrained', (): void => {
|
||||
it('makes lane findings append-only, board writes single-writer, and seat state seat-owned', async (): Promise<void> => {
|
||||
const sut = await loadSut('MB-REQ-07 write policy');
|
||||
|
||||
expect(
|
||||
sut.classifyBrainWrite({
|
||||
path: 'lanes/lane-a/findings/new.md',
|
||||
actor: 'seat-a',
|
||||
seat: 'seat-a',
|
||||
}),
|
||||
).toMatchObject({ allowed: true, mode: 'append-only' });
|
||||
expect(
|
||||
sut.classifyBrainWrite({
|
||||
path: 'board/assignments.json',
|
||||
actor: 'tl-mosaic',
|
||||
seat: 'seat-a',
|
||||
boardWriter: 'tl-mosaic',
|
||||
}),
|
||||
).toMatchObject({ allowed: true, mode: 'single-writer' });
|
||||
expect(
|
||||
sut.classifyBrainWrite({
|
||||
path: 'board/assignments.json',
|
||||
actor: 'seat-a',
|
||||
seat: 'seat-a',
|
||||
boardWriter: 'tl-mosaic',
|
||||
}),
|
||||
).toMatchObject({ allowed: false, mode: 'refused' });
|
||||
expect(
|
||||
sut.classifyBrainWrite({
|
||||
path: 'agents/other-seat/STATE.md',
|
||||
actor: 'seat-a',
|
||||
seat: 'seat-a',
|
||||
}),
|
||||
).toMatchObject({ allowed: false, mode: 'refused' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user