feat: Wave 1 complete — state machine, Paseo hub, collision detection, PTY search
- Task state machine: TIMED_OUT state, retriable steps, timeout detection - Paseo hub: paseo-client.ts (HTTP+CLI), PaseoBackend (AgentBackend), 14 tests - Collision detection: collision-detector.ts, conflict-index.ts, ws-frames type - PTY search: ring buffer, search route, capture-pane fallback
This commit is contained in:
195
apps/coder/src/services/__tests__/paseo-client.test.ts
Normal file
195
apps/coder/src/services/__tests__/paseo-client.test.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { PaseoClient, PaseoClientError } from '../paseo-client.js';
|
||||
|
||||
/**
|
||||
* Create a PaseoClient whose runCli method is replaced with a mock.
|
||||
* The mock is returned as the second tuple element so tests can
|
||||
* control and inspect it directly.
|
||||
*/
|
||||
function makeClient(config?: { paseoBin?: string; cliHost?: string }): {
|
||||
client: PaseoClient;
|
||||
mockRunCli: ReturnType<typeof vi.fn>;
|
||||
} {
|
||||
const client = new PaseoClient(config);
|
||||
const mockRunCli = vi.fn();
|
||||
(client as any).runCli = mockRunCli;
|
||||
return { client, mockRunCli };
|
||||
}
|
||||
|
||||
describe('PaseoClient', () => {
|
||||
describe('listAgents', () => {
|
||||
it('returns parsed agent list from paseo ls --json', async () => {
|
||||
const agents = [
|
||||
{ id: 'abc-123', shortId: 'abc', name: 'Agent 1', provider: 'opencode', status: 'running' },
|
||||
{ id: 'def-456', shortId: 'def', name: 'Agent 2', provider: 'claude', status: 'idle' },
|
||||
];
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockResolvedValue(JSON.stringify(agents));
|
||||
|
||||
const result = await client.listAgents();
|
||||
|
||||
expect(mockRunCli).toHaveBeenCalledWith(['ls', '--json']);
|
||||
expect(result).toEqual(agents);
|
||||
});
|
||||
|
||||
it('throws PaseoClientError on non-JSON output', async () => {
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockResolvedValue('not json');
|
||||
|
||||
await expect(client.listAgents()).rejects.toThrow(PaseoClientError);
|
||||
await expect(client.listAgents()).rejects.toThrow(/invalid JSON/);
|
||||
});
|
||||
|
||||
it('propagates runCli rejection as-is', async () => {
|
||||
const { client, mockRunCli } = makeClient();
|
||||
const err = new PaseoClientError('ls failed: connection refused', 'ls', 1, 'connection refused');
|
||||
mockRunCli.mockRejectedValue(err);
|
||||
|
||||
await expect(client.listAgents()).rejects.toThrow(PaseoClientError);
|
||||
await expect(client.listAgents()).rejects.toThrow(/ls failed/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAgentStatus', () => {
|
||||
it('returns parsed agent detail from paseo inspect --json', async () => {
|
||||
const detail = {
|
||||
Id: 'abc-123', Name: 'Agent 1', Provider: 'opencode',
|
||||
Status: 'idle', Archived: false,
|
||||
CreatedAt: '2026-01-01T00:00:00Z', UpdatedAt: '2026-01-01T01:00:00Z',
|
||||
};
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockResolvedValue(JSON.stringify(detail));
|
||||
|
||||
const result = await client.getAgentStatus('abc-123');
|
||||
|
||||
expect(mockRunCli).toHaveBeenCalledWith(['inspect', '--json', 'abc-123']);
|
||||
expect(result.Id).toBe('abc-123');
|
||||
expect(result.Status).toBe('idle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('health', () => {
|
||||
it('returns ok when paseo ls succeeds', async () => {
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockResolvedValue('[]');
|
||||
|
||||
const result = await client.health();
|
||||
|
||||
expect(result).toEqual({ status: 'ok' });
|
||||
});
|
||||
|
||||
it('returns error when runCli throws', async () => {
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockRejectedValue(new Error('connection refused'));
|
||||
|
||||
const result = await client.health();
|
||||
|
||||
expect(result).toEqual({ status: 'error' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('importAgent', () => {
|
||||
it('calls paseo import with provider and labels', async () => {
|
||||
const agentResult = { Id: 'new-789', Name: 'Imported', Provider: 'opencode', Status: 'idle' };
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockResolvedValue(JSON.stringify(agentResult));
|
||||
|
||||
const result = await client.importAgent('ses-001', 'opencode', {
|
||||
origin: 'boocode',
|
||||
project: 'proj-1',
|
||||
});
|
||||
|
||||
expect(mockRunCli).toHaveBeenCalledWith([
|
||||
'import', '--json',
|
||||
'--provider', 'opencode',
|
||||
'--label', 'origin=boocode',
|
||||
'--label', 'project=proj-1',
|
||||
'ses-001',
|
||||
]);
|
||||
expect(result.Id).toBe('new-789');
|
||||
});
|
||||
|
||||
it('works without labels', async () => {
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockResolvedValue(JSON.stringify({ Id: 'new-789' }));
|
||||
|
||||
const result = await client.importAgent('ses-001', 'claude');
|
||||
|
||||
expect(mockRunCli).toHaveBeenCalledWith([
|
||||
'import', '--json',
|
||||
'--provider', 'claude',
|
||||
'ses-001',
|
||||
]);
|
||||
expect(result.Id).toBe('new-789');
|
||||
});
|
||||
});
|
||||
|
||||
describe('archiveAgent', () => {
|
||||
it('calls paseo archive --json', async () => {
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockResolvedValue('{}');
|
||||
|
||||
await client.archiveAgent('abc-123');
|
||||
|
||||
expect(mockRunCli).toHaveBeenCalledWith(['archive', '--json', 'abc-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendPrompt', () => {
|
||||
it('sends prompt and parses JSON result', async () => {
|
||||
const sendResult = { text: 'Hello!', ok: true };
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockResolvedValue(JSON.stringify(sendResult));
|
||||
|
||||
const result = await client.sendPrompt('abc-123', 'Hello');
|
||||
|
||||
expect(mockRunCli).toHaveBeenCalledWith(['send', '--json', 'abc-123', 'Hello'], undefined);
|
||||
expect(result).toEqual(sendResult);
|
||||
});
|
||||
|
||||
it('falls back to plain text on non-JSON output', async () => {
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockResolvedValue('plain text response');
|
||||
|
||||
const result = await client.sendPrompt('abc-123', 'Hi');
|
||||
|
||||
expect(result).toEqual({ text: 'plain text response', ok: true });
|
||||
});
|
||||
|
||||
it('supports --no-wait flag', async () => {
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockResolvedValue('{}');
|
||||
|
||||
await client.sendPrompt('abc-123', 'Hi', { noWait: true });
|
||||
|
||||
expect(mockRunCli).toHaveBeenCalledWith([
|
||||
'send', '--json', '--no-wait',
|
||||
'abc-123', 'Hi',
|
||||
], undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopAgent', () => {
|
||||
it('calls paseo stop', async () => {
|
||||
const { client, mockRunCli } = makeClient();
|
||||
mockRunCli.mockResolvedValue('');
|
||||
|
||||
await client.stopAgent('abc-123');
|
||||
|
||||
expect(mockRunCli).toHaveBeenCalledWith(['stop', 'abc-123']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cliHost config', () => {
|
||||
it('includes --host flag in args when cliHost is set', async () => {
|
||||
const { client, mockRunCli } = makeClient({ cliHost: 'tcp://localhost:6767?ssl=true' });
|
||||
mockRunCli.mockResolvedValue('[]');
|
||||
|
||||
await client.listAgents();
|
||||
|
||||
expect(mockRunCli).toHaveBeenCalledWith([
|
||||
'ls', '--json', '--host', 'tcp://localhost:6767?ssl=true',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user