Files
stack/packages/mosaic/src/runtime/claude-settings-base.spec.ts
T
terra 326a1a58b5 fix(fleet): harden managed launch composition
AMD1213-C: repair stale array consumer, fail closed on foreign link provenance, validate manifests before mutation, and exercise the fleet MCP preflight call path.
2026-08-13 15:37:32 -05:00

166 lines
5.6 KiB
TypeScript

import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
type Json = null | boolean | number | string | Json[] | { [key: string]: Json };
type JsonObject = { [key: string]: Json };
const frameworkRoot = fileURLToPath(new URL('../../framework/', import.meta.url));
const basePath = `${frameworkRoot}runtime/claude/settings.json`;
const overlayPath = `${frameworkRoot}runtime/claude/lease-overlay.json`;
const gatedFixturePath = fileURLToPath(
new URL('./fixtures/claude-settings.gated.pre-split.json', import.meta.url),
);
function readJson(path: string): JsonObject {
return JSON.parse(readFileSync(path, 'utf8')) as JsonObject;
}
function isObject(value: unknown): value is JsonObject {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
// Production composition uses universal last-layer-wins array replacement. The
// lease overlay therefore carries complete affected event arrays, including the
// two QA carry-forward entries needed to avoid dropping non-lease hooks.
function deepMerge(base: Json, overlay: Json): Json {
if (Array.isArray(base) && Array.isArray(overlay)) return overlay;
if (isObject(base) && isObject(overlay)) {
const merged: JsonObject = { ...base };
for (const [key, value] of Object.entries(overlay)) {
merged[key] = key in merged ? deepMerge(merged[key]!, value) : value;
}
return merged;
}
return overlay;
}
function normalize(value: Json): Json {
if (Array.isArray(value)) {
return value
.map(normalize)
.sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
}
if (isObject(value)) {
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, nested]) => [key, normalize(nested)]),
);
}
return value;
}
function hookCommands(settings: JsonObject): string[] {
const hooks = settings['hooks'];
if (!isObject(hooks)) return [];
return Object.values(hooks).flatMap((event) => {
if (!Array.isArray(event)) return [];
return event.flatMap((entry) => {
if (!isObject(entry) || !Array.isArray(entry['hooks'])) return [];
return entry['hooks'].flatMap((hook) =>
isObject(hook) && typeof hook['command'] === 'string' ? [hook['command']] : [],
);
});
});
}
const sequentialThinking: JsonObject = {
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
};
describe('canonical Claude base and lease-promotion overlay', () => {
it('keeps every lease command out of the ungated base', () => {
const base = readJson(basePath);
const commands = hookCommands(base);
for (const marker of ['mutator-gate', 'receipt-observer', 'promote-', 'revoke-lease']) {
expect(
commands.some((command) => command.includes(marker)),
marker,
).toBe(false);
}
});
it('reconstructs the pre-split gated hooks while retaining the canonical MCP correction', () => {
const base = readJson(basePath);
const overlay = readJson(overlayPath);
const preSplit = readJson(gatedFixturePath);
const expected: JsonObject = {
...preSplit,
hooks: {
...(preSplit['hooks'] as JsonObject),
Stop: [
{
hooks: [
{
type: 'command',
command: '~/.config/mosaic/tools/qa/reflect-stop-hook.sh',
timeout: 15,
},
],
},
{
hooks: [
{
type: 'command',
command:
'python3 ~/.config/mosaic/tools/lease-broker/receipt-observer-client.py --runtime claude --latest-entry; observer_status=$?; python3 ~/.config/mosaic/tools/lease-broker/promote-complete.py; exit $observer_status',
timeout: 15,
},
],
},
],
},
mcpServers: { 'sequential-thinking': sequentialThinking },
};
expect(normalize(deepMerge(base, overlay))).toEqual(normalize(expected));
});
it('ships sequential-thinking in the base', () => {
const base = readJson(basePath);
expect(base['mcpServers']).toEqual({ 'sequential-thinking': sequentialThinking });
});
it('carries six lease commands plus exactly two deliberate QA carry-forward commands', () => {
const overlay = readJson(overlayPath);
expect(Object.keys(overlay)).toEqual(['hooks']);
const commands = hookCommands(overlay);
const lease = commands.filter((command) =>
/mutator-gate|receipt-observer|promote-|revoke-lease/.test(command),
);
const qa = commands.filter((command) => /prevent-memory-write|reflect-stop/.test(command));
expect(lease).toHaveLength(6);
expect(qa).toHaveLength(2);
expect(commands).toHaveLength(8);
});
it.each(['prevent-memory-write', 'reflect-stop'])(
'fails lossless reconstruction if QA carry-forward %s is removed',
(marker) => {
const base = readJson(basePath);
const overlay = readJson(overlayPath);
const expected = {
...readJson(gatedFixturePath),
mcpServers: { 'sequential-thinking': sequentialThinking },
};
const hooks = overlay['hooks'] as JsonObject;
const mutated: JsonObject = {
hooks: Object.fromEntries(
Object.entries(hooks).map(([event, entries]) => [
event,
Array.isArray(entries)
? entries.filter((entry) => !JSON.stringify(entry).includes(marker))
: entries,
]),
),
};
expect(normalize(deepMerge(base, mutated))).not.toEqual(normalize(expected));
},
);
});