This commit is contained in:
118
packages/server/tests/unit/command-interpret.test.ts
Normal file
118
packages/server/tests/unit/command-interpret.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { interpretCommand, type InterpretDeps } from '../../src/local/command-interpret.js';
|
||||
|
||||
function deps(over: Partial<InterpretDeps> & { llm: InterpretDeps['llm'] }): InterpretDeps {
|
||||
return {
|
||||
text: 'do the thing',
|
||||
workspaceId: 'w1',
|
||||
currentTier: 'FREE',
|
||||
workspaces: [
|
||||
{ id: 'w1', name: 'Acme Redesign' },
|
||||
{ id: 'w2', name: 'Personal Notes' },
|
||||
],
|
||||
memoryContext: 'Active: redesigning the Acme landing page.',
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
const llmReturning = (s: string) => async () => s;
|
||||
|
||||
describe('interpretCommand — Tier 1 resolver', () => {
|
||||
it('resolves a create-workspace intent to a single action with the name filled', async () => {
|
||||
const res = await interpretCommand(deps({
|
||||
text: 'make a new workspace for the Phoenix project',
|
||||
llm: llmReturning('{"kind":"action","actionId":"create_workspace","params":{"name":"Phoenix"}}'),
|
||||
}));
|
||||
expect(res.kind).toBe('action');
|
||||
expect(res.action?.id).toBe('create_workspace');
|
||||
expect(res.action?.sideEffect).toBe(true);
|
||||
expect(res.action?.endpoint).toEqual({ method: 'POST', path: '/api/workspaces', body: { name: 'Phoenix', group: 'Personal' } });
|
||||
});
|
||||
|
||||
it('resolves a memory-dependent "continue" intent against the recent workspaces', async () => {
|
||||
const res = await interpretCommand(deps({
|
||||
text: 'continue what I was working on',
|
||||
llm: llmReturning('{"kind":"action","actionId":"open_workspace","params":{"workspaceId":"w1"}}'),
|
||||
}));
|
||||
expect(res.kind).toBe('action');
|
||||
expect(res.action?.navigate).toEqual({ type: 'workspace', id: 'workspace:w1' });
|
||||
});
|
||||
|
||||
it('resolves install_mcp to an action on FREE — MCP install is free/Solo (never tier_gated)', async () => {
|
||||
const res = await interpretCommand(deps({
|
||||
currentTier: 'FREE',
|
||||
text: 'install the postgres mcp server',
|
||||
llm: llmReturning('{"kind":"action","actionId":"install_mcp","params":{"mcpId":"postgres"}}'),
|
||||
}));
|
||||
expect(res.kind).toBe('action');
|
||||
expect(res.action?.id).toBe('install_mcp');
|
||||
});
|
||||
|
||||
it('passes the action through on a paid tier as well', async () => {
|
||||
const res = await interpretCommand(deps({
|
||||
currentTier: 'TEAMS',
|
||||
llm: llmReturning('{"kind":"action","actionId":"install_mcp","params":{"mcpId":"postgres"}}'),
|
||||
}));
|
||||
expect(res.kind).toBe('action');
|
||||
expect(res.action?.id).toBe('install_mcp');
|
||||
});
|
||||
|
||||
it('returns none (never an invented action) for an out-of-registry id', async () => {
|
||||
const res = await interpretCommand(deps({
|
||||
llm: llmReturning('{"kind":"action","actionId":"delete_production_db","params":{}}'),
|
||||
}));
|
||||
expect(res.kind).toBe('none');
|
||||
expect(res.fallback).toBe(true);
|
||||
});
|
||||
|
||||
it('passes clarify through', async () => {
|
||||
const res = await interpretCommand(deps({
|
||||
llm: llmReturning('{"kind":"clarify","question":"Which workspace?","options":["Acme","Personal"]}'),
|
||||
}));
|
||||
expect(res.kind).toBe('clarify');
|
||||
expect(res.question).toBe('Which workspace?');
|
||||
expect(res.options).toEqual(['Acme', 'Personal']);
|
||||
});
|
||||
|
||||
it('collapses a single-step plan to one action', async () => {
|
||||
const res = await interpretCommand(deps({
|
||||
llm: llmReturning('{"kind":"plan","steps":[{"actionId":"open_app","params":{"app":"memory"}}]}'),
|
||||
}));
|
||||
expect(res.kind).toBe('action');
|
||||
expect(res.action?.navigate).toEqual({ type: 'command', id: 'command:memory' });
|
||||
});
|
||||
|
||||
it('downgrades a multi-step plan to clarify (v1 does not execute plans)', async () => {
|
||||
const res = await interpretCommand(deps({
|
||||
llm: llmReturning('{"kind":"plan","steps":[{"actionId":"create_workspace","params":{"name":"A"}},{"actionId":"open_app","params":{"app":"memory"}}]}'),
|
||||
}));
|
||||
expect(res.kind).toBe('clarify');
|
||||
expect(res.steps?.length).toBe(2);
|
||||
});
|
||||
|
||||
it('strips code fences before parsing', async () => {
|
||||
const res = await interpretCommand(deps({
|
||||
llm: llmReturning('```json\n{"kind":"action","actionId":"open_app","params":{"app":"settings"}}\n```'),
|
||||
}));
|
||||
expect(res.kind).toBe('action');
|
||||
expect(res.action?.navigate?.id).toBe('command:settings');
|
||||
});
|
||||
|
||||
it('degrades to a Tier-0 fallback on unparseable model output', async () => {
|
||||
const res = await interpretCommand(deps({ llm: llmReturning('I think you want to open memory!') }));
|
||||
expect(res.kind).toBe('none');
|
||||
expect(res.fallback).toBe(true);
|
||||
});
|
||||
|
||||
it('degrades to a Tier-0 fallback when the model is unavailable (no key → null)', async () => {
|
||||
const res = await interpretCommand(deps({ llm: async () => null }));
|
||||
expect(res.kind).toBe('none');
|
||||
expect(res.fallback).toBe(true);
|
||||
});
|
||||
|
||||
it('degrades to a Tier-0 fallback when the llm throws', async () => {
|
||||
const res = await interpretCommand(deps({ llm: async () => { throw new Error('proxy down'); } }));
|
||||
expect(res.kind).toBe('none');
|
||||
expect(res.fallback).toBe(true);
|
||||
});
|
||||
});
|
||||
112
packages/server/tests/unit/command-registry.test.ts
Normal file
112
packages/server/tests/unit/command-registry.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
ACTION_IDS,
|
||||
validateAndBuildAction,
|
||||
checkTier,
|
||||
buildActionCatalog,
|
||||
type RegistryContext,
|
||||
} from '../../src/local/command-registry.js';
|
||||
|
||||
const ctx: RegistryContext = {
|
||||
workspaceId: 'w1',
|
||||
workspaces: [
|
||||
{ id: 'w1', name: 'Acme Redesign' },
|
||||
{ id: 'w2', name: 'Personal Notes' },
|
||||
],
|
||||
};
|
||||
|
||||
describe('command-registry — closed action registry', () => {
|
||||
it('exposes only the v1 action ids', () => {
|
||||
expect(ACTION_IDS).toEqual(['open_app', 'open_workspace', 'create_workspace', 'install_mcp']);
|
||||
});
|
||||
|
||||
it('rejects an unknown action id (closed registry)', () => {
|
||||
expect(validateAndBuildAction('rm_rf_slash', {}, ctx)).toBeNull();
|
||||
expect(validateAndBuildAction('exec_shell', { cmd: 'whoami' }, ctx)).toBeNull();
|
||||
});
|
||||
|
||||
describe('open_app (nav, read)', () => {
|
||||
it('builds a navigate target for a valid app id', () => {
|
||||
const a = validateAndBuildAction('open_app', { app: 'memory' }, ctx);
|
||||
expect(a).toMatchObject({
|
||||
id: 'open_app',
|
||||
sideEffect: false,
|
||||
riskLevel: 'low',
|
||||
navigate: { type: 'command', id: 'command:memory' },
|
||||
});
|
||||
expect(a?.endpoint).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects an app id outside NAV_TARGETS (enum guard)', () => {
|
||||
expect(validateAndBuildAction('open_app', { app: 'admin-secrets' }, ctx)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('open_workspace (nav, read)', () => {
|
||||
it('resolves by id', () => {
|
||||
const a = validateAndBuildAction('open_workspace', { workspaceId: 'w2' }, ctx);
|
||||
expect(a?.navigate).toEqual({ type: 'workspace', id: 'workspace:w2' });
|
||||
});
|
||||
|
||||
it('resolves by fuzzy name when id is unknown', () => {
|
||||
const a = validateAndBuildAction('open_workspace', { name: 'acme' }, ctx);
|
||||
expect(a?.navigate).toEqual({ type: 'workspace', id: 'workspace:w1' });
|
||||
});
|
||||
|
||||
it('returns null when the workspace cannot be resolved', () => {
|
||||
expect(validateAndBuildAction('open_workspace', { name: 'Nonexistent' }, ctx)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('create_workspace (side-effect, low)', () => {
|
||||
it('builds a POST /api/workspaces endpoint with the name filled', () => {
|
||||
const a = validateAndBuildAction('create_workspace', { name: 'X Project' }, ctx);
|
||||
expect(a).toMatchObject({
|
||||
id: 'create_workspace',
|
||||
sideEffect: true,
|
||||
riskLevel: 'low',
|
||||
endpoint: { method: 'POST', path: '/api/workspaces', body: { name: 'X Project', group: 'Personal' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null without a name', () => {
|
||||
expect(validateAndBuildAction('create_workspace', {}, ctx)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('install_mcp (side-effect, medium — free/Solo)', () => {
|
||||
it('builds a POST /api/mcps/install endpoint for a catalog id', () => {
|
||||
const a = validateAndBuildAction('install_mcp', { mcpId: 'postgres' }, ctx);
|
||||
expect(a).toMatchObject({
|
||||
id: 'install_mcp',
|
||||
sideEffect: true,
|
||||
riskLevel: 'medium',
|
||||
endpoint: { method: 'POST', path: '/api/mcps/install', body: { mcpId: 'postgres' } },
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects an mcp id outside the surfaced popular set (enum guard)', () => {
|
||||
expect(validateAndBuildAction('install_mcp', { mcpId: 'totally-made-up' }, ctx)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkTier', () => {
|
||||
it('never gates install_mcp — MCP install is free/Solo (PRO removed)', () => {
|
||||
expect(checkTier('install_mcp', 'FREE')).toEqual({ gated: false });
|
||||
expect(checkTier('install_mcp', 'TEAMS')).toEqual({ gated: false });
|
||||
});
|
||||
it('never gates a free/read action', () => {
|
||||
expect(checkTier('open_app', 'FREE')).toEqual({ gated: false });
|
||||
expect(checkTier('create_workspace', 'FREE')).toEqual({ gated: false });
|
||||
});
|
||||
});
|
||||
|
||||
it('renders a prompt catalog naming every action + nav/mcp targets', () => {
|
||||
const catalog = buildActionCatalog();
|
||||
for (const id of ACTION_IDS) expect(catalog).toContain(id);
|
||||
expect(catalog).toContain('NAV TARGETS');
|
||||
expect(catalog).toContain('memory');
|
||||
expect(catalog).toContain('POPULAR MCP SERVERS');
|
||||
expect(catalog).toContain('postgres');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user