mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-11 05:41:08 -07:00
746 lines
22 KiB
TypeScript
746 lines
22 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { SchedulerStateManager } from './state-manager.js';
|
|
import {
|
|
CoreToolCallStatus,
|
|
ROOT_SCHEDULER_ID,
|
|
type ValidatingToolCall,
|
|
type WaitingToolCall,
|
|
type SuccessfulToolCall,
|
|
type ErroredToolCall,
|
|
type CancelledToolCall,
|
|
type ExecutingToolCall,
|
|
type ToolCallRequestInfo,
|
|
type ToolCallResponseInfo,
|
|
} from './types.js';
|
|
import {
|
|
ToolConfirmationOutcome,
|
|
type AnyDeclarativeTool,
|
|
type AnyToolInvocation,
|
|
} from '../tools/tools.js';
|
|
import { MessageBusType } from '../confirmation-bus/types.js';
|
|
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
|
import { ApprovalMode } from '../policy/types.js';
|
|
|
|
describe('SchedulerStateManager', () => {
|
|
const mockRequest: ToolCallRequestInfo = {
|
|
callId: 'call-1',
|
|
name: 'test-tool',
|
|
args: { foo: 'bar' },
|
|
isClientInitiated: false,
|
|
prompt_id: 'prompt-1',
|
|
};
|
|
|
|
const mockTool = {
|
|
name: 'test-tool',
|
|
displayName: 'Test Tool',
|
|
} as AnyDeclarativeTool;
|
|
|
|
const mockInvocation = {
|
|
shouldConfirmExecute: vi.fn(),
|
|
} as unknown as AnyToolInvocation;
|
|
|
|
const createValidatingCall = (
|
|
id = 'call-1',
|
|
mode: ApprovalMode = ApprovalMode.DEFAULT,
|
|
): ValidatingToolCall => ({
|
|
status: CoreToolCallStatus.Validating,
|
|
request: { ...mockRequest, callId: id },
|
|
tool: mockTool,
|
|
invocation: mockInvocation,
|
|
startTime: Date.now(),
|
|
approvalMode: mode,
|
|
});
|
|
|
|
const createMockResponse = (id: string): ToolCallResponseInfo => ({
|
|
callId: id,
|
|
responseParts: [],
|
|
resultDisplay: 'Success',
|
|
error: undefined,
|
|
errorType: undefined,
|
|
});
|
|
|
|
let stateManager: SchedulerStateManager;
|
|
let mockMessageBus: MessageBus;
|
|
let onUpdate: (calls: unknown[]) => void;
|
|
|
|
beforeEach(() => {
|
|
onUpdate = vi.fn();
|
|
mockMessageBus = {
|
|
publish: vi.fn(),
|
|
subscribe: vi.fn(),
|
|
unsubscribe: vi.fn(),
|
|
} as unknown as MessageBus;
|
|
|
|
// Capture the update when published
|
|
vi.mocked(mockMessageBus.publish).mockImplementation((msg) => {
|
|
// Return a Promise to satisfy the void | Promise<void> signature if needed,
|
|
// though typically mocks handle it.
|
|
if (msg.type === MessageBusType.TOOL_CALLS_UPDATE) {
|
|
onUpdate(msg.toolCalls);
|
|
}
|
|
return Promise.resolve();
|
|
});
|
|
|
|
stateManager = new SchedulerStateManager(mockMessageBus);
|
|
});
|
|
|
|
describe('Observer Callback', () => {
|
|
it('should trigger onTerminalCall when finalizing a call', () => {
|
|
const onTerminalCall = vi.fn();
|
|
const manager = new SchedulerStateManager(
|
|
mockMessageBus,
|
|
ROOT_SCHEDULER_ID,
|
|
onTerminalCall,
|
|
);
|
|
const call = createValidatingCall();
|
|
manager.enqueue([call]);
|
|
manager.dequeue();
|
|
manager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Success,
|
|
createMockResponse(call.request.callId),
|
|
);
|
|
manager.finalizeCall(call.request.callId);
|
|
|
|
expect(onTerminalCall).toHaveBeenCalledTimes(1);
|
|
expect(onTerminalCall).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
status: CoreToolCallStatus.Success,
|
|
request: expect.objectContaining({ callId: call.request.callId }),
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('should trigger onTerminalCall for every call in cancelAllQueued', () => {
|
|
const onTerminalCall = vi.fn();
|
|
const manager = new SchedulerStateManager(
|
|
mockMessageBus,
|
|
ROOT_SCHEDULER_ID,
|
|
onTerminalCall,
|
|
);
|
|
manager.enqueue([createValidatingCall('1'), createValidatingCall('2')]);
|
|
|
|
manager.cancelAllQueued('Test cancel');
|
|
|
|
expect(onTerminalCall).toHaveBeenCalledTimes(2);
|
|
expect(onTerminalCall).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
status: CoreToolCallStatus.Cancelled,
|
|
request: expect.objectContaining({ callId: '1' }),
|
|
}),
|
|
);
|
|
expect(onTerminalCall).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
status: CoreToolCallStatus.Cancelled,
|
|
request: expect.objectContaining({ callId: '2' }),
|
|
}),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Initialization', () => {
|
|
it('should start with empty state', () => {
|
|
expect(stateManager.isActive).toBe(false);
|
|
expect(stateManager.activeCallCount).toBe(0);
|
|
expect(stateManager.queueLength).toBe(0);
|
|
expect(stateManager.getSnapshot()).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('Lookup Operations', () => {
|
|
it('should find tool calls in active calls', () => {
|
|
const call = createValidatingCall('active-1');
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
expect(stateManager.getToolCall('active-1')).toEqual(call);
|
|
});
|
|
|
|
it('should find tool calls in the queue', () => {
|
|
const call = createValidatingCall('queued-1');
|
|
stateManager.enqueue([call]);
|
|
expect(stateManager.getToolCall('queued-1')).toEqual(call);
|
|
});
|
|
|
|
it('should find tool calls in the completed batch', () => {
|
|
const call = createValidatingCall('completed-1');
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
stateManager.updateStatus(
|
|
'completed-1',
|
|
CoreToolCallStatus.Success,
|
|
createMockResponse('completed-1'),
|
|
);
|
|
stateManager.finalizeCall('completed-1');
|
|
expect(stateManager.getToolCall('completed-1')).toBeDefined();
|
|
});
|
|
|
|
it('should return undefined for non-existent callIds', () => {
|
|
expect(stateManager.getToolCall('void')).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Queue Management', () => {
|
|
it('should enqueue calls and notify', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
|
|
expect(stateManager.queueLength).toBe(1);
|
|
expect(onUpdate).toHaveBeenCalledWith([call]);
|
|
});
|
|
|
|
it('should dequeue calls and notify', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
|
|
const dequeued = stateManager.dequeue();
|
|
|
|
expect(dequeued).toEqual(call);
|
|
expect(stateManager.queueLength).toBe(0);
|
|
expect(stateManager.activeCallCount).toBe(1);
|
|
expect(onUpdate).toHaveBeenCalled();
|
|
});
|
|
|
|
it('should return undefined when dequeueing from empty queue', () => {
|
|
const dequeued = stateManager.dequeue();
|
|
expect(dequeued).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe('Status Transitions', () => {
|
|
it('should transition validating to scheduled', () => {
|
|
const call = createValidatingCall('call-1', ApprovalMode.PLAN);
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Scheduled,
|
|
);
|
|
|
|
const snapshot = stateManager.getSnapshot();
|
|
expect(snapshot[0].status).toBe(CoreToolCallStatus.Scheduled);
|
|
expect(snapshot[0].request.callId).toBe(call.request.callId);
|
|
expect(snapshot[0].approvalMode).toBe(ApprovalMode.PLAN);
|
|
});
|
|
|
|
it('should transition scheduled to executing', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Scheduled,
|
|
);
|
|
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Executing,
|
|
);
|
|
|
|
expect(stateManager.firstActiveCall?.status).toBe(
|
|
CoreToolCallStatus.Executing,
|
|
);
|
|
});
|
|
|
|
it('should transition to success and move to completed batch', () => {
|
|
const call = createValidatingCall('call-1', ApprovalMode.PLAN);
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
const response: ToolCallResponseInfo = {
|
|
callId: call.request.callId,
|
|
responseParts: [],
|
|
resultDisplay: 'Success',
|
|
error: undefined,
|
|
errorType: undefined,
|
|
};
|
|
|
|
vi.mocked(onUpdate).mockClear();
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Success,
|
|
response,
|
|
);
|
|
expect(onUpdate).toHaveBeenCalledTimes(1);
|
|
|
|
vi.mocked(onUpdate).mockClear();
|
|
stateManager.finalizeCall(call.request.callId);
|
|
expect(onUpdate).toHaveBeenCalledTimes(1);
|
|
|
|
expect(stateManager.isActive).toBe(false);
|
|
expect(stateManager.completedBatch).toHaveLength(1);
|
|
const completed = stateManager.completedBatch[0] as SuccessfulToolCall;
|
|
expect(completed.status).toBe(CoreToolCallStatus.Success);
|
|
expect(completed.response).toEqual(response);
|
|
expect(completed.durationMs).toBeDefined();
|
|
expect(completed.approvalMode).toBe(ApprovalMode.PLAN);
|
|
});
|
|
|
|
it('should transition to error and move to completed batch', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
const response: ToolCallResponseInfo = {
|
|
callId: call.request.callId,
|
|
responseParts: [],
|
|
resultDisplay: 'Error',
|
|
error: new Error('Failed'),
|
|
errorType: undefined,
|
|
};
|
|
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Error,
|
|
response,
|
|
);
|
|
stateManager.finalizeCall(call.request.callId);
|
|
|
|
expect(stateManager.isActive).toBe(false);
|
|
expect(stateManager.completedBatch).toHaveLength(1);
|
|
const completed = stateManager.completedBatch[0] as ErroredToolCall;
|
|
expect(completed.status).toBe(CoreToolCallStatus.Error);
|
|
expect(completed.response).toEqual(response);
|
|
});
|
|
|
|
it('should transition to awaiting_approval with details', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
const details = {
|
|
type: 'info' as const,
|
|
title: 'Confirm',
|
|
prompt: 'Proceed?',
|
|
onConfirm: vi.fn(),
|
|
};
|
|
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.AwaitingApproval,
|
|
details,
|
|
);
|
|
|
|
const active = stateManager.firstActiveCall as WaitingToolCall;
|
|
expect(active.status).toBe(CoreToolCallStatus.AwaitingApproval);
|
|
expect(active.confirmationDetails).toEqual(details);
|
|
});
|
|
|
|
it('should transition to awaiting_approval with event-driven format', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
const details = {
|
|
type: 'info' as const,
|
|
title: 'Confirm',
|
|
prompt: 'Proceed?',
|
|
};
|
|
const eventDrivenData = {
|
|
correlationId: 'corr-123',
|
|
confirmationDetails: details,
|
|
};
|
|
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.AwaitingApproval,
|
|
eventDrivenData,
|
|
);
|
|
|
|
const active = stateManager.firstActiveCall as WaitingToolCall;
|
|
expect(active.status).toBe(CoreToolCallStatus.AwaitingApproval);
|
|
expect(active.correlationId).toBe('corr-123');
|
|
expect(active.confirmationDetails).toEqual(details);
|
|
});
|
|
|
|
it('should preserve diff when cancelling an edit tool call', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
const details = {
|
|
type: 'edit' as const,
|
|
title: 'Edit',
|
|
fileName: 'test.txt',
|
|
filePath: '/path/to/test.txt',
|
|
fileDiff: 'diff',
|
|
originalContent: 'old',
|
|
newContent: 'new',
|
|
onConfirm: vi.fn(),
|
|
};
|
|
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.AwaitingApproval,
|
|
details,
|
|
);
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Cancelled,
|
|
'User said no',
|
|
);
|
|
stateManager.finalizeCall(call.request.callId);
|
|
|
|
const completed = stateManager.completedBatch[0] as CancelledToolCall;
|
|
expect(completed.status).toBe(CoreToolCallStatus.Cancelled);
|
|
expect(completed.response.resultDisplay).toEqual({
|
|
fileDiff: 'diff',
|
|
fileName: 'test.txt',
|
|
filePath: '/path/to/test.txt',
|
|
originalContent: 'old',
|
|
newContent: 'new',
|
|
});
|
|
});
|
|
|
|
it('should ignore status updates for non-existent callIds', () => {
|
|
stateManager.updateStatus('unknown', CoreToolCallStatus.Scheduled);
|
|
expect(onUpdate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should ignore status updates for terminal calls', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Success,
|
|
createMockResponse(call.request.callId),
|
|
);
|
|
stateManager.finalizeCall(call.request.callId);
|
|
|
|
vi.mocked(onUpdate).mockClear();
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Scheduled,
|
|
);
|
|
expect(onUpdate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should only finalize terminal calls', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Executing,
|
|
);
|
|
stateManager.finalizeCall(call.request.callId);
|
|
|
|
expect(stateManager.isActive).toBe(true);
|
|
expect(stateManager.completedBatch).toHaveLength(0);
|
|
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Success,
|
|
createMockResponse(call.request.callId),
|
|
);
|
|
stateManager.finalizeCall(call.request.callId);
|
|
|
|
expect(stateManager.isActive).toBe(false);
|
|
expect(stateManager.completedBatch).toHaveLength(1);
|
|
});
|
|
|
|
it('should merge liveOutput and pid during executing updates', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
// Start executing
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Executing,
|
|
);
|
|
let active = stateManager.firstActiveCall as ExecutingToolCall;
|
|
expect(active.status).toBe(CoreToolCallStatus.Executing);
|
|
expect(active.liveOutput).toBeUndefined();
|
|
|
|
// Update with live output
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Executing,
|
|
{
|
|
liveOutput: 'chunk 1',
|
|
},
|
|
);
|
|
active = stateManager.firstActiveCall as ExecutingToolCall;
|
|
expect(active.liveOutput).toBe('chunk 1');
|
|
|
|
// Update with pid (should preserve liveOutput)
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Executing,
|
|
{
|
|
pid: 1234,
|
|
},
|
|
);
|
|
active = stateManager.firstActiveCall as ExecutingToolCall;
|
|
expect(active.liveOutput).toBe('chunk 1');
|
|
expect(active.pid).toBe(1234);
|
|
|
|
// Update live output again (should preserve pid)
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Executing,
|
|
{
|
|
liveOutput: 'chunk 2',
|
|
},
|
|
);
|
|
active = stateManager.firstActiveCall as ExecutingToolCall;
|
|
expect(active.liveOutput).toBe('chunk 2');
|
|
expect(active.pid).toBe(1234);
|
|
});
|
|
|
|
it('should update progressMessage and progressPercent during executing updates', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
// Update with progress
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Executing,
|
|
{
|
|
progressMessage: 'Starting...',
|
|
progressPercent: 10,
|
|
},
|
|
);
|
|
let active = stateManager.firstActiveCall as ExecutingToolCall;
|
|
expect(active.progressMessage).toBe('Starting...');
|
|
expect(active.progressPercent).toBe(10);
|
|
|
|
// Update progress further
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Executing,
|
|
{
|
|
progressMessage: 'Halfway!',
|
|
progressPercent: 50,
|
|
},
|
|
);
|
|
active = stateManager.firstActiveCall as ExecutingToolCall;
|
|
expect(active.progressMessage).toBe('Halfway!');
|
|
expect(active.progressPercent).toBe(50);
|
|
});
|
|
});
|
|
|
|
describe('Argument Updates', () => {
|
|
it('should update args and invocation', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
const newArgs = { foo: 'updated' };
|
|
const newInvocation = { ...mockInvocation } as AnyToolInvocation;
|
|
|
|
stateManager.updateArgs(call.request.callId, newArgs, newInvocation);
|
|
|
|
const active = stateManager.firstActiveCall;
|
|
if (active && 'invocation' in active) {
|
|
expect(active.invocation).toEqual(newInvocation);
|
|
} else {
|
|
throw new Error('Active call should have invocation');
|
|
}
|
|
});
|
|
|
|
it('should ignore arg updates for errored calls', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Error,
|
|
createMockResponse(call.request.callId),
|
|
);
|
|
stateManager.finalizeCall(call.request.callId);
|
|
|
|
stateManager.updateArgs(
|
|
call.request.callId,
|
|
{ foo: 'new' },
|
|
mockInvocation,
|
|
);
|
|
|
|
const completed = stateManager.completedBatch[0];
|
|
expect(completed.request.args).toEqual(mockRequest.args);
|
|
});
|
|
});
|
|
|
|
describe('Outcome Tracking', () => {
|
|
it('should set outcome and notify', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
stateManager.setOutcome(
|
|
call.request.callId,
|
|
ToolConfirmationOutcome.ProceedAlways,
|
|
);
|
|
|
|
const active = stateManager.firstActiveCall;
|
|
expect(active?.outcome).toBe(ToolConfirmationOutcome.ProceedAlways);
|
|
expect(onUpdate).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Batch Operations', () => {
|
|
it('should cancel all queued calls', () => {
|
|
stateManager.enqueue([
|
|
createValidatingCall('1'),
|
|
createValidatingCall('2'),
|
|
]);
|
|
|
|
vi.mocked(onUpdate).mockClear();
|
|
stateManager.cancelAllQueued('Batch cancel');
|
|
|
|
expect(stateManager.queueLength).toBe(0);
|
|
expect(stateManager.completedBatch).toHaveLength(2);
|
|
expect(
|
|
stateManager.completedBatch.every(
|
|
(c) => c.status === CoreToolCallStatus.Cancelled,
|
|
),
|
|
).toBe(true);
|
|
expect(onUpdate).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('should not notify if cancelAllQueued is called on an empty queue', () => {
|
|
vi.mocked(onUpdate).mockClear();
|
|
stateManager.cancelAllQueued('Batch cancel');
|
|
expect(onUpdate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should clear batch and notify', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Success,
|
|
createMockResponse(call.request.callId),
|
|
);
|
|
stateManager.finalizeCall(call.request.callId);
|
|
|
|
stateManager.clearBatch();
|
|
|
|
expect(stateManager.completedBatch).toHaveLength(0);
|
|
expect(onUpdate).toHaveBeenCalledWith([]);
|
|
});
|
|
|
|
it('should return a copy of the completed batch (defensive)', () => {
|
|
const call = createValidatingCall();
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Success,
|
|
createMockResponse(call.request.callId),
|
|
);
|
|
stateManager.finalizeCall(call.request.callId);
|
|
|
|
const batch = stateManager.completedBatch;
|
|
expect(batch).toHaveLength(1);
|
|
|
|
// Mutate the returned array
|
|
batch.pop();
|
|
expect(batch).toHaveLength(0);
|
|
|
|
// Verify internal state is unchanged
|
|
expect(stateManager.completedBatch).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('Snapshot and Ordering', () => {
|
|
it('should return snapshot in order: completed, active, queue', () => {
|
|
// 1. Completed
|
|
const call1 = createValidatingCall('1');
|
|
stateManager.enqueue([call1]);
|
|
stateManager.dequeue();
|
|
stateManager.updateStatus(
|
|
'1',
|
|
CoreToolCallStatus.Success,
|
|
createMockResponse('1'),
|
|
);
|
|
stateManager.finalizeCall('1');
|
|
|
|
// 2. Active
|
|
const call2 = createValidatingCall('2');
|
|
stateManager.enqueue([call2]);
|
|
stateManager.dequeue();
|
|
|
|
// 3. Queue
|
|
const call3 = createValidatingCall('3');
|
|
stateManager.enqueue([call3]);
|
|
|
|
const snapshot = stateManager.getSnapshot();
|
|
expect(snapshot).toHaveLength(3);
|
|
expect(snapshot[0].request.callId).toBe('1');
|
|
expect(snapshot[1].request.callId).toBe('2');
|
|
expect(snapshot[2].request.callId).toBe('3');
|
|
});
|
|
});
|
|
|
|
describe('progress field preservation', () => {
|
|
it('should preserve progress and progressTotal in toExecuting', () => {
|
|
const call = createValidatingCall('progress-1');
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Executing,
|
|
{
|
|
progress: 5,
|
|
progressTotal: 10,
|
|
progressMessage: 'Working',
|
|
progressPercent: 50,
|
|
},
|
|
);
|
|
|
|
const active = stateManager.firstActiveCall as ExecutingToolCall;
|
|
expect(active.status).toBe(CoreToolCallStatus.Executing);
|
|
expect(active.progress).toBe(5);
|
|
expect(active.progressTotal).toBe(10);
|
|
expect(active.progressMessage).toBe('Working');
|
|
expect(active.progressPercent).toBe(50);
|
|
});
|
|
|
|
it('should preserve progress fields after a liveOutput update', () => {
|
|
const call = createValidatingCall('progress-2');
|
|
stateManager.enqueue([call]);
|
|
stateManager.dequeue();
|
|
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Executing,
|
|
{
|
|
progress: 5,
|
|
progressTotal: 10,
|
|
progressMessage: 'Working',
|
|
progressPercent: 50,
|
|
},
|
|
);
|
|
|
|
stateManager.updateStatus(
|
|
call.request.callId,
|
|
CoreToolCallStatus.Executing,
|
|
{
|
|
liveOutput: 'some output',
|
|
},
|
|
);
|
|
|
|
const active = stateManager.firstActiveCall as ExecutingToolCall;
|
|
expect(active.status).toBe(CoreToolCallStatus.Executing);
|
|
expect(active.liveOutput).toBe('some output');
|
|
expect(active.progress).toBe(5);
|
|
expect(active.progressTotal).toBe(10);
|
|
expect(active.progressMessage).toBe('Working');
|
|
expect(active.progressPercent).toBe(50);
|
|
});
|
|
});
|
|
});
|