mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
* fix(web): stop Nexus AI agent when user clicks Stop Wire AbortController through chat streaming so Stop cancels the LangGraph run instead of only hiding the loading UI. Fixes #1615. * fix(web): address PR review feedback for Nexus AI stop Guard stream cleanup against Stop-then-Send races, remove dead cancelled handler, tighten abort error detection, add stopped tool-call status, and extend abort unit tests. Fixes #1615. * chore(autofix): apply prettier + eslint fixes via /autofix command * fix(web): address review findings for Nexus AI stop/cancel - Fix race conditions in useAppState.tsx abort lifecycle: - Replace stale isChatLoading closure guard with chatStateRef - Track and cancel rAF handles in stopChatResponse/finally - Move cancelled chunk check before onChunk dispatch - Simplify finally block to unconditional cleanup via chatStateRef - Guard tool_result from overwriting stopped status - Have clearChat abort in-flight streams before clearing - Reorder isAbortError to check error identity before signal.aborted - Refactor AgentStreamChunk to discriminated union for exhaustive switch - Fix test assertions to use exact .toEqual() per DoD §2.7 - Add test for plain Error with name AbortError - Remove dead markStopped alias, simplify signal spread-conditional --------- Co-authored-by: Gergő Magyar <gergomagyar@icloud.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Test <test@example.com>
97 lines
2.9 KiB
TypeScript
97 lines
2.9 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { streamAgentResponse, type AgentMessage } from '../../src/core/llm/agent';
|
|
|
|
describe('streamAgentResponse abort', () => {
|
|
const userMessage: AgentMessage[] = [{ role: 'user', content: 'hello' }];
|
|
|
|
it('yields cancelled when the LangGraph stream throws AbortError', async () => {
|
|
const agent = {
|
|
stream: async () => {
|
|
throw new DOMException('The operation was aborted', 'AbortError');
|
|
},
|
|
};
|
|
|
|
const chunks = [];
|
|
for await (const chunk of streamAgentResponse(agent as any, userMessage, {
|
|
signal: new AbortController().signal,
|
|
})) {
|
|
chunks.push(chunk);
|
|
}
|
|
|
|
expect(chunks).toEqual([{ type: 'cancelled' }]);
|
|
});
|
|
|
|
it('yields cancelled when the abort signal is set mid-stream', async () => {
|
|
const controller = new AbortController();
|
|
const agent = {
|
|
stream: async function* () {
|
|
yield ['values', { messages: [] }];
|
|
controller.abort();
|
|
for (let i = 0; i < 100; i++) {
|
|
yield ['messages', [{ _getType: () => 'ai', content: 'still going' }]];
|
|
}
|
|
},
|
|
};
|
|
|
|
const chunks = [];
|
|
for await (const chunk of streamAgentResponse(agent as any, userMessage, {
|
|
signal: controller.signal,
|
|
})) {
|
|
chunks.push(chunk);
|
|
if (chunk.type === 'cancelled') break;
|
|
}
|
|
|
|
expect(chunks[chunks.length - 1]).toEqual({ type: 'cancelled' });
|
|
expect(chunks.filter((c) => c.type === 'error')).toEqual([]);
|
|
});
|
|
|
|
it('passes AbortSignal to agent.stream config', async () => {
|
|
const controller = new AbortController();
|
|
let capturedConfig: Record<string, unknown> | undefined;
|
|
|
|
const agent = {
|
|
stream: async (_input: unknown, config: Record<string, unknown>) => {
|
|
capturedConfig = config;
|
|
throw new DOMException('aborted', 'AbortError');
|
|
},
|
|
};
|
|
|
|
for await (const _chunk of streamAgentResponse(agent as any, userMessage, {
|
|
signal: controller.signal,
|
|
})) {
|
|
// drain
|
|
}
|
|
|
|
expect(capturedConfig?.signal).toBe(controller.signal);
|
|
});
|
|
|
|
it('yields cancelled for a plain Error with name AbortError', async () => {
|
|
const agent = {
|
|
stream: async () => {
|
|
throw Object.assign(new Error('aborted'), { name: 'AbortError' });
|
|
},
|
|
};
|
|
|
|
const chunks = [];
|
|
for await (const chunk of streamAgentResponse(agent as any, userMessage)) {
|
|
chunks.push(chunk);
|
|
}
|
|
|
|
expect(chunks).toEqual([{ type: 'cancelled' }]);
|
|
});
|
|
|
|
it('does not treat unrelated errors mentioning abort as cancellation', async () => {
|
|
const agent = {
|
|
stream: async () => {
|
|
throw new Error('Cannot abort the current transaction');
|
|
},
|
|
};
|
|
|
|
const chunks = [];
|
|
for await (const chunk of streamAgentResponse(agent as any, userMessage)) {
|
|
chunks.push(chunk);
|
|
}
|
|
|
|
expect(chunks).toEqual([{ type: 'error', error: 'Cannot abort the current transaction' }]);
|
|
});
|
|
});
|