Files
DeskClaw/tests/unit/agent-config.test.ts
2026-03-14 17:18:20 +08:00

224 lines
6.8 KiB
TypeScript

import { access, mkdir, readFile, rm, writeFile } from 'fs/promises';
import { join } from 'path';
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { testHome, testUserData } = vi.hoisted(() => {
const suffix = Math.random().toString(36).slice(2);
return {
testHome: `/tmp/clawx-agent-config-${suffix}`,
testUserData: `/tmp/clawx-agent-config-user-data-${suffix}`,
};
});
vi.mock('os', async () => {
const actual = await vi.importActual<typeof import('os')>('os');
const mocked = {
...actual,
homedir: () => testHome,
};
return {
...mocked,
default: mocked,
};
});
vi.mock('electron', () => ({
app: {
isPackaged: false,
getPath: () => testUserData,
getVersion: () => '0.0.0-test',
},
}));
async function writeOpenClawJson(config: unknown): Promise<void> {
const openclawDir = join(testHome, '.openclaw');
await mkdir(openclawDir, { recursive: true });
await writeFile(join(openclawDir, 'openclaw.json'), JSON.stringify(config, null, 2), 'utf8');
}
async function readOpenClawJson(): Promise<Record<string, unknown>> {
const content = await readFile(join(testHome, '.openclaw', 'openclaw.json'), 'utf8');
return JSON.parse(content) as Record<string, unknown>;
}
describe('agent config lifecycle', () => {
beforeEach(async () => {
vi.resetModules();
vi.restoreAllMocks();
await rm(testHome, { recursive: true, force: true });
await rm(testUserData, { recursive: true, force: true });
});
it('lists configured agent ids from openclaw.json', async () => {
await writeOpenClawJson({
agents: {
list: [
{ id: 'main', name: 'Main', default: true },
{ id: 'test3', name: 'test3' },
],
},
});
const { listConfiguredAgentIds } = await import('@electron/utils/agent-config');
await expect(listConfiguredAgentIds()).resolves.toEqual(['main', 'test3']);
});
it('falls back to the implicit main agent when no list exists', async () => {
await writeOpenClawJson({});
const { listConfiguredAgentIds } = await import('@electron/utils/agent-config');
await expect(listConfiguredAgentIds()).resolves.toEqual(['main']);
});
it('includes canonical per-agent main session keys in the snapshot', async () => {
await writeOpenClawJson({
session: {
mainKey: 'desk',
},
agents: {
list: [
{ id: 'main', name: 'Main', default: true },
{ id: 'research', name: 'Research' },
],
},
});
const { listAgentsSnapshot } = await import('@electron/utils/agent-config');
const snapshot = await listAgentsSnapshot();
expect(snapshot.agents).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: 'main',
mainSessionKey: 'agent:main:desk',
}),
expect.objectContaining({
id: 'research',
mainSessionKey: 'agent:research:desk',
}),
]),
);
});
it('deletes the config entry, bindings, runtime directory, and managed workspace for a removed agent', async () => {
await writeOpenClawJson({
agents: {
defaults: {
model: {
primary: 'custom-custom27/MiniMax-M2.5',
fallbacks: [],
},
},
list: [
{
id: 'main',
name: 'Main',
default: true,
workspace: '~/.openclaw/workspace',
agentDir: '~/.openclaw/agents/main/agent',
},
{
id: 'test2',
name: 'test2',
workspace: '~/.openclaw/workspace-test2',
agentDir: '~/.openclaw/agents/test2/agent',
},
{
id: 'test3',
name: 'test3',
workspace: '~/.openclaw/workspace-test3',
agentDir: '~/.openclaw/agents/test3/agent',
},
],
},
channels: {
feishu: {
enabled: true,
},
},
bindings: [
{
agentId: 'test2',
match: {
channel: 'feishu',
},
},
],
});
const test2RuntimeDir = join(testHome, '.openclaw', 'agents', 'test2');
const test2WorkspaceDir = join(testHome, '.openclaw', 'workspace-test2');
await mkdir(join(test2RuntimeDir, 'agent'), { recursive: true });
await mkdir(join(test2RuntimeDir, 'sessions'), { recursive: true });
await mkdir(join(test2WorkspaceDir, '.openclaw'), { recursive: true });
await writeFile(
join(test2RuntimeDir, 'agent', 'auth-profiles.json'),
JSON.stringify({ version: 1, profiles: {} }, null, 2),
'utf8',
);
await writeFile(join(test2WorkspaceDir, 'AGENTS.md'), '# test2', 'utf8');
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
const { deleteAgentConfig } = await import('@electron/utils/agent-config');
const { snapshot } = await deleteAgentConfig('test2');
expect(snapshot.agents.map((agent) => agent.id)).toEqual(['main', 'test3']);
expect(snapshot.channelOwners.feishu).toBe('main');
const config = await readOpenClawJson();
expect((config.agents as { list: Array<{ id: string }> }).list.map((agent) => agent.id)).toEqual([
'main',
'test3',
]);
expect(config.bindings).toEqual([]);
await expect(access(test2RuntimeDir)).rejects.toThrow();
// Workspace deletion is intentionally deferred by `deleteAgentConfig` to avoid
// ENOENT errors during Gateway restart, so it should still exist here.
await expect(access(test2WorkspaceDir)).resolves.toBeUndefined();
infoSpy.mockRestore();
});
it('preserves unmanaged custom workspaces when deleting an agent', async () => {
const customWorkspaceDir = join(testHome, 'custom-workspace-test2');
await writeOpenClawJson({
agents: {
list: [
{
id: 'main',
name: 'Main',
default: true,
workspace: '~/.openclaw/workspace',
agentDir: '~/.openclaw/agents/main/agent',
},
{
id: 'test2',
name: 'test2',
workspace: customWorkspaceDir,
agentDir: '~/.openclaw/agents/test2/agent',
},
],
},
});
await mkdir(join(testHome, '.openclaw', 'agents', 'test2', 'agent'), { recursive: true });
await mkdir(customWorkspaceDir, { recursive: true });
await writeFile(join(customWorkspaceDir, 'AGENTS.md'), '# custom', 'utf8');
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {});
const { deleteAgentConfig } = await import('@electron/utils/agent-config');
await deleteAgentConfig('test2');
await expect(access(customWorkspaceDir)).resolves.toBeUndefined();
warnSpy.mockRestore();
infoSpy.mockRestore();
});
});