Repository navigation
increase jest coverage #158
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
03c9f9a
test(core): raise Jest coverage
koreyba dcb8364
test(core): clarify empty bulk summary case
koreyba f1aee75
test(core): fix test type-check errors
koreyba 93d65a9
test(core): address coverage review findings
koreyba d88406c
test(core): normalize offline sync timestamps
koreyba 79fbde7
test(core): cover truthy malformed WordPress settings response
koreyba 9190dbc
test: strengthen core coverage assertions
koreyba e811ed2
test: cover offline queue id fallbacks
koreyba File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
93 changes: 93 additions & 0 deletions
93
core/tests/unit/api-keys-settings-additional-branches.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| import { ApiKeysSettingsService } from '@core/services/apiKeysSettings' | ||
| import { SETTINGS_SERVICE_UNAVAILABLE_MESSAGE } from '@core/services/settingsErrorMessage' | ||
|
|
||
| const serviceWithInvoke = (invoke: jest.Mock) => ({ functions: { invoke } }) as never | ||
|
|
||
| describe('ApiKeysSettingsService additional branch behavior', () => { | ||
| it('handles configured status variants and sends exact status, key, and remove payloads', async () => { | ||
| const invoke = jest.fn() | ||
| .mockResolvedValueOnce({ data: { gemini: { configured: true } }, error: null }) | ||
| .mockResolvedValueOnce({ data: { gemini: { configured: false } }, error: null }) | ||
| .mockResolvedValueOnce({ data: { gemini: { configured: false } }, error: null }) | ||
| const service = new ApiKeysSettingsService(serviceWithInvoke(invoke)) | ||
|
|
||
| await expect(service.getStatus()).resolves.toEqual({ gemini: { configured: true } }) | ||
| await expect(service.upsert('gemini-secret')).resolves.toEqual({ gemini: { configured: false } }) | ||
| await expect(service.removeGeminiApiKey()).resolves.toEqual({ gemini: { configured: false } }) | ||
|
|
||
| expect(invoke).toHaveBeenNthCalledWith(1, 'api-keys-status', { body: {} }) | ||
| expect(invoke).toHaveBeenNthCalledWith(2, 'api-keys-upsert', { body: { geminiApiKey: 'gemini-secret' } }) | ||
| expect(invoke).toHaveBeenNthCalledWith(3, 'api-keys-upsert', { body: { removeGeminiApiKey: true } }) | ||
| }) | ||
|
|
||
| it('passes an empty API key through the exact upsert payload', async () => { | ||
| const invoke = jest.fn().mockResolvedValue({ data: { gemini: { configured: false } }, error: null }) | ||
| const service = new ApiKeysSettingsService(serviceWithInvoke(invoke)) | ||
|
|
||
| await expect(service.upsert('')).resolves.toEqual({ gemini: { configured: false } }) | ||
| expect(invoke).toHaveBeenCalledWith('api-keys-upsert', { body: { geminiApiKey: '' } }) | ||
| }) | ||
|
|
||
| it('rejects malformed status and upsert response shapes', async () => { | ||
| const invoke = jest.fn() | ||
| const service = new ApiKeysSettingsService(serviceWithInvoke(invoke)) | ||
| const malformedResponses: unknown[] = [ | ||
| null, | ||
| 42, | ||
| 'malformed response', | ||
| {}, | ||
| { gemini: null }, | ||
| { gemini: 'malformed settings' }, | ||
| { gemini: {} }, | ||
| { gemini: { configured: 'yes' } }, | ||
| ] | ||
|
|
||
| for (const data of malformedResponses) { | ||
| invoke.mockResolvedValueOnce({ data, error: null }) | ||
| await expect(service.getStatus()).rejects.toThrow('Unexpected response while loading API key settings') | ||
| } | ||
|
|
||
| invoke.mockResolvedValueOnce({ data: { gemini: {} }, error: null }) | ||
| await expect(service.upsert('secret')).rejects.toThrow('Unexpected response while updating API key settings') | ||
| }) | ||
|
|
||
| it('uses context messages, unavailable status fallbacks, and operation-specific fallbacks', async () => { | ||
| const invoke = jest.fn() | ||
| .mockResolvedValueOnce({ | ||
| data: null, | ||
| error: { context: { json: jest.fn().mockResolvedValue({ message: 'status context message' }) } }, | ||
| }) | ||
| .mockResolvedValueOnce({ | ||
| data: null, | ||
| error: { context: { status: 503, json: jest.fn().mockResolvedValue({}) } }, | ||
| }) | ||
| .mockResolvedValueOnce({ data: null, error: { code: 'STATUS_FAILED' } }) | ||
| .mockResolvedValueOnce({ | ||
| data: null, | ||
| error: { context: { json: jest.fn().mockResolvedValue({ msg: 'upsert context message' }) } }, | ||
| }) | ||
| .mockResolvedValueOnce({ | ||
| data: null, | ||
| error: { context: { status: 502, json: jest.fn().mockResolvedValue({}) } }, | ||
| }) | ||
| .mockResolvedValueOnce({ data: null, error: { code: 'UPSERT_FAILED' } }) | ||
| const service = new ApiKeysSettingsService(serviceWithInvoke(invoke)) | ||
|
|
||
| await expect(service.getStatus()).rejects.toThrow('status context message') | ||
| await expect(service.getStatus()).rejects.toThrow(SETTINGS_SERVICE_UNAVAILABLE_MESSAGE) | ||
| await expect(service.getStatus()).rejects.toThrow('Failed to load API key settings') | ||
| await expect(service.upsert('secret')).rejects.toThrow('upsert context message') | ||
| await expect(service.upsert('secret')).rejects.toThrow(SETTINGS_SERVICE_UNAVAILABLE_MESSAGE) | ||
| await expect(service.upsert('secret')).rejects.toThrow('Failed to save API key') | ||
| }) | ||
|
|
||
| it('preserves ordinary Error messages from status and upsert requests', async () => { | ||
| const invoke = jest.fn() | ||
| .mockResolvedValueOnce({ data: null, error: new Error('status request failed') }) | ||
| .mockResolvedValueOnce({ data: null, error: new Error('upsert request failed') }) | ||
| const service = new ApiKeysSettingsService(serviceWithInvoke(invoke)) | ||
|
|
||
| await expect(service.getStatus()).rejects.toThrow('status request failed') | ||
| await expect(service.upsert('secret')).rejects.toThrow('upsert request failed') | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| import { | ||
| formatBulkIndexSummary, | ||
| incrementBulkIndexCounters, | ||
| processBulkIndexNote, | ||
| } from '../../bulkIndex' | ||
| import type { AIIndexNoteRow } from '../../types/aiIndex' | ||
|
|
||
| const note = (id: string, status: AIIndexNoteRow['status']): AIIndexNoteRow => ({ | ||
| id, | ||
| title: id, | ||
| updatedAt: '2026-07-21T00:00:00.000Z', | ||
| lastIndexedAt: null, | ||
| status, | ||
| }) | ||
|
|
||
| describe('bulk index additional branches', () => { | ||
| it('formats zero counters as an empty summary', () => { | ||
| expect(formatBulkIndexSummary(0, 0, 0)).toBe('') | ||
| }) | ||
|
|
||
| it('uses the exact index/reindex payload and applies successful status mutations', async () => { | ||
| const invoke = jest.fn().mockResolvedValue({ data: { outcome: 'indexed', chunkCount: 1 }, error: null }) | ||
| const applyMutationResult = jest.fn() | ||
|
|
||
| await expect(processBulkIndexNote({ | ||
| applyMutationResult, | ||
| invoke, | ||
| note: note('already-indexed', 'indexed'), | ||
| })).resolves.toBe('indexed') | ||
| expect(invoke).toHaveBeenNthCalledWith(1, 'rag-index', { | ||
| body: { noteId: 'already-indexed', action: 'reindex' }, | ||
| }) | ||
|
|
||
| await expect(processBulkIndexNote({ | ||
| applyMutationResult, | ||
| invoke, | ||
| note: note('new-note', 'not_indexed'), | ||
| })).resolves.toBe('indexed') | ||
| expect(invoke).toHaveBeenNthCalledWith(2, 'rag-index', { | ||
| body: { noteId: 'new-note', action: 'index' }, | ||
| }) | ||
| expect(applyMutationResult).toHaveBeenNthCalledWith(1, { | ||
| noteId: 'already-indexed', previousStatus: 'indexed', nextStatus: 'indexed', | ||
| }) | ||
| expect(applyMutationResult).toHaveBeenNthCalledWith(2, { | ||
| noteId: 'new-note', previousStatus: 'not_indexed', nextStatus: 'indexed', | ||
| }) | ||
| }) | ||
|
|
||
| it('aggregates successful, skipped and failed results with only intended side effects', async () => { | ||
| const invoke = jest.fn() | ||
| .mockResolvedValueOnce({ data: { outcome: 'indexed', chunkCount: 2 }, error: null }) | ||
| .mockResolvedValueOnce({ data: { outcome: 'skipped', reason: 'too_short' }, error: null }) | ||
| .mockResolvedValueOnce({ data: { outcome: 'skipped', reason: 'other' }, error: null }) | ||
| .mockResolvedValueOnce({ data: { outcome: 'deleted' }, error: null }) | ||
| .mockResolvedValueOnce({ data: { outcome: 'unknown', message: 'unexpected' }, error: null }) | ||
| .mockResolvedValueOnce({ data: null, error: { message: 'backend failed' } }) | ||
| const applyMutationResult = jest.fn() | ||
| const batch = [ | ||
| note('indexed', 'outdated'), | ||
| note('too-short', 'not_indexed'), | ||
| note('skipped', 'indexed'), | ||
| note('deleted', 'outdated'), | ||
| note('unknown', 'not_indexed'), | ||
| note('failed', 'outdated'), | ||
| ] | ||
| let counters = { successCount: 0, skippedCount: 0, errorCount: 0 } | ||
|
|
||
| const outcomes = [] | ||
| for (const currentNote of batch) { | ||
| const outcome = await processBulkIndexNote({ applyMutationResult, invoke, note: currentNote }) | ||
| outcomes.push(outcome) | ||
| counters = incrementBulkIndexCounters(counters, outcome) | ||
| } | ||
|
|
||
| expect(outcomes).toEqual(['indexed', 'skipped', 'skipped', 'failed', 'failed', 'failed']) | ||
| expect(counters).toEqual({ successCount: 1, skippedCount: 2, errorCount: 3 }) | ||
| expect(formatBulkIndexSummary(counters.successCount, counters.skippedCount, counters.errorCount)) | ||
| .toBe(`1 indexed ${String.fromCodePoint(0x2022)} 2 skipped ${String.fromCodePoint(0x2022)} 3 failed`) | ||
| expect(applyMutationResult).toHaveBeenCalledTimes(2) | ||
| expect(applyMutationResult).toHaveBeenNthCalledWith(1, { | ||
| noteId: 'indexed', previousStatus: 'outdated', nextStatus: 'indexed', | ||
| }) | ||
| expect(applyMutationResult).toHaveBeenNthCalledWith(2, { | ||
| noteId: 'too-short', previousStatus: 'not_indexed', nextStatus: 'not_indexed', | ||
| }) | ||
| }) | ||
|
|
||
| it('turns thrown invocations and truthy backend errors into failed outcomes without mutation', async () => { | ||
| const invoke = jest.fn() | ||
| .mockRejectedValueOnce(new Error('network failure')) | ||
| .mockResolvedValueOnce({ data: { outcome: 'indexed', chunkCount: 1 }, error: { message: 'rejected' } }) | ||
| const applyMutationResult = jest.fn() | ||
| const currentNote = note('failed-note', 'outdated') | ||
|
|
||
| await expect(processBulkIndexNote({ applyMutationResult, invoke, note: currentNote })).resolves.toBe('failed') | ||
| await expect(processBulkIndexNote({ applyMutationResult, invoke, note: currentNote })).resolves.toBe('failed') | ||
| expect(applyMutationResult).not.toHaveBeenCalled() | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| import { compactQueue } from '@core/utils/compactQueue' | ||
| import type { MutationOperation, MutationQueueItem } from '@core/types/offline' | ||
|
|
||
| const item = ( | ||
| id: string, | ||
| noteId: string, | ||
| operation: MutationOperation, | ||
| clientUpdatedAt: string, | ||
| payload: Record<string, unknown> = {}, | ||
| overrides: Partial<MutationQueueItem> = {}, | ||
| ): MutationQueueItem => ({ | ||
| id, | ||
| noteId, | ||
| operation, | ||
| payload: payload as MutationQueueItem['payload'], | ||
| clientUpdatedAt, | ||
| status: 'failed', | ||
| attempts: 1, | ||
| ...overrides, | ||
| }) | ||
|
|
||
| describe('compactQueue additional branch behavior', () => { | ||
| it('compacts create plus multiple updates into one create with the final payload and metadata', () => { | ||
| const result = compactQueue([ | ||
| item('update-late', 'new-note', 'update', '2026-01-01T00:00:03Z', { title: 'final' }), | ||
| item('create', 'new-note', 'create', '2026-01-01T00:00:01Z', { title: 'initial' }, { attempts: 2 }), | ||
| item('update-middle', 'new-note', 'update', '2026-01-01T00:00:02Z', { title: 'middle' }), | ||
| ]) | ||
|
|
||
| expect(result).toEqual([ | ||
| item('update-late', 'new-note', 'create', '2026-01-01T00:00:03Z', { title: 'final' }, { attempts: 2, status: 'pending' }), | ||
| ]) | ||
| }) | ||
|
|
||
| it('collapses create-update-delete chains to a real no-op', () => { | ||
| const result = compactQueue([ | ||
| item('create', 'temporary-note', 'create', '2026-01-01T00:00:01Z', { title: 'initial' }), | ||
| item('update-one', 'temporary-note', 'update', '2026-01-01T00:00:02Z', { title: 'edited' }), | ||
| item('update-two', 'temporary-note', 'update', '2026-01-01T00:00:03Z', { title: 'edited again' }), | ||
| item('delete', 'temporary-note', 'delete', '2026-01-01T00:00:04Z'), | ||
| ]) | ||
|
|
||
| expect(result).toEqual([]) | ||
| }) | ||
|
|
||
| it('keeps a final delete, but drops update-after-delete chains as implemented', () => { | ||
| const result = compactQueue([ | ||
| item('update-delete', 'deleted-note', 'update', '2026-01-01T00:00:01Z', { title: 'before delete' }), | ||
| item('final-update', 'updated-note', 'update', '2026-01-01T00:00:05Z', { title: 'final update' }), | ||
| item('delete', 'deleted-note', 'delete', '2026-01-01T00:00:02Z', { reason: 'removed' }), | ||
| item('update-again', 'updated-note', 'update', '2026-01-01T00:00:03Z', { title: 'intermediate' }), | ||
| item('delete-then-update', 'updated-note', 'delete', '2026-01-01T00:00:04Z'), | ||
| ]) | ||
|
|
||
| expect(result).toEqual([ | ||
| item('delete', 'deleted-note', 'delete', '2026-01-01T00:00:02Z', { reason: 'removed' }, { status: 'pending' }), | ||
| ]) | ||
|
|
||
| expect(compactQueue([ | ||
| item('update-one', 'updated-only', 'update', '2026-01-01T00:00:01Z', { title: 'initial' }), | ||
| item('update-two', 'updated-only', 'update', '2026-01-01T00:00:02Z', { title: 'final' }), | ||
| ])).toEqual([ | ||
| item('update-two', 'updated-only', 'update', '2026-01-01T00:00:02Z', { title: 'final' }, { status: 'pending' }), | ||
| ]) | ||
| }) | ||
|
|
||
| it('orders the final compacted queue by the surviving operation timestamps', () => { | ||
| const result = compactQueue([ | ||
| item('late-update', 'late-note', 'update', '2026-01-01T00:00:10Z', { title: 'late' }), | ||
| item('early-create', 'early-note', 'create', '2026-01-01T00:00:01Z', { title: 'initial' }), | ||
| item('early-update', 'early-note', 'update', '2026-01-01T00:00:02Z', { title: 'final' }), | ||
| item('middle-delete', 'middle-note', 'delete', '2026-01-01T00:00:05Z'), | ||
| ]) | ||
|
|
||
| expect(result.map(({ noteId, operation, clientUpdatedAt, status }) => ({ noteId, operation, clientUpdatedAt, status }))).toEqual([ | ||
| { noteId: 'early-note', operation: 'create', clientUpdatedAt: '2026-01-01T00:00:02Z', status: 'pending' }, | ||
| { noteId: 'middle-note', operation: 'delete', clientUpdatedAt: '2026-01-01T00:00:05Z', status: 'pending' }, | ||
| { noteId: 'late-note', operation: 'update', clientUpdatedAt: '2026-01-01T00:00:10Z', status: 'pending' }, | ||
| ]) | ||
| }) | ||
|
|
||
| it('drops an unsupported runtime operation instead of emitting an invalid queue item', () => { | ||
| const unsupported = item( | ||
| 'unsupported', | ||
| 'note', | ||
| 'archive' as MutationOperation, | ||
| '2026-01-01T00:00:01Z', | ||
| ) | ||
|
|
||
| expect(compactQueue([unsupported])).toEqual([]) | ||
| }) | ||
| }) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { compactQueue } from '../../utils/compactQueue' | ||
| import type { MutationQueueItem } from '../../types/offline' | ||
|
|
||
| const item = (id: string, noteId: string, operation: 'create' | 'update' | 'delete', time: string, payload = {}) : MutationQueueItem => ({ | ||
| id, noteId, operation, payload, clientUpdatedAt: time, status: 'failed', attempts: 1, | ||
| }) | ||
|
|
||
| describe('compactQueue state transitions', () => { | ||
| it('removes create/delete pairs and keeps a standalone delete pending', () => { | ||
| expect(compactQueue([ | ||
| item('1', 'n1', 'create', '2026-01-01T00:00:00Z'), item('2', 'n1', 'delete', '2026-01-01T00:00:01Z'), | ||
| ])).toEqual([]) | ||
| const result = compactQueue([item('3', 'n2', 'delete', '2026-01-01T00:00:00Z')]) | ||
| expect(result).toHaveLength(1) | ||
| expect(result[0]).toMatchObject({ operation: 'delete', status: 'pending' }) | ||
| }) | ||
|
|
||
| it('collapses create/update and update/update to the latest payload', () => { | ||
| const created = compactQueue([ | ||
| item('1', 'n1', 'create', '2026-01-01T00:00:00Z', { title: 'old' }), | ||
| item('2', 'n1', 'update', '2026-01-01T00:00:01Z', { title: 'new' }), | ||
| ]) | ||
| expect(created).toMatchObject([{ operation: 'create', payload: { title: 'new' }, status: 'pending', id: '2' }]) | ||
| const updated = compactQueue([ | ||
| item('1', 'n2', 'update', '2026-01-01T00:00:00Z', { title: 'old' }), | ||
| item('2', 'n2', 'update', '2026-01-01T00:00:01Z', { title: 'new' }), | ||
| ]) | ||
| expect(updated).toMatchObject([{ operation: 'update', payload: { title: 'new' }, status: 'pending' }]) | ||
| }) | ||
| }) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.