- Updated all package.json name fields and dependency references - Updated all TypeScript/JavaScript imports - Updated .woodpecker/publish.yml filters and registry paths - Updated tools/install.sh scope default - Updated .npmrc registry paths (worktree + host) - Enhanced update-checker.ts with checkForAllUpdates() multi-package support - Updated CLI update command to show table of all packages - Added KNOWN_PACKAGES, formatAllPackagesTable, getInstallAllCommand - Marked checkForUpdate() with @deprecated JSDoc Closes #391
250 lines
8.5 KiB
TypeScript
250 lines
8.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { CommandExecutorService } from './command-executor.service.js';
|
|
import type { SlashCommandPayload } from '@mosaicstack/types';
|
|
|
|
// Minimal mock implementations
|
|
const mockRegistry = {
|
|
getManifest: vi.fn(() => ({
|
|
version: 1,
|
|
commands: [
|
|
{ name: 'provider', aliases: [], scope: 'agent', execution: 'hybrid', available: true },
|
|
{ name: 'mission', aliases: [], scope: 'agent', execution: 'socket', available: true },
|
|
{ name: 'agent', aliases: ['a'], scope: 'agent', execution: 'socket', available: true },
|
|
{ name: 'prdy', aliases: [], scope: 'agent', execution: 'socket', available: true },
|
|
{ name: 'tools', aliases: [], scope: 'agent', execution: 'socket', available: true },
|
|
],
|
|
skills: [],
|
|
})),
|
|
};
|
|
|
|
const mockAgentService = {
|
|
getSession: vi.fn(() => undefined),
|
|
applyAgentConfig: vi.fn(),
|
|
updateSessionModel: vi.fn(),
|
|
};
|
|
|
|
const mockSystemOverride = {
|
|
set: vi.fn(),
|
|
get: vi.fn(),
|
|
clear: vi.fn(),
|
|
renew: vi.fn(),
|
|
};
|
|
|
|
const mockSessionGC = {
|
|
sweepOrphans: vi.fn(() => ({ orphanedSessions: 0, totalCleaned: [], duration: 0 })),
|
|
};
|
|
|
|
const mockRedis = {
|
|
set: vi.fn().mockResolvedValue('OK'),
|
|
get: vi.fn(),
|
|
del: vi.fn(),
|
|
};
|
|
|
|
// Mock agent config returned by brain.agents.findByName for "my-agent-id"
|
|
const mockAgentConfig = {
|
|
id: 'my-agent-id',
|
|
name: 'my-agent-id',
|
|
model: 'claude-sonnet-4-6',
|
|
provider: 'anthropic',
|
|
systemPrompt: null,
|
|
allowedTools: null,
|
|
isSystem: false,
|
|
ownerId: 'user-123',
|
|
status: 'idle',
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
|
|
const mockBrain = {
|
|
agents: {
|
|
// findByName resolves with the agent when name matches, undefined otherwise
|
|
findByName: vi.fn((name: string) =>
|
|
Promise.resolve(name === 'my-agent-id' ? mockAgentConfig : undefined),
|
|
),
|
|
findById: vi.fn((id: string) =>
|
|
Promise.resolve(id === 'my-agent-id' ? mockAgentConfig : undefined),
|
|
),
|
|
create: vi.fn(),
|
|
},
|
|
};
|
|
|
|
const mockChatGateway = {
|
|
broadcastSessionInfo: vi.fn(),
|
|
};
|
|
|
|
function buildService(): CommandExecutorService {
|
|
return new CommandExecutorService(
|
|
mockRegistry as never,
|
|
mockAgentService as never,
|
|
mockSystemOverride as never,
|
|
mockSessionGC as never,
|
|
mockRedis as never,
|
|
mockBrain as never,
|
|
null,
|
|
mockChatGateway as never,
|
|
null,
|
|
);
|
|
}
|
|
|
|
describe('CommandExecutorService — P8-012 commands', () => {
|
|
let service: CommandExecutorService;
|
|
const userId = 'user-123';
|
|
const conversationId = 'conv-456';
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
service = buildService();
|
|
});
|
|
|
|
// /provider login — missing provider name
|
|
it('/provider login with no provider name returns usage error', async () => {
|
|
const payload: SlashCommandPayload = { command: 'provider', args: 'login', conversationId };
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toContain('Usage: /provider login');
|
|
expect(result.command).toBe('provider');
|
|
});
|
|
|
|
// /provider login anthropic — success with URL containing poll token
|
|
it('/provider login <name> returns success with URL and poll token', async () => {
|
|
const payload: SlashCommandPayload = {
|
|
command: 'provider',
|
|
args: 'login anthropic',
|
|
conversationId,
|
|
};
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(true);
|
|
expect(result.command).toBe('provider');
|
|
expect(result.message).toContain('anthropic');
|
|
expect(result.message).toContain('http');
|
|
// data should contain loginUrl and pollToken
|
|
expect(result.data).toBeDefined();
|
|
const data = result.data as Record<string, unknown>;
|
|
expect(typeof data['loginUrl']).toBe('string');
|
|
expect(typeof data['pollToken']).toBe('string');
|
|
expect(data['loginUrl'] as string).toContain('anthropic');
|
|
expect(data['loginUrl'] as string).toContain(data['pollToken'] as string);
|
|
// Verify Valkey was called
|
|
expect(mockRedis.set).toHaveBeenCalledOnce();
|
|
const [key, value, , ttl] = mockRedis.set.mock.calls[0] as [string, string, string, number];
|
|
expect(key).toContain('mosaic:auth:poll:');
|
|
const stored = JSON.parse(value) as { status: string; provider: string; userId: string };
|
|
expect(stored.status).toBe('pending');
|
|
expect(stored.provider).toBe('anthropic');
|
|
expect(stored.userId).toBe(userId);
|
|
expect(ttl).toBe(300);
|
|
});
|
|
|
|
// /provider with no args — returns usage
|
|
it('/provider with no args returns usage message', async () => {
|
|
const payload: SlashCommandPayload = { command: 'provider', conversationId };
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toContain('Usage: /provider');
|
|
});
|
|
|
|
// /provider list
|
|
it('/provider list returns success', async () => {
|
|
const payload: SlashCommandPayload = { command: 'provider', args: 'list', conversationId };
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(true);
|
|
expect(result.command).toBe('provider');
|
|
});
|
|
|
|
// /provider logout with no name — usage error
|
|
it('/provider logout with no name returns error', async () => {
|
|
const payload: SlashCommandPayload = { command: 'provider', args: 'logout', conversationId };
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toContain('Usage: /provider logout');
|
|
});
|
|
|
|
// /provider unknown subcommand
|
|
it('/provider unknown subcommand returns error', async () => {
|
|
const payload: SlashCommandPayload = {
|
|
command: 'provider',
|
|
args: 'unknown',
|
|
conversationId,
|
|
};
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(false);
|
|
expect(result.message).toContain('Unknown subcommand');
|
|
});
|
|
|
|
// /mission status
|
|
it('/mission status returns stub message', async () => {
|
|
const payload: SlashCommandPayload = { command: 'mission', args: 'status', conversationId };
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(true);
|
|
expect(result.command).toBe('mission');
|
|
expect(result.message).toContain('Mission status');
|
|
});
|
|
|
|
// /mission with no args
|
|
it('/mission with no args returns status stub', async () => {
|
|
const payload: SlashCommandPayload = { command: 'mission', conversationId };
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toContain('Mission status');
|
|
});
|
|
|
|
// /mission set <id>
|
|
it('/mission set <id> returns confirmation', async () => {
|
|
const payload: SlashCommandPayload = {
|
|
command: 'mission',
|
|
args: 'set my-mission-123',
|
|
conversationId,
|
|
};
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toContain('my-mission-123');
|
|
});
|
|
|
|
// /agent list
|
|
it('/agent list returns stub message', async () => {
|
|
const payload: SlashCommandPayload = { command: 'agent', args: 'list', conversationId };
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(true);
|
|
expect(result.command).toBe('agent');
|
|
expect(result.message).toContain('agent');
|
|
});
|
|
|
|
// /agent with no args
|
|
it('/agent with no args returns usage', async () => {
|
|
const payload: SlashCommandPayload = { command: 'agent', conversationId };
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toContain('Usage: /agent');
|
|
});
|
|
|
|
// /agent <id> — switch
|
|
it('/agent <id> returns switch confirmation', async () => {
|
|
const payload: SlashCommandPayload = {
|
|
command: 'agent',
|
|
args: 'my-agent-id',
|
|
conversationId,
|
|
};
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(true);
|
|
expect(result.message).toContain('my-agent-id');
|
|
});
|
|
|
|
// /prdy
|
|
it('/prdy returns PRD wizard message', async () => {
|
|
const payload: SlashCommandPayload = { command: 'prdy', conversationId };
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(true);
|
|
expect(result.command).toBe('prdy');
|
|
expect(result.message).toContain('mosaic prdy');
|
|
});
|
|
|
|
// /tools
|
|
it('/tools returns tools stub message', async () => {
|
|
const payload: SlashCommandPayload = { command: 'tools', conversationId };
|
|
const result = await service.execute(payload, userId);
|
|
expect(result.success).toBe(true);
|
|
expect(result.command).toBe('tools');
|
|
expect(result.message).toContain('tools');
|
|
});
|
|
});
|