1160 lines
40 KiB
TypeScript
1160 lines
40 KiB
TypeScript
import { afterEach, describe, expect, it } from 'vitest';
|
||
import {
|
||
existsSync,
|
||
lstatSync,
|
||
mkdtempSync,
|
||
mkdirSync,
|
||
readFileSync,
|
||
readdirSync,
|
||
renameSync,
|
||
rmSync,
|
||
symlinkSync,
|
||
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.5 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 MigrationOwnerResolution {
|
||
readonly verdict: 'resolved' | 'refused' | 'not-measured';
|
||
readonly reasonCode: string;
|
||
readonly principal: {
|
||
readonly name: string;
|
||
readonly kind:
|
||
| 'active-lane'
|
||
| 'durable-team'
|
||
| 'durable-human'
|
||
| 'durable-queue'
|
||
| 'mission-seat';
|
||
} | null;
|
||
readonly authority: {
|
||
readonly system: 'gitea' | 'glpi' | 'mosaic-mission-state';
|
||
readonly endpoint: string;
|
||
readonly contentType: 'application/json';
|
||
} | null;
|
||
}
|
||
|
||
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: MigrationOwnerResolution['principal'];
|
||
}
|
||
|
||
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 rootPrivate: boolean;
|
||
readonly gitRepository: boolean;
|
||
readonly remote: string | null;
|
||
readonly branch: string | null;
|
||
readonly worktreeState: 'clean' | 'dirty' | 'unmeasurable';
|
||
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,
|
||
brainNamespace: 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;
|
||
},
|
||
resolveOwner?: (lane: string) => MigrationOwnerResolution,
|
||
approveContent?: (path: string, content: Uint8Array) => boolean,
|
||
): MigrationPlan;
|
||
migrateBrainState(
|
||
plan: MigrationPlan,
|
||
publish: (
|
||
brainRoot: string,
|
||
entries: readonly { readonly path: string; readonly content: Uint8Array }[],
|
||
) => MigrationPublishEvidence,
|
||
brainRoot: string,
|
||
hooks?: {
|
||
readonly beforeDestinationWrite?: (destination: string) => void;
|
||
readonly beforeSourceCleanup?: (source: string) => void;
|
||
},
|
||
): 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',
|
||
providerLogin: string | null = outcome === 'ok'
|
||
? 'external-seat'
|
||
: reasonCode === 'provider-identity-mismatch'
|
||
? 'Mos'
|
||
: null,
|
||
): 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:
|
||
providerLogin === null
|
||
? null
|
||
: {
|
||
login: providerLogin,
|
||
endpoint: 'GET /api/v1/user',
|
||
contentType: 'application/json',
|
||
},
|
||
repositoryPermission:
|
||
outcome === 'ok'
|
||
? {
|
||
requested: 'write',
|
||
effective: 'write',
|
||
endpoint: 'GET /api/v1/repos/mosaicstack/mosaic-brain',
|
||
contentType: 'application/json',
|
||
}
|
||
: null,
|
||
writeDifferential:
|
||
outcome === 'ok'
|
||
? {
|
||
state: 'can-write',
|
||
credentialBinding: 'same-resolution',
|
||
transportPrincipal: 'external-seat',
|
||
authenticatedReceivePack: 'advertised',
|
||
readOnlyControl: {
|
||
identity: 'homelab-read-control',
|
||
providerPermission: 'read',
|
||
receivePack: 'refused',
|
||
},
|
||
unauthenticatedReceivePack: 'refused',
|
||
artifactCreated: false,
|
||
proves: 'non-secret evidence',
|
||
doesNotProve: 'branch update acceptance',
|
||
}
|
||
: 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',
|
||
'durable-owner',
|
||
),
|
||
).toEqual({
|
||
estate: 'homelab',
|
||
host: 'git.mosaicstack.dev',
|
||
owner: 'durable-owner',
|
||
repo: 'durable-owner/mosaic-brain',
|
||
cloneUrl: 'https://git.mosaicstack.dev/durable-owner/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',
|
||
'durable-owner',
|
||
),
|
||
).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);
|
||
expect(
|
||
existsSync(join(root, directory, '.gitkeep')),
|
||
`${directory} tracked placeholder`,
|
||
).toBe(true);
|
||
}
|
||
const rules = readFileSync(join(root, '.gitignore'), 'utf8').trim().split('\n');
|
||
expect(rules).toEqual(
|
||
expect.arrayContaining([
|
||
'*.token',
|
||
'*.key',
|
||
'*.pem',
|
||
'.env',
|
||
'credentials.json',
|
||
'*.TOKEN',
|
||
'*.KEY',
|
||
'*.PEM',
|
||
'.env.*',
|
||
'credentials.*',
|
||
'secrets.*',
|
||
'id_rsa',
|
||
'id_ed25519',
|
||
]),
|
||
);
|
||
});
|
||
|
||
it('creates the brain root and memory directories owner-only under a permissive umask', async (): Promise<void> => {
|
||
const sut = await loadSut('MB-REQ-06 owner-only brain checkout');
|
||
const brain = join(tempRoot(), 'brain');
|
||
const previousUmask = process.umask(0o022);
|
||
try {
|
||
sut.createBrainSkeleton(brain);
|
||
} finally {
|
||
process.umask(previousUmask);
|
||
}
|
||
|
||
for (const path of [
|
||
brain,
|
||
...['agents', 'lanes', 'board', 'specs', 'methods', 'archives'].map((name) =>
|
||
join(brain, name),
|
||
),
|
||
]) {
|
||
expect(lstatSync(path).mode & 0o077, path).toBe(0);
|
||
}
|
||
});
|
||
|
||
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();
|
||
const brain = join(root, 'brain');
|
||
const outside = join(root, 'outside');
|
||
mkdirSync(brain);
|
||
mkdirSync(outside);
|
||
symlinkSync(outside, join(brain, 'agents'));
|
||
|
||
expect(() => sut.createBrainSkeleton(brain)).toThrow(/brain-layout-directory-unsafe/);
|
||
expect(existsSync(join(outside, '.gitkeep'))).toBe(false);
|
||
});
|
||
|
||
it('refuses a symlinked gitignore instead of reading and tracking its external target', async (): Promise<void> => {
|
||
const sut = await loadSut('MB-REQ-06 no-follow gitignore');
|
||
const root = tempRoot();
|
||
const brain = join(root, 'brain');
|
||
const outside = join(root, 'outside-secret');
|
||
mkdirSync(brain);
|
||
writeFileSync(outside, 'DO-NOT-TRACK\n');
|
||
symlinkSync(outside, join(brain, '.gitignore'));
|
||
|
||
expect(() => sut.createBrainSkeleton(brain)).toThrow(/brain-layout-ignore-unsafe/);
|
||
expect(readFileSync(outside, 'utf8')).toBe('DO-NOT-TRACK\n');
|
||
});
|
||
|
||
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.5 terminal classes', (): void => {
|
||
it.each([
|
||
['ok', 0, 'validation-verified'],
|
||
['refused', 10, 'no-token-for-identity'],
|
||
['refused', 10, 'provider-identity-mismatch'],
|
||
['error', 20, 'estate-registry-invalid'],
|
||
['indeterminate', 30, 'provider-unavailable'],
|
||
['indeterminate', 30, 'identity-not-visible'],
|
||
['indeterminate', 30, 'identity-not-measured'],
|
||
['refused', 10, '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', 'validation-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');
|
||
});
|
||
|
||
it('makes an apparent write ok indeterminate when the side-effect-free differential is absent', async (): Promise<void> => {
|
||
const sut = await loadSut('AC-MB-01 required write differential');
|
||
const raw = JSON.parse(credentialResult('ok', 'validation-verified')) as {
|
||
evidence: { writeDifferential: unknown };
|
||
};
|
||
raw.evidence.writeDifferential = null;
|
||
|
||
const result = sut.assessCredentialResult(JSON.stringify(raw));
|
||
|
||
expect(result).toMatchObject({
|
||
outcome: 'indeterminate',
|
||
exitCode: 30,
|
||
reasonCode: 'readback-missing',
|
||
});
|
||
});
|
||
|
||
it('rejects the superseded indeterminate/credential-rejected pairing from before the v1.4 correction', async (): Promise<void> => {
|
||
const sut = await loadSut('AC-MB-01 credential-rejected stable class');
|
||
|
||
const legacy = JSON.parse(credentialResult('indeterminate', 'provider-unavailable')) as Record<
|
||
string,
|
||
unknown
|
||
>;
|
||
legacy['reason'] = {
|
||
code: 'credential-rejected',
|
||
message: 'superseded classification',
|
||
};
|
||
const result = sut.assessCredentialResult(JSON.stringify(legacy));
|
||
|
||
expect(result.outcome).toBe('indeterminate');
|
||
expect(result.exitCode).toBe(30);
|
||
expect(result.reasonCode).toBe('unexpected-provider-shape');
|
||
});
|
||
|
||
it('preserves scope-limited identity as not-measured rather than a dead credential refusal', async (): Promise<void> => {
|
||
const sut = await loadSut('AC-MB-01 scope-limited identity is not dead credential');
|
||
|
||
const result = sut.assessCredentialResult(
|
||
credentialResult('indeterminate', 'identity-not-measured'),
|
||
);
|
||
|
||
expect(result).toMatchObject({
|
||
outcome: 'indeterminate',
|
||
exitCode: 30,
|
||
reasonCode: 'identity-not-measured',
|
||
});
|
||
});
|
||
|
||
it('refuses an apparent ok whose provider /user read-back names a different principal', async (): Promise<void> => {
|
||
const sut = await loadSut('AC-MB-01 provider identity MISMATCH is first-class');
|
||
|
||
const result = sut.assessCredentialResult(
|
||
credentialResult('ok', 'validation-verified', 'looks successful', 'Mos'),
|
||
);
|
||
|
||
expect(result.outcome).toBe('refused');
|
||
expect(result.exitCode).toBe(10);
|
||
expect(result.reasonCode).toBe('provider-identity-mismatch');
|
||
});
|
||
});
|
||
|
||
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', 'validation-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 => {
|
||
const activeLaneOwner = (): MigrationOwnerResolution => ({
|
||
verdict: 'resolved',
|
||
reasonCode: 'active-lane-owner-verified',
|
||
principal: { name: 'lane:lane-a', kind: 'active-lane' },
|
||
authority: {
|
||
system: 'mosaic-mission-state',
|
||
endpoint: 'file:///var/lib/mosaic/missions/lane-a.json',
|
||
contentType: 'application/json',
|
||
},
|
||
});
|
||
|
||
it('uses an injected authoritative owner resolver for unit mechanics without claiming live owner validation', async (): Promise<void> => {
|
||
const sut = await loadSut('MB-REQ-07 injected owner-resolver seam');
|
||
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 },
|
||
activeLaneOwner,
|
||
(): boolean => true,
|
||
);
|
||
|
||
expect(plan.status).toBe('ready');
|
||
expect(plan.owner).toEqual({ name: 'lane:lane-a', kind: 'active-lane' });
|
||
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('retains and reports otherwise-benign legacy content when no approved scanner is available', async (): Promise<void> => {
|
||
const sut = await loadSut('MB-REQ-06 approved scanner required');
|
||
const root = tempRoot();
|
||
const sourceRoot = join(root, 'local-memory');
|
||
mkdirSync(join(sourceRoot, 'lanes', 'lane-a'), { recursive: true });
|
||
const source = join(sourceRoot, 'lanes', 'lane-a', 'finding.md');
|
||
writeFileSync(source, 'ordinary finding\n');
|
||
|
||
const plan = sut.discoverBrainMigration(
|
||
{
|
||
sourceRoot,
|
||
brainRoot: join(root, 'brain'),
|
||
seat: 'seat-a',
|
||
lane: 'lane-a',
|
||
laneActive: true,
|
||
},
|
||
activeLaneOwner,
|
||
);
|
||
|
||
expect(plan.candidates).toHaveLength(0);
|
||
expect(plan.reported).toEqual(
|
||
expect.arrayContaining([
|
||
expect.objectContaining({
|
||
path: source,
|
||
reason: expect.stringMatching(/scanner|approval/i),
|
||
}),
|
||
]),
|
||
);
|
||
});
|
||
|
||
it('ignores a caller-asserted owner string/validated flag and leaves the gate blocking', async (): Promise<void> => {
|
||
const sut = await loadSut('MB-REQ-09 caller owner assertion cannot satisfy 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 callerAssertion = {
|
||
sourceRoot,
|
||
brainRoot: join(root, 'brain'),
|
||
seat: 'seat-a',
|
||
lane: 'closed-lane',
|
||
laneActive: false,
|
||
owner: { name: 'some-string', kind: 'durable-team', validated: true },
|
||
};
|
||
|
||
const plan = sut.discoverBrainMigration(callerAssertion);
|
||
|
||
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.each([
|
||
['mission seat', { name: 'user:be-coder-07', kind: 'mission-seat' }],
|
||
['unicode dash', { name: 'team:platform–maintainers', kind: 'durable-team' }],
|
||
['padded', { name: ' team:platform-maintainers ', kind: 'durable-team' }],
|
||
['dot presentation', { name: 'team:platform.maintainers', kind: 'durable-team' }],
|
||
['space presentation', { name: 'team:platform maintainers', kind: 'durable-team' }],
|
||
] as const)(
|
||
'rejects %s owner evidence through the injected resolver allowlist',
|
||
async (_caseName, principal): Promise<void> => {
|
||
const sut = await loadSut('MB-REQ-09 durable-owner allowlist');
|
||
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');
|
||
|
||
let resolverCalls = 0;
|
||
const plan = sut.discoverBrainMigration(
|
||
{
|
||
sourceRoot,
|
||
brainRoot: join(root, 'brain'),
|
||
seat: 'seat-a',
|
||
lane: 'closed-lane',
|
||
laneActive: false,
|
||
},
|
||
(): MigrationOwnerResolution => {
|
||
resolverCalls += 1;
|
||
return {
|
||
verdict: 'resolved',
|
||
reasonCode: 'owner-verified',
|
||
principal,
|
||
authority: {
|
||
system: 'gitea',
|
||
endpoint: 'GET /api/v1/teams/1',
|
||
contentType: 'application/json',
|
||
},
|
||
};
|
||
},
|
||
);
|
||
|
||
expect(resolverCalls).toBe(1);
|
||
expect(plan.status).toBe('blocked');
|
||
expect(plan.candidates).toHaveLength(0);
|
||
},
|
||
);
|
||
|
||
it('reports secret-shaped legacy files without ever copying them into the brain', async (): Promise<void> => {
|
||
const sut = await loadSut('MB-REQ-06 migration secret exclusion');
|
||
const root = tempRoot();
|
||
const sourceRoot = join(root, 'local-memory');
|
||
const laneRoot = join(sourceRoot, 'lanes', 'lane-a');
|
||
const brainRoot = join(root, 'brain');
|
||
mkdirSync(laneRoot, { recursive: true });
|
||
const secretNames = [
|
||
'access.token',
|
||
'private.key',
|
||
'client.pem',
|
||
'CLIENT.PEM',
|
||
'.env',
|
||
'.env.local',
|
||
'.env.production',
|
||
'credentials.json',
|
||
'credentials.yaml',
|
||
'id_rsa',
|
||
'secrets.txt',
|
||
];
|
||
for (const name of secretNames) {
|
||
writeFileSync(join(laneRoot, name), 'DO-NOT-MIGRATE\n');
|
||
}
|
||
const nestedSecretPaths = [
|
||
join('.env.d', 'database.txt'),
|
||
join('credentials.d', 'token.txt'),
|
||
join('secrets', 'private.txt'),
|
||
];
|
||
for (const path of nestedSecretPaths) {
|
||
mkdirSync(join(laneRoot, path, '..'), { recursive: true });
|
||
writeFileSync(join(laneRoot, path), 'NESTED-SECRET-MARKER\n');
|
||
}
|
||
const embeddedSecrets = new Map([
|
||
['benign-auth.md', 'Authorization: Bearer NESTED-CONTENT-SECRET-MARKER-1234567890\n'],
|
||
['benign-token.md', 'token=fixture-value\n'],
|
||
['benign-aws.md', 'AWS_SECRET_ACCESS_KEY=fixture-value\n'],
|
||
['benign-header.md', 'X-Api-Key: fixture-value\n'],
|
||
['benign-jwt.md', 'session=eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJmaXh0dXJlIn0.signature\n'],
|
||
]);
|
||
for (const [name, content] of embeddedSecrets) writeFileSync(join(laneRoot, name), content);
|
||
|
||
const plan = sut.discoverBrainMigration(
|
||
{ sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
|
||
activeLaneOwner,
|
||
(): boolean => true,
|
||
);
|
||
|
||
expect(plan.status).toBe('ready');
|
||
expect(plan.candidates).toHaveLength(0);
|
||
expect(plan.reported).toHaveLength(
|
||
secretNames.length + nestedSecretPaths.length + embeddedSecrets.size,
|
||
);
|
||
expect(plan.reported.every((entry) => /secret/i.test(entry.reason))).toBe(true);
|
||
expect(JSON.stringify(plan)).not.toMatch(/NESTED-(?:CONTENT-)?SECRET-MARKER/);
|
||
expect(existsSync(brainRoot)).toBe(false);
|
||
});
|
||
|
||
it('publishes collision-safe append-only copies, retains the source explicitly, 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 },
|
||
activeLaneOwner,
|
||
(): boolean => 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, entries): MigrationPublishEvidence => {
|
||
publishedPaths = entries.map((entry): string => entry.path);
|
||
return { commit: 'a'.repeat(40), remoteHead: 'a'.repeat(40), reachable: true };
|
||
},
|
||
brainRoot,
|
||
);
|
||
|
||
expect(result.status).toBe('reported');
|
||
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(true);
|
||
expect(result.reported).toEqual(
|
||
expect.arrayContaining([
|
||
expect.objectContaining({
|
||
path: candidate.source,
|
||
reason: expect.stringMatching(/retained/i),
|
||
}),
|
||
]),
|
||
);
|
||
expect(publishedPaths).toContain(candidate.destination);
|
||
expect(publishedPaths).toContain(candidate.archive);
|
||
});
|
||
|
||
it.each(['modified', 'replaced'] as const)(
|
||
'retains and reports a source that is %s while its published snapshot is in flight',
|
||
async (change): Promise<void> => {
|
||
const sut = await loadSut('MB-REQ-07 source identity before cleanup');
|
||
const root = tempRoot();
|
||
const sourceRoot = join(root, 'local-memory');
|
||
const brainRoot = join(root, 'brain');
|
||
mkdirSync(join(sourceRoot, 'lanes', 'lane-a'), { recursive: true });
|
||
const source = join(sourceRoot, 'lanes', 'lane-a', 'finding.md');
|
||
writeFileSync(source, 'published snapshot\n');
|
||
const plan = sut.discoverBrainMigration(
|
||
{ sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
|
||
activeLaneOwner,
|
||
(): boolean => true,
|
||
);
|
||
|
||
const result = sut.migrateBrainState(
|
||
plan,
|
||
(): MigrationPublishEvidence => {
|
||
if (change === 'replaced') rmSync(source);
|
||
writeFileSync(source, 'new concurrent state\n');
|
||
return { commit: 'a'.repeat(40), remoteHead: 'a'.repeat(40), reachable: true };
|
||
},
|
||
brainRoot,
|
||
);
|
||
|
||
expect(result.status).toBe('reported');
|
||
expect(readFileSync(source, 'utf8')).toBe('new concurrent state\n');
|
||
expect(readFileSync(plan.candidates[0]!.destination, 'utf8')).toBe('published snapshot\n');
|
||
expect(result.reported).toEqual(
|
||
expect.arrayContaining([
|
||
expect.objectContaining({
|
||
path: source,
|
||
reason: expect.stringMatching(/retained|cleanup/i),
|
||
}),
|
||
]),
|
||
);
|
||
},
|
||
);
|
||
|
||
it('holds the destination directory while a concurrent actor replaces its ancestor with a symlink', async (): Promise<void> => {
|
||
const sut = await loadSut('MB-REQ-07 destination descriptor race');
|
||
const root = tempRoot();
|
||
const sourceRoot = join(root, 'local-memory');
|
||
const brainRoot = join(root, 'brain');
|
||
const outside = join(root, 'outside');
|
||
mkdirSync(join(sourceRoot, 'lanes', 'lane-a'), { recursive: true });
|
||
mkdirSync(join(brainRoot, 'lanes', 'lane-a'), { recursive: true });
|
||
mkdirSync(outside);
|
||
const source = join(sourceRoot, 'lanes', 'lane-a', 'finding.md');
|
||
writeFileSync(source, 'lane state\n');
|
||
const plan = sut.discoverBrainMigration(
|
||
{ sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
|
||
activeLaneOwner,
|
||
(): boolean => true,
|
||
);
|
||
let swapped = false;
|
||
let publishCalls = 0;
|
||
|
||
const result = sut.migrateBrainState(
|
||
plan,
|
||
(): MigrationPublishEvidence => {
|
||
publishCalls += 1;
|
||
return { commit: 'a'.repeat(40), remoteHead: 'a'.repeat(40), reachable: true };
|
||
},
|
||
brainRoot,
|
||
{
|
||
beforeDestinationWrite: (): void => {
|
||
if (swapped) return;
|
||
swapped = true;
|
||
renameSync(join(brainRoot, 'lanes', 'lane-a'), join(brainRoot, 'lanes', 'lane-a-held'));
|
||
symlinkSync(outside, join(brainRoot, 'lanes', 'lane-a'));
|
||
},
|
||
},
|
||
);
|
||
|
||
expect(result.status).toBe('failed');
|
||
expect(readFileSync(source, 'utf8')).toBe('lane state\n');
|
||
expect(readdirSync(outside)).toEqual([]);
|
||
expect(publishCalls).toBe(0);
|
||
});
|
||
|
||
it('refuses nested symlink destinations without copying a migration outside the brain', async (): Promise<void> => {
|
||
const sut = await loadSut('MB-REQ-07 migration destination no-follow');
|
||
const root = tempRoot();
|
||
const sourceRoot = join(root, 'local-memory');
|
||
const brainRoot = join(root, 'brain');
|
||
const outside = join(root, 'outside');
|
||
mkdirSync(join(sourceRoot, 'lanes', 'lane-a'), { recursive: true });
|
||
mkdirSync(join(brainRoot, 'lanes'), { recursive: true });
|
||
mkdirSync(outside);
|
||
const source = join(sourceRoot, 'lanes', 'lane-a', 'finding.md');
|
||
writeFileSync(source, 'lane state\n');
|
||
symlinkSync(outside, join(brainRoot, 'lanes', 'lane-a'));
|
||
const plan = sut.discoverBrainMigration(
|
||
{ sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
|
||
activeLaneOwner,
|
||
(): boolean => true,
|
||
);
|
||
let publishCalls = 0;
|
||
|
||
const result = sut.migrateBrainState(
|
||
plan,
|
||
(): MigrationPublishEvidence => {
|
||
publishCalls += 1;
|
||
return { commit: 'a'.repeat(40), remoteHead: 'a'.repeat(40), reachable: true };
|
||
},
|
||
brainRoot,
|
||
);
|
||
|
||
expect(result.status).toBe('failed');
|
||
expect(readFileSync(source, 'utf8')).toBe('lane state\n');
|
||
expect(readdirSync(outside)).toEqual([]);
|
||
expect(publishCalls).toBe(0);
|
||
});
|
||
|
||
it('never path-unlinks a source when identity-aware unlink is unavailable', async (): Promise<void> => {
|
||
const sut = await loadSut('MB-REQ-07 no path-based source unlink');
|
||
const root = tempRoot();
|
||
const sourceRoot = join(root, 'local-memory');
|
||
const brainRoot = join(root, 'brain');
|
||
mkdirSync(join(sourceRoot, 'lanes', 'lane-a'), { recursive: true });
|
||
const source = join(sourceRoot, 'lanes', 'lane-a', 'finding.md');
|
||
writeFileSync(source, 'published snapshot\n');
|
||
const plan = sut.discoverBrainMigration(
|
||
{ sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
|
||
activeLaneOwner,
|
||
(): boolean => true,
|
||
);
|
||
let cleanupHookCalled = false;
|
||
|
||
const result = sut.migrateBrainState(
|
||
plan,
|
||
(): MigrationPublishEvidence => ({
|
||
commit: 'a'.repeat(40),
|
||
remoteHead: 'a'.repeat(40),
|
||
reachable: true,
|
||
}),
|
||
brainRoot,
|
||
{
|
||
beforeSourceCleanup: (): void => {
|
||
cleanupHookCalled = true;
|
||
rmSync(source);
|
||
writeFileSync(source, 'replacement must survive\n');
|
||
},
|
||
},
|
||
);
|
||
|
||
expect(cleanupHookCalled).toBe(true);
|
||
expect(result.status).toBe('reported');
|
||
expect(readFileSync(source, 'utf8')).toBe('replacement must survive\n');
|
||
expect(result.reported).toEqual(
|
||
expect.arrayContaining([
|
||
expect.objectContaining({ path: source, reason: expect.stringMatching(/retained/i) }),
|
||
]),
|
||
);
|
||
});
|
||
|
||
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 },
|
||
activeLaneOwner,
|
||
(): boolean => 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(plan.candidates[0]).toBeDefined();
|
||
expect(existsSync(plan.candidates[0]!.destination)).toBe(true);
|
||
expect(existsSync(plan.candidates[0]!.archive)).toBe(true);
|
||
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,
|
||
rootPrivate: false,
|
||
gitRepository: false,
|
||
remote: null,
|
||
branch: null,
|
||
worktreeState: 'unmeasurable',
|
||
access: sut.assessCredentialResult(credentialResult('refused', 'no-token-for-identity')),
|
||
},
|
||
expected,
|
||
);
|
||
expect(missing.map((finding) => finding.code)).toEqual(
|
||
expect.arrayContaining(['brain-clone-missing', 'brain-write-access-refused']),
|
||
);
|
||
|
||
const defects = sut.evaluateBrainDoctor(
|
||
{
|
||
rootExists: true,
|
||
rootPrivate: true,
|
||
gitRepository: true,
|
||
remote: 'https://git.uscllc.com/usc/mosaic-brain.git',
|
||
branch: 'main',
|
||
worktreeState: 'dirty',
|
||
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',
|
||
'durable-owner',
|
||
);
|
||
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(['mosaic', 'git', 'git']);
|
||
expect(actions[0]?.findingCode).toBe('brain-write-access-refused');
|
||
expect(actions[1]?.findingCode).toBe('brain-clone-missing');
|
||
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',
|
||
'durable-owner',
|
||
);
|
||
|
||
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' });
|
||
});
|
||
});
|