test(#1051): preregister mosaic-brain acceptance contract

This commit is contained in:
2026-08-05 14:24:27 -05:00
parent 5916aeefd6
commit 52d91fd869
3 changed files with 750 additions and 0 deletions
+43
View File
@@ -79,6 +79,49 @@ Jarvis (v0.2.0) is a self-hosted AI assistant with a Python FastAPI backend and
--- ---
## Per-estate durable agent working memory (#1051)
### Problem and objective
Agent and lane continuity currently accumulates as plain local files with no repository backing. The installer must make a private, per-estate `mosaic-brain` clone at `~/.mosaic` reproducible without authorizing cross-estate access or introducing an independent credential path.
### Normative requirements
1. `MB-REQ-01` (R1): Ensure the target estate's existing `mosaic-brain` can be cloned to `~/.mosaic`; repository creation and live access granting remain broker-mediated.
2. `MB-REQ-02` (R2/Q1): Derive estate and brain target from the configured target git host through the credential broker's estate registry. A second `brain_repo` authority and host-machine inference are forbidden; an unknown host fails closed with a named diagnosis.
3. `MB-REQ-03` (R3): Seat access is granted only through `mosaic cred`; callers must never resolve or read a token independently. Live grant verification is gated on MC-CRED-01 implementation.
4. `MB-REQ-04` (R4): The eventual live postcondition requires `~/.mosaic` to be a `main`-branch git repo with the expected remote and a seat-owned read/write round-trip. This live validation is gated on MC-CRED-01 implementation and cannot be replaced by a clone exit code.
5. `MB-REQ-05` (R5): The out-of-estate refusal control covers both Git and API resolver axes. Axis disagreement is `indeterminate` failure, never permission; contract tests bind to the broker's four terminal classes and stable reason codes.
6. `MB-REQ-06` (R6): The brain skeleton excludes `*.token`, `*.key`, `*.pem`, `.env`, and `credentials.json`; credentials remain broker-owned and no error path may print secret material.
7. `MB-REQ-07` (R7): Detect existing local lane directories and seat state files, migrate them into the durable layout without overwrite or deletion, and explicitly report every detected item that cannot be migrated. Lane findings are append-only; `board/` has a named single writer; writes push immediately rather than on a timer.
8. `MB-REQ-08` (R8): `mosaic doctor` reports missing clone, wrong remote, incomplete write-access evidence, and uncommitted local state. `--fix` repairs the first three only through the approved installer/broker path; it never hand-rolls credential resolution.
9. `MB-REQ-09`: Retention is ownership-first and archive-only. Every retained artifact requires a named durable owner; absent or non-durable ownership leaves the gate open and blocking. Age and size never authorize deletion.
10. `MB-REQ-10`: Brain provisioning occupies canonical installer P7 only after the applicable P5 credential postcondition commits; canonical phase numbers are unchanged.
### Current delivery slice
In scope now: estate derivation, secret exclusion, non-destructive migration, doctor reporting/repair orchestration, and red-first tests over all four credential-contract terminal classes. Live grant and live read/write round-trip evidence remain explicitly gated on the working MC-CRED-01 broker and must not be mocked or replaced by independent token lookup.
### Acceptance criteria
1. `AC-MB-01`: Contract tests observe RED before implementation and then distinguish `ok/0`, `refused/10`, `error/20`, and `indeterminate/30`, preserving stable reason codes including `identity-not-found`, `credential-rejected`, and `provider-unavailable`.
2. `AC-MB-02`: Estate resolution uses the configured target git host and one registry; unknown, mismatched, and host-machine-derived inputs fail closed.
3. `AC-MB-03`: A clean fixture contains the required layout and exact secret exclusions, and seeded secret-shaped files remain ignored without their values appearing in output.
4. `AC-MB-04`: Migration moves lane-durable and seat-state content into collision-safe archive/ledger paths, preserves source on any incomplete move, never overwrites an existing finding, never deletes by age/size, and reports unresolved items explicitly.
5. `AC-MB-05`: Doctor detects all four R8 defect classes; `--fix` repairs eligible classes through the approved P7/broker seam and leaves unresolved credential-dependent states visible.
6. `AC-MB-06`: Git-axis and API-axis refusal must both be authoritative `refused` outcomes with matching stable reason codes; any disagreement yields `indeterminate`.
7. `AC-MB-07`: Independent code review and security review pass at the exact head, and HOMELAB Woodpecker instance `mosaic` is terminal green before integration.
8. `AC-MB-08`: Integration into `next` is reported only as **believed-fixed, pending validation AND pending promotion to `main`**; issue #1051 remains open for #1037 promotion and W-jarvis validation.
### Constraints and risks
- MC-CRED-01 contract v1.3 is the caller boundary; no independent credential/token lookup is permitted.
- C1 owns installer phase sequencing. This slice consumes P5/P7 ordering without renumbering or duplicating the phase machine.
- Lane content is findings, so last-writer-wins is data loss. Append-only names and explicit collision handling are mandatory.
- A created-but-empty brain beside unbacked local doctrine fails the objective; migration is a primary acceptance gate.
---
## Compaction Refresh Trust Lifecycle (M1, #827#830) ## Compaction Refresh Trust Lifecycle (M1, #827#830)
### Problem and objective ### Problem and objective
@@ -0,0 +1,84 @@
# #1051 — per-estate mosaic-brain installer
Last updated: 2026-08-05
## Objective
Codify estate-derived, repository-backed `~/.mosaic` support with secret exclusions, non-destructive migration, doctor diagnostics/fixes, and credential-contract terminal-class handling. Live broker grants and live read/write round-trips remain gated on MC-CRED-01.
## Sources and bindings
- Provider issue: HOMELAB `git.mosaicstack.dev`, `GET /api/v1/repos/mosaicstack/stack/issues/1051`, `application/json;charset=utf-8`.
- Issue requirements: R1R8 read directly on 2026-08-05.
- MC-CRED caller contract: v1.3, SHA-256 `8cfa4853d2b0b0e8cc9e792fa8411310e16d7704c06e0af9d9a57155131d8086` at intake.
- Fleet doctrine: SHA-256 `026b43322e0551ef15b646a9f30d3a6aef58c662a810b732be2a03b1ecf7d36e` at intake.
- Base: HOMELAB provider `next` = `4df478cdd150fdf8d52ea109f02ade5d85017acd`; `main` = `5916aeefd6ed12bcac086c6834c7f6c4ae38e1bc`; provider branch objects matched fetched refs and `main` is reachable from `next`.
## Scope
### In now
- R2/Q1 target-host estate derivation using one registry.
- R5 both-axis refusal parity and disagreement failure.
- R6 exact secret exclusions and no secret-bearing diagnostics.
- R7 detection plus non-destructive, collision-safe migration/reporting.
- R8 doctor checks and approved-seam fix orchestration.
- Red-first tests over all four contract terminal classes and stable reason codes.
### Gated / excluded
- R3 live grant: waits for working MC-CRED-01.
- R4 live seat-owned read/write round-trip: waits for working MC-CRED-01.
- No independent token lookup, grant helper, or shared-credential fallback.
- No phase renumbering; C1 owns the phase machine and provides the P5→P7 seam.
- No age/size reaping or deletion.
## Plan
1. Pre-register acceptance tests and observe each requirement RED for its own missing behavior.
2. Commit the red tests before implementation.
3. Implement a narrow brain provisioning/doctor helper that consumes broker JSON outcomes and the shared estate registry without credential resolution.
4. Implement safe migration and exact brain skeleton/ignore policy.
5. Integrate the helper into C1's P7 seam and `mosaic doctor` after C1 lands/rebase.
6. Run focused, package, installer, lint, typecheck, format, and situational security tests.
7. Run independent code and security reviews in parallel; remediate and re-review.
8. Push after HOMELAB queue guard, open PR to `next`, and wait for merge order C1 → MC-CRED → MB-BRAIN.
9. Re-take CI measurement at the rebased exact head; do not rework code solely because base evidence moved.
## Acceptance interpretation registered before results
- `ok/0`: complete authoritative evidence only.
- `refused/10`: complete authoritative denial only.
- `error/20`: local contract/control failure; never reinterpret as denial.
- `indeterminate/30`: incomplete/disagreeing evidence; fail closed, never resolve permissively.
- Both Git and API axes must return authoritative `refused` with the same stable reason code for R5. Any axis disagreement is `indeterminate`.
- Migration success requires the durable object to contain the moved item and no overwrite; incomplete moves retain the source and are reported.
- Secret exclusion is tested through both exact ignore rules and seeded secret-shaped controls; output is scanned without printing secret values.
## Budget
No explicit token ceiling was supplied. Working cap: 55K tokens for implementation/review and 3 focused remediation attempts per failure class. Reduce optional refactoring and documentation breadth before touching required acceptance scope.
## Risks
- C1 and MC-CRED branches have not merged into `next`; integration edits must wait for their exact interfaces or be confined to stable contract seams.
- A broker runtime test before MC-CRED lands would either fail for an irrelevant reason or pressure a hand-rolled workaround; contract fixtures are allowed, live capability claims are not.
- Migration can lose data through overwrite, cross-device move failure, or partial copy. Implementation must stage, verify resulting bytes, and retain/report source on incomplete transfer.
- `~/.mosaic` is a git repo, while current working state may live under multiple local roots; detection must be explicit and cannot treat age/size as ownership.
## Progress / evidence
- [x] Charter receipt accepted by `tl-mosaic`.
- [x] Issue #1051 R1R8 read directly from provider.
- [x] Contract re-derived at v1.3.
- [x] C1 P5→P7 seam receipt read; no brain implementation is in C1.
- [ ] RED acceptance set committed.
- [ ] Implementation green.
- [ ] Independent code review.
- [ ] Independent security review.
- [ ] HOMELAB CI terminal green at exact head.
- [ ] Integrated to `next` after C1 and MC-CRED.
## Completion language
Only: **believed-fixed, pending validation AND pending promotion to `main`**. Issue #1051 remains open; #1037 is the promotion vehicle and W-jarvis is the external validator.
@@ -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' });
});
});