agent-ecosystem/test/main/services/team/OpenCodeManagedOverlay.test.ts

190 lines
6.1 KiB
TypeScript

import { promises as fs } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import {
assertManagedOverlayDoesNotShadowUserConfig,
buildManagedOverlayConfig,
OpenCodeBehaviorSourceScanner,
OpenCodeManagedOverlayBuilder,
pickAppOwnedMcpServerName,
} from '../../../../src/main/services/team/opencode/config/OpenCodeManagedOverlay';
describe('OpenCodeManagedOverlay', () => {
let tempDir: string;
let homePath: string;
let projectPath: string;
beforeEach(async () => {
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'opencode-managed-overlay-'));
homePath = path.join(tempDir, 'home');
projectPath = path.join(tempDir, 'project');
await fs.mkdir(projectPath, { recursive: true });
});
afterEach(async () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it('builds a minimal inline MCP overlay without user behavior keys or pure mode', async () => {
await writeJson(path.join(projectPath, 'opencode.json'), {
plugin: ['user-plugin'],
mcp: { user_server: { type: 'local', command: 'custom', enabled: true } },
});
const builder = new OpenCodeManagedOverlayBuilder(
new OpenCodeBehaviorSourceScanner({ homePath }),
() => new Date('2026-04-21T12:00:00.000Z')
);
const overlay = await builder.build({
projectPath,
preferredMcpName: 'agent-teams',
appMcpCommand: 'node',
appMcpArgs: ['server.js'],
appMcpEnv: { TEAM_RUNTIME: '1' },
});
const config = JSON.parse(overlay.env.OPENCODE_CONFIG_CONTENT);
expect(config).toEqual({
mcp: {
'agent-teams': {
type: 'local',
command: 'node',
args: ['server.js'],
enabled: true,
environment: { TEAM_RUNTIME: '1' },
timeout: 10_000,
},
},
});
expect(overlay.env).not.toHaveProperty('OPENCODE_PURE');
expect(overlay.env).not.toHaveProperty('OPENCODE_DISABLE_PROJECT_CONFIG');
expect(overlay.env.OPENCODE_DISABLE_AUTOUPDATE).toBe('1');
expect(config).not.toHaveProperty('plugin');
expect(config).not.toHaveProperty('model');
expect(overlay.diagnostics).toContain(
'OpenCode managed overlay checked at 2026-04-21T12:00:00.000Z'
);
});
it('allows app-managed OpenCode auto-update only behind an explicit override', async () => {
const builder = new OpenCodeManagedOverlayBuilder(
new OpenCodeBehaviorSourceScanner({ homePath })
);
const overlay = await builder.build({
projectPath,
preferredMcpName: 'agent-teams',
appMcpCommand: 'node',
appMcpArgs: ['server.js'],
appMcpEnv: {},
env: {
CLAUDE_TEAM_OPENCODE_ALLOW_AUTOUPDATE: '1',
OPENCODE_DISABLE_AUTOUPDATE: '1',
},
});
expect(overlay.env.OPENCODE_CONFIG_CONTENT).toBeTruthy();
expect(overlay.env.OPENCODE_DISABLE_AUTOUPDATE).toBeUndefined();
});
it('renames the app-owned MCP server when user config already declares the preferred name', async () => {
await writeJson(path.join(projectPath, 'opencode.json'), {
mcp: {
'agent-teams': { type: 'local', command: 'custom', enabled: true },
'agent-teams-runtime-1': { type: 'local', command: 'custom-2', enabled: true },
},
});
const builder = new OpenCodeManagedOverlayBuilder(
new OpenCodeBehaviorSourceScanner({ homePath })
);
const overlay = await builder.build({
projectPath,
preferredMcpName: 'agent-teams',
appMcpCommand: 'node',
appMcpArgs: ['server.js'],
appMcpEnv: {},
});
expect(overlay.appMcpServerName).toBe('agent-teams-runtime-2');
expect(overlay.diagnostics.join('\n')).toContain('already declares MCP server "agent-teams"');
});
it('reads JSONC project config and fingerprints plugin behavior sources', async () => {
await fs.mkdir(path.join(projectPath, '.opencode/plugins'), { recursive: true });
await fs.writeFile(
path.join(projectPath, 'opencode.jsonc'),
`{
// user-owned MCP must be observed, not overwritten
"mcp": {
"agent-teams": { "type": "local", "command": "custom", "enabled": true }
}
}`,
'utf8'
);
await fs.writeFile(
path.join(projectPath, '.opencode/plugins/example.ts'),
'export default {}',
'utf8'
);
const scanner = new OpenCodeBehaviorSourceScanner({ homePath });
await expect(scanner.readDeclaredMcpNames(projectPath)).resolves.toEqual(
new Set(['agent-teams'])
);
const sources = await scanner.scan(projectPath);
expect(sources).toContainEqual(
expect.objectContaining({
kind: 'project_plugin_dir',
exists: true,
fileCount: 1,
fingerprint: expect.stringMatching(/^[a-f0-9]{64}$/),
})
);
expect(sources).toContainEqual(
expect.objectContaining({
kind: 'project_opencode_dir',
exists: true,
fileCount: 1,
})
);
});
it('rejects managed overlays that would shadow user behavior keys', () => {
expect(() =>
assertManagedOverlayDoesNotShadowUserConfig({
...buildManagedOverlayConfig({
serverName: 'agent-teams',
command: 'node',
args: [],
environment: {},
timeout: 10_000,
}),
plugin: ['managed-plugin'],
model: 'managed-model',
})
).toThrow('Managed OpenCode overlay must not set user behavior keys: plugin, model');
});
it('picks deterministic collision-safe app MCP names', () => {
expect(pickAppOwnedMcpServerName('agent-teams', new Set())).toBe('agent-teams');
expect(pickAppOwnedMcpServerName('agent-teams', new Set(['agent-teams']))).toBe(
'agent-teams-runtime-1'
);
expect(
pickAppOwnedMcpServerName(
'agent-teams',
new Set(['agent-teams', 'agent-teams-runtime-1', 'agent-teams-runtime-2'])
)
).toBe('agent-teams-runtime-3');
});
});
async function writeJson(filePath: string, value: unknown): Promise<void> {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
}