|
|
@@ -1,959 +0,0 @@
|
|
|
-import { describe, expect, mock, test } from 'bun:test';
|
|
|
-import type { PluginConfig } from '../config';
|
|
|
-import { CouncilConfigSchema } from '../config/council-schema';
|
|
|
-import { CouncilManager } from './council-manager';
|
|
|
-
|
|
|
-function createMockContext(overrides?: {
|
|
|
- sessionCreateResult?:
|
|
|
- | (() => { data?: { id?: string } })
|
|
|
- | {
|
|
|
- data?: { id?: string };
|
|
|
- };
|
|
|
- sessionMessagesResult?: {
|
|
|
- data?: Array<{
|
|
|
- info?: { role: string };
|
|
|
- parts?: Array<{ type: string; text?: string }>;
|
|
|
- }>;
|
|
|
- };
|
|
|
- promptImpl?: (args: unknown) => Promise<unknown>;
|
|
|
-}) {
|
|
|
- let callCount = 0;
|
|
|
- return {
|
|
|
- client: {
|
|
|
- session: {
|
|
|
- create: mock(async () => {
|
|
|
- callCount++;
|
|
|
- const overrideResult = overrides?.sessionCreateResult;
|
|
|
- if (typeof overrideResult === 'function') {
|
|
|
- return overrideResult();
|
|
|
- }
|
|
|
- return (
|
|
|
- overrideResult ?? {
|
|
|
- data: { id: `test-session-${callCount}` },
|
|
|
- }
|
|
|
- );
|
|
|
- }),
|
|
|
- messages: mock(
|
|
|
- async () => overrides?.sessionMessagesResult ?? { data: [] },
|
|
|
- ),
|
|
|
- prompt: mock(async (args: unknown) => {
|
|
|
- if (overrides?.promptImpl) {
|
|
|
- return await overrides.promptImpl(args);
|
|
|
- }
|
|
|
- return {};
|
|
|
- }),
|
|
|
- abort: mock(async () => ({})),
|
|
|
- },
|
|
|
- },
|
|
|
- directory: '/tmp/test',
|
|
|
- } as any;
|
|
|
-}
|
|
|
-
|
|
|
-function createTestCouncilConfig(overrides?: {
|
|
|
- presets?: Record<string, Record<string, { model: string; variant?: string }>>;
|
|
|
- default_preset?: string;
|
|
|
- timeout?: number;
|
|
|
-}): PluginConfig {
|
|
|
- const councilConfig = CouncilConfigSchema.parse({
|
|
|
- presets: overrides?.presets ?? {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- beta: { model: 'openai/gpt-5.3-codex' },
|
|
|
- },
|
|
|
- },
|
|
|
- default_preset: overrides?.default_preset,
|
|
|
- timeout: overrides?.timeout,
|
|
|
- });
|
|
|
-
|
|
|
- return { council: councilConfig } as any;
|
|
|
-}
|
|
|
-
|
|
|
-describe('CouncilManager', () => {
|
|
|
- describe('constructor', () => {
|
|
|
- test('creates manager without config', () => {
|
|
|
- const ctx = createMockContext();
|
|
|
- const manager = new CouncilManager(ctx, undefined);
|
|
|
- expect(manager).toBeDefined();
|
|
|
- });
|
|
|
-
|
|
|
- test('creates manager with plugin config', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Councillor response' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config = createTestCouncilConfig();
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-session-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(true);
|
|
|
- expect(result.result).toBeDefined();
|
|
|
- expect(result.councillorResults).toHaveLength(2);
|
|
|
-
|
|
|
- // Check all councillors completed
|
|
|
- expect(
|
|
|
- result.councillorResults.every((r) => r.status === 'completed'),
|
|
|
- ).toBe(true);
|
|
|
- });
|
|
|
-
|
|
|
- test('returns error when all councillors fail', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionCreateResult: () => ({ data: {} }), // Missing ID triggers failure
|
|
|
- });
|
|
|
- const config = createTestCouncilConfig();
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-session-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(false);
|
|
|
- expect(result.error).toBe('All councillors failed or timed out');
|
|
|
- expect(result.councillorResults).toHaveLength(2);
|
|
|
- expect(result.councillorResults.every((r) => r.status === 'failed')).toBe(
|
|
|
- true,
|
|
|
- );
|
|
|
- });
|
|
|
-
|
|
|
- test('uses default_preset when presetName is undefined', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Councillor response' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config = createTestCouncilConfig({
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- },
|
|
|
- custom: {
|
|
|
- beta: { model: 'openai/gpt-5.3-codex' },
|
|
|
- },
|
|
|
- },
|
|
|
- default_preset: 'custom',
|
|
|
- });
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-session-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(true);
|
|
|
- expect(result.councillorResults).toHaveLength(1);
|
|
|
- expect(result.councillorResults[0].name).toBe('beta');
|
|
|
- });
|
|
|
-
|
|
|
- test('handles mixed councillor success/failure', async () => {
|
|
|
- let createCallCount = 0;
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionCreateResult: () => {
|
|
|
- createCallCount++;
|
|
|
- // First councillor succeeds, second fails
|
|
|
- if (createCallCount === 1) {
|
|
|
- return { data: { id: 'councillor-success' } };
|
|
|
- }
|
|
|
- if (createCallCount === 2) {
|
|
|
- return { data: {} }; // Missing ID = failure
|
|
|
- }
|
|
|
- return { data: { id: `session-${createCallCount}` } };
|
|
|
- },
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Successful response' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- councillor1: { model: 'openai/gpt-5.6-luna' },
|
|
|
- councillor2: { model: 'openai/gpt-5.3-codex' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-session-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(true);
|
|
|
- expect(result.councillorResults).toHaveLength(2);
|
|
|
-
|
|
|
- // Check that one completed and one failed (order not guaranteed)
|
|
|
- const completedCount = result.councillorResults.filter(
|
|
|
- (r) => r.status === 'completed',
|
|
|
- ).length;
|
|
|
- const failedCount = result.councillorResults.filter(
|
|
|
- (r) => r.status === 'failed',
|
|
|
- ).length;
|
|
|
-
|
|
|
- expect(completedCount).toBe(1);
|
|
|
- expect(failedCount).toBe(1);
|
|
|
- });
|
|
|
-
|
|
|
- test('uses custom timeouts from config', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Response' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- },
|
|
|
- custom: {
|
|
|
- beta: { model: 'openai/gpt-5.3-codex' },
|
|
|
- },
|
|
|
- },
|
|
|
- default_preset: 'custom',
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-session-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(true);
|
|
|
- });
|
|
|
-
|
|
|
- test('handles councillor timeout', async () => {
|
|
|
- let sessionCount = 0;
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionCreateResult: () => {
|
|
|
- sessionCount++;
|
|
|
- return { data: { id: `session-${sessionCount}` } };
|
|
|
- },
|
|
|
- promptImpl: async (args: any) => {
|
|
|
- // First councillor times out, second succeeds
|
|
|
- const sessionId = args.path?.id;
|
|
|
- if (sessionId === 'session-1') {
|
|
|
- // Simulate timeout
|
|
|
- throw new Error('Prompt timed out after 180000ms');
|
|
|
- }
|
|
|
- return {};
|
|
|
- },
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Success' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- timeout: { model: 'openai/gpt-5.6-luna' },
|
|
|
- success: { model: 'openai/gpt-5.3-codex' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-session-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(true);
|
|
|
- expect(result.councillorResults).toHaveLength(2);
|
|
|
-
|
|
|
- const timeoutResult = result.councillorResults.find(
|
|
|
- (r) => r.name === 'timeout',
|
|
|
- );
|
|
|
- const successResult = result.councillorResults.find(
|
|
|
- (r) => r.name === 'success',
|
|
|
- );
|
|
|
-
|
|
|
- expect(timeoutResult?.status).toBe('timed_out');
|
|
|
- expect(timeoutResult?.error).toContain('timed out');
|
|
|
- expect(successResult?.status).toBe('completed');
|
|
|
- });
|
|
|
-
|
|
|
- test('passes variant to councillor sessions', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Response' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna', variant: 'low' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- await manager.runCouncil('test prompt', undefined, 'parent-session-id');
|
|
|
-
|
|
|
- const promptCalls = ctx.client.session.prompt.mock.calls as Array<
|
|
|
- [{ body?: { variant?: string; agent?: string } }]
|
|
|
- >;
|
|
|
- // Find the councillor call by agent field (notification may be at [0])
|
|
|
- const councillorCall = promptCalls.find(
|
|
|
- (c) => c[0].body?.agent === 'councillor',
|
|
|
- );
|
|
|
- expect(councillorCall).toBeDefined();
|
|
|
- expect(councillorCall?.[0].body?.variant).toBe('low');
|
|
|
- });
|
|
|
-
|
|
|
- test('always aborts councillor sessions after completion', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Response' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- beta: { model: 'openai/gpt-5.3-codex' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- await manager.runCouncil('test prompt', undefined, 'parent-session-id');
|
|
|
-
|
|
|
- // Should abort 2 councillors
|
|
|
- expect(ctx.client.session.abort).toHaveBeenCalledTimes(2);
|
|
|
- });
|
|
|
-
|
|
|
- test('handles councillor with invalid model format', async () => {
|
|
|
- const ctx = createMockContext();
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- badmodel: { model: 'invalid-model-no-slash' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-session-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(false);
|
|
|
- expect(result.error).toBe('All councillors failed or timed out');
|
|
|
- expect(result.councillorResults).toHaveLength(1);
|
|
|
- expect(result.councillorResults[0].status).toBe('failed');
|
|
|
- expect(result.councillorResults[0].error).toContain(
|
|
|
- 'Invalid model format',
|
|
|
- );
|
|
|
- });
|
|
|
-
|
|
|
- test('extracts text and reasoning content from councillor responses', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [
|
|
|
- { type: 'reasoning', text: 'I am thinking...' },
|
|
|
- { type: 'text', text: 'Final answer.' },
|
|
|
- ],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-session-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(true);
|
|
|
- // Councillors filter out reasoning parts to avoid bloating the synthesis
|
|
|
- expect(result.councillorResults[0].result).not.toContain(
|
|
|
- 'I am thinking...',
|
|
|
- );
|
|
|
- expect(result.councillorResults[0].result).toContain('Final answer.');
|
|
|
- });
|
|
|
-
|
|
|
- test('handles concurrent council sessions with different presets', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionCreateResult: () => ({ data: { id: 'session-1' } }),
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Response' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const defaultConfig = createTestCouncilConfig({
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- },
|
|
|
- fast: {
|
|
|
- beta: { model: 'openai/gpt-5.3-codex' },
|
|
|
- },
|
|
|
- },
|
|
|
- });
|
|
|
- const manager1 = new CouncilManager(ctx, defaultConfig, undefined);
|
|
|
- const manager2 = new CouncilManager(ctx, defaultConfig, undefined);
|
|
|
-
|
|
|
- const [result1, result2] = await Promise.all([
|
|
|
- manager1.runCouncil('test prompt 1', 'default', 'parent-1'),
|
|
|
- manager2.runCouncil('test prompt 2', 'fast', 'parent-2'),
|
|
|
- ]);
|
|
|
-
|
|
|
- expect(result1.success).toBe(true);
|
|
|
- expect(result2.success).toBe(true);
|
|
|
- expect(result1.councillorResults[0].name).toBe('alpha');
|
|
|
- expect(result2.councillorResults[0].name).toBe('beta');
|
|
|
- });
|
|
|
-
|
|
|
- test('handles empty preset gracefully', async () => {
|
|
|
- const ctx = createMockContext();
|
|
|
- const config = createTestCouncilConfig({
|
|
|
- presets: {
|
|
|
- empty: {},
|
|
|
- },
|
|
|
- });
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- 'empty',
|
|
|
- 'parent-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(false);
|
|
|
- expect(result.error).toContain(
|
|
|
- 'Preset "empty" has no councillors configured',
|
|
|
- );
|
|
|
- expect(result.councillorResults).toHaveLength(0);
|
|
|
- });
|
|
|
-
|
|
|
- test('returns available presets when invalid preset name given', async () => {
|
|
|
- const ctx = createMockContext();
|
|
|
- const config = createTestCouncilConfig({
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- },
|
|
|
- roled: {
|
|
|
- beta: { model: 'openai/gpt-5.3-codex' },
|
|
|
- },
|
|
|
- },
|
|
|
- });
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- 'architect',
|
|
|
- 'parent-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(false);
|
|
|
- expect(result.error).toContain('Preset "architect" does not exist');
|
|
|
- expect(result.error).toContain('Omit the preset parameter');
|
|
|
- expect(result.error).toContain('default, roled');
|
|
|
- expect(result.councillorResults).toHaveLength(0);
|
|
|
- });
|
|
|
-
|
|
|
- test('passes agent field in councillor prompt body', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Response' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config = createTestCouncilConfig();
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- await manager.runCouncil('test prompt', undefined, 'parent-id');
|
|
|
-
|
|
|
- const promptCalls = ctx.client.session.prompt.mock.calls as Array<
|
|
|
- [{ body?: { agent?: string } }]
|
|
|
- >;
|
|
|
- // Find councillor call by agent (notification may interleave)
|
|
|
- const councillorCall = promptCalls.find(
|
|
|
- (c) => c[0].body?.agent === 'councillor',
|
|
|
- );
|
|
|
- expect(councillorCall).toBeDefined();
|
|
|
- });
|
|
|
-
|
|
|
- test('disables mutating and delegation tools in councillor prompt body', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Response' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config = createTestCouncilConfig();
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- await manager.runCouncil('test prompt', undefined, 'parent-id');
|
|
|
-
|
|
|
- const promptCalls = ctx.client.session.prompt.mock.calls as Array<
|
|
|
- [{ body?: { agent?: string; tools?: Record<string, boolean> } }]
|
|
|
- >;
|
|
|
- const councillorCall = promptCalls.find(
|
|
|
- (c) => c[0].body?.agent === 'councillor',
|
|
|
- );
|
|
|
- expect(councillorCall?.[0].body?.tools).toEqual({
|
|
|
- task: false,
|
|
|
- question: false,
|
|
|
- edit: false,
|
|
|
- write: false,
|
|
|
- apply_patch: false,
|
|
|
- ast_grep_replace: false,
|
|
|
- bash: false,
|
|
|
- });
|
|
|
- });
|
|
|
-
|
|
|
- test('creates session with model label in title', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Response' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- await manager.runCouncil('test prompt', undefined, 'parent-id');
|
|
|
-
|
|
|
- const createCalls = ctx.client.session.create.mock.calls as Array<
|
|
|
- [{ body?: { title?: string } }]
|
|
|
- >;
|
|
|
- // Councillor title: "Council alpha (gpt-5.6-luna)"
|
|
|
- expect(createCalls[0][0].body?.title).toBe(
|
|
|
- 'Council alpha (gpt-5.6-luna)',
|
|
|
- );
|
|
|
- });
|
|
|
-
|
|
|
- test('passes councillor prompt to councillor session', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Response with role guidance' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: {
|
|
|
- model: 'openai/gpt-5.6-luna',
|
|
|
- prompt: 'You are a meticulous reviewer focused on edge cases.',
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- await manager.runCouncil('test prompt', undefined, 'parent-id');
|
|
|
-
|
|
|
- const promptCalls = ctx.client.session.prompt.mock.calls as Array<
|
|
|
- [
|
|
|
- {
|
|
|
- body?: {
|
|
|
- parts?: Array<{ type: string; text?: string }>;
|
|
|
- agent?: string;
|
|
|
- };
|
|
|
- },
|
|
|
- ]
|
|
|
- >;
|
|
|
- const councillorCall = promptCalls.find(
|
|
|
- (c) => c[0].body?.agent === 'councillor',
|
|
|
- );
|
|
|
- expect(councillorCall).toBeDefined();
|
|
|
- const promptText = councillorCall?.[0]?.body?.parts?.[0]?.text;
|
|
|
- expect(promptText).toContain('test prompt');
|
|
|
- expect(promptText).toContain(
|
|
|
- 'You are a meticulous reviewer focused on edge cases.',
|
|
|
- );
|
|
|
- });
|
|
|
-
|
|
|
- test('works without any prompt overrides (backward compatible)', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Response' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(true);
|
|
|
- // Verify no prompt contamination - councillor gets raw prompt
|
|
|
- const promptCalls = ctx.client.session.prompt.mock.calls as Array<
|
|
|
- [
|
|
|
- {
|
|
|
- body?: {
|
|
|
- parts?: Array<{ type: string; text?: string }>;
|
|
|
- agent?: string;
|
|
|
- };
|
|
|
- },
|
|
|
- ]
|
|
|
- >;
|
|
|
- const councillorCall = promptCalls.find(
|
|
|
- (c) => c[0].body?.agent === 'councillor',
|
|
|
- );
|
|
|
- // Without prompt override, councillor gets just the raw user prompt
|
|
|
- expect(councillorCall?.[0]?.body?.parts?.[0]?.text).toBe('test prompt');
|
|
|
- });
|
|
|
-
|
|
|
- test('retries councillor on empty response', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- promptImpl: async () => ({}),
|
|
|
- });
|
|
|
-
|
|
|
- // Track messages call count and return empty first, then success
|
|
|
- let councillorMessagesCallCount = 0;
|
|
|
- const originalMessages = ctx.client.session.messages;
|
|
|
- ctx.client.session.messages = mock(async (args) => {
|
|
|
- // First call (first councillor attempt): empty response
|
|
|
- // Second call (councillor retry): success
|
|
|
- councillorMessagesCallCount++;
|
|
|
- if (councillorMessagesCallCount === 1) {
|
|
|
- return {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: '' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- };
|
|
|
- }
|
|
|
- if (councillorMessagesCallCount === 2) {
|
|
|
- return {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Success' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- };
|
|
|
- }
|
|
|
- // Any other calls: use original
|
|
|
- return originalMessages(args);
|
|
|
- });
|
|
|
-
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- councillor_retries: 1,
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(true);
|
|
|
- // First two messages calls are for councillor (empty + success)
|
|
|
- expect(councillorMessagesCallCount).toBeGreaterThanOrEqual(2);
|
|
|
- expect(result.councillorResults).toHaveLength(1);
|
|
|
- expect(result.councillorResults[0].status).toBe('completed');
|
|
|
- expect(result.councillorResults[0].result).toBe('Success');
|
|
|
- });
|
|
|
-
|
|
|
- test('does not retry councillor on non-empty failure (timeout)', async () => {
|
|
|
- let messagesCallCount = 0;
|
|
|
- const ctx = createMockContext({
|
|
|
- promptImpl: async () => {
|
|
|
- // Simulate timeout error
|
|
|
- throw new Error('Prompt timed out after 180000ms');
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- // Override messages to track calls (won't be reached due to timeout)
|
|
|
- ctx.client.session.messages = mock(async () => {
|
|
|
- messagesCallCount++;
|
|
|
- return {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Success' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- };
|
|
|
- });
|
|
|
-
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- councillor_retries: 2,
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(false);
|
|
|
- // No retry on timeout - messages should not be called
|
|
|
- expect(messagesCallCount).toBe(0);
|
|
|
- expect(result.councillorResults).toHaveLength(1);
|
|
|
- expect(result.councillorResults[0].status).toBe('timed_out');
|
|
|
- expect(result.councillorResults[0].error).toContain('timed out');
|
|
|
- });
|
|
|
-
|
|
|
- test('falls back to next model in councillor chain on failure', async () => {
|
|
|
- let sessionCount = 0;
|
|
|
- const ctx = createMockContext({
|
|
|
- sessionCreateResult: () => {
|
|
|
- sessionCount++;
|
|
|
- return { data: { id: `session-${sessionCount}` } };
|
|
|
- },
|
|
|
- promptImpl: async (args: any) => {
|
|
|
- // First model (session-1) fails; second model (session-2) succeeds.
|
|
|
- if (args.path?.id === 'session-1') {
|
|
|
- throw new Error('Prompt timed out after 180000ms');
|
|
|
- }
|
|
|
- return {};
|
|
|
- },
|
|
|
- sessionMessagesResult: {
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: 'Fallback success' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: {
|
|
|
- model: ['openai/gpt-5.6-luna', 'openai/gpt-5.3-codex'],
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(true);
|
|
|
- expect(result.councillorResults).toHaveLength(1);
|
|
|
- expect(result.councillorResults[0].status).toBe('completed');
|
|
|
- expect(result.councillorResults[0].result).toBe('Fallback success');
|
|
|
- // Reported model reflects the fallback that actually responded.
|
|
|
- expect(result.councillorResults[0].model).toBe('openai/gpt-5.3-codex');
|
|
|
- });
|
|
|
-
|
|
|
- test('exhausts councillor retries and returns failure', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- promptImpl: async () => ({}),
|
|
|
- });
|
|
|
-
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- councillor_retries: 1,
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-id',
|
|
|
- );
|
|
|
-
|
|
|
- expect(result.success).toBe(false);
|
|
|
- expect(result.error).toBe('All councillors failed or timed out');
|
|
|
- expect(result.councillorResults).toHaveLength(1);
|
|
|
- expect(result.councillorResults[0].status).toBe('failed');
|
|
|
- expect(result.councillorResults[0].error).toContain(
|
|
|
- 'Empty response from provider',
|
|
|
- );
|
|
|
- });
|
|
|
-
|
|
|
- test('returns empty councillor result when retry_on_empty is false', async () => {
|
|
|
- const ctx = createMockContext({
|
|
|
- promptImpl: async () => ({}),
|
|
|
- });
|
|
|
-
|
|
|
- // Always return empty response
|
|
|
- ctx.client.session.messages = mock(async () => ({
|
|
|
- data: [
|
|
|
- {
|
|
|
- info: { role: 'assistant' },
|
|
|
- parts: [{ type: 'text', text: '' }],
|
|
|
- },
|
|
|
- ],
|
|
|
- }));
|
|
|
-
|
|
|
- const config: PluginConfig = {
|
|
|
- council: {
|
|
|
- councillor_retries: 1,
|
|
|
- presets: {
|
|
|
- default: {
|
|
|
- alpha: { model: 'openai/gpt-5.6-luna' },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- fallback: {
|
|
|
- retry_on_empty: false,
|
|
|
- },
|
|
|
- } as any;
|
|
|
- const manager = new CouncilManager(ctx, config, undefined);
|
|
|
-
|
|
|
- const result = await manager.runCouncil(
|
|
|
- 'test prompt',
|
|
|
- undefined,
|
|
|
- 'parent-id',
|
|
|
- );
|
|
|
-
|
|
|
- // With retry_on_empty: false, empty response is accepted as completed
|
|
|
- expect(result.councillorResults).toHaveLength(1);
|
|
|
- expect(result.councillorResults[0].status).toBe('completed');
|
|
|
- expect(result.councillorResults[0].result).toBe('');
|
|
|
- // Council succeeds because empty is accepted as valid response
|
|
|
- // The formatted result contains the message about all councillors failing
|
|
|
- expect(result.success).toBe(true);
|
|
|
- expect(result.result).toContain(
|
|
|
- 'All councillors failed to produce output',
|
|
|
- );
|
|
|
- expect(result.result).toContain('test prompt');
|
|
|
- });
|
|
|
- });
|
|
|
-});
|