Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions .axioma/quality.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,7 @@
## 2024-06-26 - [Verifying Polymorphic Type Inference]
**Learning:** Hooks that perform automatic type inference (like `useAIPrompt` for multimodal inputs) should have explicit unit tests for each supported input type (e.g., `ArrayBuffer`, `Blob`, `string[]`). Relying only on happy-path text tests can leave type-specific logic uncovered and prone to regressions.
**Action:** Always include a dedicated test case for each supported input type in internal normalization or inference functions to ensure full branch coverage and robustness.

## 2026-06-07 - [Explicit AbortError State Management]
**Learning:** When wrapping browser-native asynchronous APIs (like the Built-in AI APIs), it's critical to explicitly handle the `AbortError` in `catch` blocks by resetting the hook's status to `'idle'`. This ensures that if an operation is cancelled (e.g., via a signal or component unmount), the UI state doesn't remain "stuck" in a loading or active state, improving the robustness of state management across the application.
**Action:** Always transition the status to `'idle'` when an `AbortError` is caught in AI-related hooks to maintain synchronization between the UI and the underlying API state.
15 changes: 15 additions & 0 deletions lib/hooks/useAIPrompt.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,4 +370,19 @@ describe('useAIPrompt', () => {
)
consoleSpy.mockRestore()
})

it('should handle AbortError by resetting to idle', async () => {
const abortError = new Error('The operation was aborted')
abortError.name = 'AbortError'
mockSession.prompt.mockRejectedValue(abortError)

const { result } = renderHook(() => useAIPrompt())

await act(async () => {
await result.current.prompt('hello')
})

expect(result.current.status).toBe('idle')
expect(result.current.error).toBeNull()
})
})
1 change: 1 addition & 0 deletions lib/hooks/useAIPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ export function useAIPrompt(
}
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
setStatus('idle')
return
}
setError(
Expand Down
15 changes: 15 additions & 0 deletions lib/hooks/useAIRewriter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,4 +312,19 @@ describe('useAIRewriter', () => {
expect(result.current.error).toBe(null);
expect(result.current.progress).toBe(null);
});

it('should handle AbortError by resetting to idle', async () => {
const abortError = new Error('The operation was aborted');
abortError.name = 'AbortError';
mockRewriter.rewrite.mockRejectedValue(abortError);

const { result } = renderHook(() => useAIRewriter());

await act(async () => {
await result.current.rewrite('original text');
});

expect(result.current.status).toBe('idle');
expect(result.current.error).toBeNull();
});
});
1 change: 1 addition & 0 deletions lib/hooks/useAIRewriter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ export function useAIRewriter(options: UseAIRewriterOptions = {}): UseAIRewriter
}
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
setStatus('idle');
return;
}
setError(err instanceof Error ? err : new Error('Unknown error during rewriting'));
Expand Down
15 changes: 15 additions & 0 deletions lib/hooks/useAISummarize.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,4 +407,19 @@ describe('useAISummarize', () => {
expect(result.current.status).toBe('error');
expect(result.current.error?.message).toContain('User activation required');
});

it('should handle AbortError by resetting to idle', async () => {
const abortError = new Error('The operation was aborted');
abortError.name = 'AbortError';
mockSummarizer.summarize.mockRejectedValue(abortError);

const { result } = renderHook(() => useAISummarize());

await act(async () => {
await result.current.summarize('input text');
});

expect(result.current.status).toBe('idle');
expect(result.current.error).toBeNull();
});
});
1 change: 1 addition & 0 deletions lib/hooks/useAISummarize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,7 @@ export function useAISummarize(options: UseAISummarizeOptions = {}): UseAISummar
}
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
setStatus('idle');
return;
}
setError(err instanceof Error ? err : new Error('Unknown error during summarization'));
Expand Down
15 changes: 15 additions & 0 deletions lib/hooks/useAIWrite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,19 @@ describe('useAIWrite', () => {
expect(result.current.error).toBe(null);
expect(result.current.progress).toBe(null);
});

it('should handle AbortError by resetting to idle', async () => {
const abortError = new Error('The operation was aborted');
abortError.name = 'AbortError';
mockWriter.write.mockRejectedValue(abortError);

const { result } = renderHook(() => useAIWrite());

await act(async () => {
await result.current.write('prompt');
});

expect(result.current.status).toBe('idle');
expect(result.current.error).toBeNull();
});
});
1 change: 1 addition & 0 deletions lib/hooks/useAIWrite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export function useAIWrite(options: UseAIWriteOptions = {}): UseAIWriteReturn {
}
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
setStatus('idle');
return;
}
setError(err instanceof Error ? err : new Error('Unknown error during writing'));
Expand Down
15 changes: 15 additions & 0 deletions lib/hooks/useTranslator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,19 @@ describe('useTranslator', () => {

expect(result.current.progress).toEqual({ loaded: 50, total: 100 });
});

it('should handle AbortError by resetting to idle', async () => {
const abortError = new Error('The operation was aborted');
abortError.name = 'AbortError';
mockTranslator.translate.mockRejectedValue(abortError);

const { result } = renderHook(() => useTranslator({ sourceLanguage: 'en', targetLanguage: 'es', streaming: false, warmup: false }));

await act(async () => {
await result.current.translate('Hello world');
});

expect(result.current.status).toBe('idle');
expect(result.current.error).toBeNull();
});
});
1 change: 1 addition & 0 deletions lib/hooks/useTranslator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ export function useTranslator(options: UseTranslatorOptions = {}): UseTranslator
}
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
setStatus('idle');
return;
}
setError(err instanceof Error ? err : new Error('Unknown error during translation'));
Expand Down
Loading