Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 108 additions & 1 deletion src/__tests__/main/cue/cue-completion-chains.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ describe('CueEngine completion chains', () => {
engine.stop();
});

it('truncates sourceOutput to 5000 chars', () => {
it('truncates sourceOutput to 5000 chars and sets outputTruncated to true', () => {
const config = createMockConfig({
subscriptions: [
{
Expand All @@ -180,6 +180,34 @@ describe('CueEngine completion chains', () => {
const request = (deps.onCueRun as ReturnType<typeof vi.fn>).mock.calls[0][0];
const event = request.event as CueEvent;
expect((event.payload.sourceOutput as string).length).toBe(5000);
expect(event.payload.outputTruncated).toBe(true);

engine.stop();
});

it('sets outputTruncated to false when output is under limit', () => {
const config = createMockConfig({
subscriptions: [
{
name: 'on-done',
event: 'agent.completed',
enabled: true,
prompt: 'follow up',
source_session: 'agent-a',
},
],
});
mockLoadCueConfig.mockReturnValue(config);
const deps = createMockDeps();
const engine = new CueEngine(deps);
engine.start();

vi.clearAllMocks();
engine.notifyAgentCompleted('agent-a', { stdout: 'short output' });

const request = (deps.onCueRun as ReturnType<typeof vi.fn>).mock.calls[0][0];
const event = request.event as CueEvent;
expect(event.payload.outputTruncated).toBe(false);

engine.stop();
});
Expand Down Expand Up @@ -451,6 +479,42 @@ describe('CueEngine completion chains', () => {
engine.stop();
});

it('sets outputTruncated in fan-in aggregate event when any source is truncated', () => {
const config = createMockConfig({
subscriptions: [
{
name: 'all-done',
event: 'agent.completed',
enabled: true,
prompt: 'aggregate',
source_session: ['agent-a', 'agent-b'],
},
],
});
mockLoadCueConfig.mockReturnValue(config);
const deps = createMockDeps();
const engine = new CueEngine(deps);
engine.start();

vi.clearAllMocks();

const longOutput = 'x'.repeat(10000);
engine.notifyAgentCompleted('agent-a', { sessionName: 'Agent A', stdout: longOutput });
engine.notifyAgentCompleted('agent-b', { sessionName: 'Agent B', stdout: 'short' });

expect(deps.onCueRun).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
payload: expect.objectContaining({
outputTruncated: true,
}),
}),
})
);

engine.stop();
});

it('logs waiting message during fan-in', () => {
const config = createMockConfig({
subscriptions: [
Expand Down Expand Up @@ -565,6 +629,49 @@ describe('CueEngine completion chains', () => {

engine.stop();
});

it('includes outputTruncated in continue-mode timeout payload', () => {
const config = createMockConfig({
subscriptions: [
{
name: 'all-done',
event: 'agent.completed',
enabled: true,
prompt: 'aggregate',
source_session: ['agent-a', 'agent-b'],
},
],
settings: {
timeout_minutes: 1,
timeout_on_fail: 'continue',
max_concurrent: 1,
queue_size: 10,
},
});
mockLoadCueConfig.mockReturnValue(config);
const deps = createMockDeps();
const engine = new CueEngine(deps);
engine.start();

vi.clearAllMocks();
const longOutput = 'x'.repeat(10000);
engine.notifyAgentCompleted('agent-a', { stdout: longOutput });

vi.advanceTimersByTime(1 * 60 * 1000 + 100);

expect(deps.onCueRun).toHaveBeenCalledWith(
expect.objectContaining({
event: expect.objectContaining({
payload: expect.objectContaining({
outputTruncated: true,
partial: true,
}),
}),
})
);

engine.stop();
});
});

describe('hasCompletionSubscribers', () => {
Expand Down
27 changes: 26 additions & 1 deletion src/__tests__/main/cue/cue-file-watcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ describe('cue-file-watcher', () => {
expect(mockClose).toHaveBeenCalled();
});

it('handles watcher errors gracefully', () => {
it('handles watcher errors gracefully with console.error fallback', () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

createCueFileWatcher({
Expand All @@ -215,4 +215,29 @@ describe('cue-file-watcher', () => {

consoleSpy.mockRestore();
});

it('routes errors through onLog when provided', () => {
const onLog = vi.fn();
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});

createCueFileWatcher({
watchGlob: '**/*.ts',
projectRoot: '/test',
debounceMs: 5000,
onEvent: vi.fn(),
triggerName: 'my-watcher',
onLog,
});

const errorHandler = mockOn.mock.calls.find((call) => call[0] === 'error')?.[1];
expect(errorHandler).toBeDefined();

errorHandler(new Error('Watch error'));

expect(onLog).toHaveBeenCalledWith('error', expect.stringContaining('my-watcher'));
expect(onLog).toHaveBeenCalledWith('error', expect.stringContaining('Watch error'));
expect(consoleSpy).not.toHaveBeenCalled();

consoleSpy.mockRestore();
});
});
45 changes: 45 additions & 0 deletions src/__tests__/main/cue/cue-filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,51 @@ describe('cue-filter', () => {
});
});

describe('NaN handling in numeric comparisons', () => {
it('rejects non-numeric payload value with > filter', () => {
expect(matchesFilter({ size: 'abc' }, { size: '>5' })).toBe(false);
});

it('rejects non-numeric payload value with >= filter', () => {
expect(matchesFilter({ size: 'abc' }, { size: '>=5' })).toBe(false);
});

it('rejects non-numeric payload value with < filter', () => {
expect(matchesFilter({ size: 'abc' }, { size: '<5' })).toBe(false);
});

it('rejects non-numeric payload value with <= filter', () => {
expect(matchesFilter({ size: 'abc' }, { size: '<=5' })).toBe(false);
});

it('rejects non-numeric threshold in >= filter', () => {
expect(matchesFilter({ size: 100 }, { size: '>=abc' })).toBe(false);
});

it('rejects non-numeric threshold in > filter', () => {
expect(matchesFilter({ size: 100 }, { size: '>abc' })).toBe(false);
});

it('rejects non-numeric threshold in < filter', () => {
expect(matchesFilter({ size: 100 }, { size: '<abc' })).toBe(false);
});

it('rejects non-numeric threshold in <= filter', () => {
expect(matchesFilter({ size: 100 }, { size: '<=abc' })).toBe(false);
});

it('rejects empty string payload value with numeric filter', () => {
expect(matchesFilter({ size: '' }, { size: '>0' })).toBe(false);
});

it('handles boolean payload coercion with numeric filter', () => {
// Number(true) === 1, so true > 0 should pass
expect(matchesFilter({ active: true }, { active: '>0' })).toBe(true);
// Number(false) === 0, so false > 0 should fail
expect(matchesFilter({ active: false }, { active: '>0' })).toBe(false);
});
});

describe('empty filter', () => {
it('matches everything when filter is empty', () => {
expect(matchesFilter({ any: 'value' }, {})).toBe(true);
Expand Down
Loading