fix(fleet): seed seat MCP preflight config
AMD1213-B5: derive Claude seat MCP configuration from the active installed runtime base and inspect the isolated seat during fleet launch.
This commit is contained in:
@@ -1,6 +1,17 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach, type MockInstance } from 'vitest';
|
||||
import { Command } from 'commander';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, symlinkSync, rmSync } from 'node:fs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import {
|
||||
chmodSync,
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdtempSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
symlinkSync,
|
||||
rmSync,
|
||||
} from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import {
|
||||
@@ -8,6 +19,7 @@ import {
|
||||
enumerateSkillDirs,
|
||||
piForceSkillNames,
|
||||
registerRuntimeLaunchers,
|
||||
checkSequentialThinking,
|
||||
type RuntimeLaunchHandler,
|
||||
type ClaudexLaunchHandler,
|
||||
} from './launch.js';
|
||||
@@ -86,6 +98,157 @@ describe('registerRuntimeLaunchers — non-yolo subcommands', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkSequentialThinking', () => {
|
||||
it('passes with a seeded seat even when operator HOME has no MCP configuration', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'mosaic-seq-home-'));
|
||||
const agentDir = mkdtempSync(join(tmpdir(), 'mosaic-seq-seat-'));
|
||||
const installed = mkdtempSync(join(tmpdir(), 'mosaic-seq-installed-'));
|
||||
const checker = join(installed, 'tools', '_scripts', 'mosaic-ensure-sequential-thinking');
|
||||
try {
|
||||
mkdirSync(join(installed, 'tools', '_scripts'), { recursive: true });
|
||||
copyFileSync(
|
||||
join(process.cwd(), 'framework', 'tools', '_scripts', 'mosaic-ensure-sequential-thinking'),
|
||||
checker,
|
||||
);
|
||||
mkdirSync(join(agentDir, '.claude'), { recursive: true });
|
||||
writeFileSync(
|
||||
join(agentDir, '.claude', '.claude.json'),
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
'sequential-thinking': {
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.stubEnv('MOSAIC_HOME', installed);
|
||||
vi.stubEnv('HOME', home);
|
||||
expect(() =>
|
||||
checkSequentialThinking('claude', { agentDir, mosaicHome: installed }),
|
||||
).not.toThrow();
|
||||
} finally {
|
||||
vi.unstubAllEnvs();
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
rmSync(agentDir, { recursive: true, force: true });
|
||||
rmSync(installed, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('repairs a legacy seat config in place without using operator HOME', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'mosaic-seq-home-'));
|
||||
const agentDir = mkdtempSync(join(tmpdir(), 'mosaic-seq-seat-'));
|
||||
const installed = mkdtempSync(join(tmpdir(), 'mosaic-seq-installed-'));
|
||||
const checker = join(installed, 'tools', '_scripts', 'mosaic-ensure-sequential-thinking');
|
||||
const bin = join(installed, 'bin');
|
||||
try {
|
||||
mkdirSync(join(installed, 'tools', '_scripts'), { recursive: true });
|
||||
mkdirSync(join(agentDir, '.claude'), { recursive: true });
|
||||
mkdirSync(bin, { recursive: true });
|
||||
copyFileSync(
|
||||
join(process.cwd(), 'framework', 'tools', '_scripts', 'mosaic-ensure-sequential-thinking'),
|
||||
checker,
|
||||
);
|
||||
for (const name of ['node', 'npx']) {
|
||||
writeFileSync(join(bin, name), '#!/usr/bin/env bash\nexit 0\n');
|
||||
chmodSync(join(bin, name), 0o755);
|
||||
}
|
||||
writeFileSync(
|
||||
join(agentDir, '.claude', '.claude.json'),
|
||||
JSON.stringify({ hasCompletedOnboarding: true, theme: 'dark' }),
|
||||
);
|
||||
const env = { ...process.env, HOME: home, PATH: `${bin}:${process.env.PATH}` };
|
||||
expect(
|
||||
spawnSync(
|
||||
checker,
|
||||
['--runtime', 'claude', '--claude-config-dir', join(agentDir, '.claude')],
|
||||
{
|
||||
env,
|
||||
},
|
||||
).status,
|
||||
).toBe(0);
|
||||
expect(
|
||||
spawnSync(
|
||||
checker,
|
||||
['--check', '--runtime', 'claude', '--claude-config-dir', join(agentDir, '.claude')],
|
||||
{ env },
|
||||
).status,
|
||||
).toBe(0);
|
||||
expect(
|
||||
JSON.parse(readFileSync(join(agentDir, '.claude', '.claude.json'), 'utf8')),
|
||||
).toMatchObject({
|
||||
hasCompletedOnboarding: true,
|
||||
theme: 'dark',
|
||||
mcpServers: { 'sequential-thinking': { command: 'npx' } },
|
||||
});
|
||||
expect(existsSync(join(home, '.claude.json'))).toBe(false);
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
rmSync(agentDir, { recursive: true, force: true });
|
||||
rmSync(installed, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('rejects a group-writable installed helper root', () => {
|
||||
const agentDir = mkdtempSync(join(tmpdir(), 'mosaic-seq-seat-'));
|
||||
const installed = mkdtempSync(join(tmpdir(), 'mosaic-seq-installed-'));
|
||||
const checker = join(installed, 'tools', '_scripts', 'mosaic-ensure-sequential-thinking');
|
||||
try {
|
||||
mkdirSync(join(installed, 'tools', '_scripts'), { recursive: true });
|
||||
copyFileSync(
|
||||
join(process.cwd(), 'framework', 'tools', '_scripts', 'mosaic-ensure-sequential-thinking'),
|
||||
checker,
|
||||
);
|
||||
chmodSync(installed, 0o770);
|
||||
expect(() => checkSequentialThinking('claude', { agentDir, mosaicHome: installed })).toThrow(
|
||||
/not a trusted installed file/,
|
||||
);
|
||||
} finally {
|
||||
chmodSync(installed, 0o700);
|
||||
rmSync(agentDir, { recursive: true, force: true });
|
||||
rmSync(installed, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('refuses an empty seat even when operator HOME is configured', () => {
|
||||
const home = mkdtempSync(join(tmpdir(), 'mosaic-seq-home-'));
|
||||
const agentDir = mkdtempSync(join(tmpdir(), 'mosaic-seq-seat-'));
|
||||
const installed = mkdtempSync(join(tmpdir(), 'mosaic-seq-installed-'));
|
||||
const checker = join(installed, 'tools', '_scripts', 'mosaic-ensure-sequential-thinking');
|
||||
const exit = vi.spyOn(process, 'exit').mockImplementation(exitThrows);
|
||||
try {
|
||||
mkdirSync(join(installed, 'tools', '_scripts'), { recursive: true });
|
||||
copyFileSync(
|
||||
join(process.cwd(), 'framework', 'tools', '_scripts', 'mosaic-ensure-sequential-thinking'),
|
||||
checker,
|
||||
);
|
||||
writeFileSync(
|
||||
join(home, '.claude.json'),
|
||||
JSON.stringify({
|
||||
mcpServers: {
|
||||
'sequential-thinking': {
|
||||
command: 'npx',
|
||||
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
vi.stubEnv('MOSAIC_HOME', installed);
|
||||
vi.stubEnv('HOME', home);
|
||||
expect(() => checkSequentialThinking('claude', { agentDir, mosaicHome: installed })).toThrow(
|
||||
'process.exit called',
|
||||
);
|
||||
expect(exit).toHaveBeenCalledWith(1);
|
||||
} finally {
|
||||
exit.mockRestore();
|
||||
vi.unstubAllEnvs();
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
rmSync(agentDir, { recursive: true, force: true });
|
||||
rmSync(installed, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildPiSkillArgs', () => {
|
||||
it('disables auto-discovery but force-loads fleet-critical skills by default', () => {
|
||||
expect(buildPiSkillArgs([], {}, fakeSkills, fakeForced)).toEqual([
|
||||
|
||||
Reference in New Issue
Block a user