Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cc04846
ci: add team-gated Cursor review (thin caller for github-workflows)
mattmillerai Jun 15, 2026
4cadbb8
fix: resolve review feedback
mattmillerai Jun 15, 2026
e498c4a
ci: bump cursor-review SHA to github-workflows#9, drop judge_model ov…
mattmillerai Jun 17, 2026
880582a
[automated] Apply ESLint and Oxfmt fixes
actions-user Jun 17, 2026
8d23daa
fix: resolve review feedback
mattmillerai Jun 17, 2026
1fee449
Merge branch 'main' into ci/cursor-review-workflow
mattmillerai Jun 17, 2026
5ef89c7
feat(assets): adopt cursor pagination in the Generated tab jobs walk
mattmillerai Jun 10, 2026
1f810a1
fix(assets): harden cursor pagination against review findings
mattmillerai Jun 10, 2026
69a4d78
fix(assets): guard the history jobs walk against a non-advancing cursor
mattmillerai Jun 16, 2026
f318718
docs(assets): add JSDoc to assetsStore per review
mattmillerai Jun 17, 2026
753b0b4
fix(assets): harden epoch guards and truncate JobsApiError body
mattmillerai Jun 19, 2026
4290619
[automated] Apply ESLint and Oxfmt fixes
actions-user Jun 19, 2026
c207f56
Merge branch 'main' into matt/fe-962-fe-adopt-cursor-pagination-in-th…
mattmillerai Jun 19, 2026
89fdbcd
Update src/stores/assetsStore.ts
mattmillerai Jun 22, 2026
c238145
Update src/platform/remote/comfyui/jobs/jobTypes.ts
mattmillerai Jun 22, 2026
6f7686b
Update src/platform/remote/comfyui/jobs/fetchJobs.ts
mattmillerai Jun 22, 2026
6c3ead5
Update src/stores/assetsStore.ts
mattmillerai Jun 22, 2026
ea0f8a9
test: add body-truncation coverage and historyError-null assertion fo…
mattmillerai Jun 22, 2026
8b40f6a
fix: reset offset to 0 on cursor-recovery fallback to prevent page drift
mattmillerai Jun 22, 2026
e471b64
Merge remote-tracking branch 'origin/main' into HEAD
mattmillerai Jun 22, 2026
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
2 changes: 1 addition & 1 deletion src/components/queue/QueueProgressOverlay.vue
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ const focusAssetInSidebar = async (item: JobListItem) => {
const assetId = String(jobId)
openAssetsSidebar()
await nextTick()
await assetsStore.updateHistory()
await assetsStore.refreshHistoryHead()
const asset = assetsStore.historyAssets.find(
(existingAsset) => existingAsset.id === assetId
)
Expand Down
4 changes: 2 additions & 2 deletions src/platform/missingMedia/missingMediaAssetResolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,13 @@ describe('resolveMissingMediaAssetSources', () => {
1,
expect.any(Function),
200,
0
{ offset: 0 }
)
expect(mockFetchHistoryPage).toHaveBeenNthCalledWith(
2,
expect.any(Function),
200,
200
{ offset: 200 }
)
})

Expand Down
2 changes: 1 addition & 1 deletion src/platform/missingMedia/missingMediaAssetResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ async function fetchGeneratedHistoryAssets(
const historyPage = await fetchHistoryPage(
api.fetchApi.bind(api),
HISTORY_MEDIA_ASSETS_PAGE_SIZE,
requestedOffset
{ offset: requestedOffset }
Comment thread
mattmillerai marked this conversation as resolved.
)

signal?.throwIfAborted()
Expand Down
6 changes: 3 additions & 3 deletions src/platform/missingMedia/missingMediaScan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -709,7 +709,7 @@ describe('verifyMediaCandidates', () => {
expect(mockFetchHistoryPage).toHaveBeenCalledWith(
expect.any(Function),
200,
0
{ offset: 0 }
)
expect(candidates[0]).toMatchObject({
name: 'subfolder/photo.png [output]',
Expand Down Expand Up @@ -843,13 +843,13 @@ describe('verifyMediaCandidates', () => {
1,
expect.any(Function),
200,
0
{ offset: 0 }
)
expect(mockFetchHistoryPage).toHaveBeenNthCalledWith(
2,
expect.any(Function),
200,
200
{ offset: 200 }
)
expect(candidates[0].isMissing).toBe(false)
})
Expand Down
133 changes: 120 additions & 13 deletions src/platform/remote/comfyui/jobs/fetchJobs.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from 'vitest'

import {
JobsApiError,
extractWorkflow,
fetchHistory,
fetchHistoryPage,
Expand Down Expand Up @@ -39,7 +40,8 @@ function createMockResponse(
offset: pagination.offset ?? 0,
limit: pagination.limit ?? 200,
total,
has_more: pagination.has_more ?? false
has_more: pagination.has_more ?? false,
next_cursor: pagination.next_cursor
}
}
}
Expand Down Expand Up @@ -135,23 +137,57 @@ describe('fetchJobs', () => {
expect(result[0].priority).toBe(999)
})

it('returns empty array on error', async () => {
it('propagates fetch errors', async () => {
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'))

const result = await fetchHistory(mockFetch)
await expect(fetchHistory(mockFetch)).rejects.toThrow('Network error')
})

it('throws a JobsApiError carrying status and body on non-ok response', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: false,
status: 400,
text: () =>
Promise.resolve('{"error":"Invalid cursor","code":"INVALID_CURSOR"}')
})

expect(result).toEqual([])
await expect(fetchHistory(mockFetch)).rejects.toBeInstanceOf(JobsApiError)
await expect(fetchHistory(mockFetch)).rejects.toMatchObject({
status: 400,
message: expect.stringContaining('INVALID_CURSOR')
})
})

it('returns empty array on non-ok response', async () => {
it('truncates oversized error bodies to 200 chars in the thrown message', async () => {
const oversized = 'x'.repeat(500)
const mockFetch = vi.fn().mockResolvedValue({
ok: false,
status: 500
status: 500,
text: () => Promise.resolve(oversized)
})

const result = await fetchHistory(mockFetch)
const err = await fetchHistory(mockFetch).catch((e) => e)
expect(err).toBeInstanceOf(JobsApiError)
expect(err.message.length).toBeLessThanOrEqual(
'[Jobs API] Failed to fetch jobs: 500 '.length + 200 + 1 // +1 for the ellipsis
)
expect(err.message).toContain('…')
})

expect(result).toEqual([])
it('parses a null next_cursor as absent', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve(
createMockResponse([createMockJob('job1', 'completed')], 1, {
next_cursor: null
})
)
})

const result = await fetchHistoryPage(mockFetch, 200, { offset: 0 })

expect(result.nextCursor).toBeUndefined()
})

it('parses batch containing text-only preview outputs', async () => {
Expand Down Expand Up @@ -205,7 +241,7 @@ describe('fetchJobs', () => {
)
})

const result = await fetchHistoryPage(mockFetch, 2, 5)
const result = await fetchHistoryPage(mockFetch, 2, { offset: 5 })

expect(mockFetch).toHaveBeenCalledWith(
'/jobs?status=completed,failed,cancelled&limit=2&offset=5'
Expand All @@ -218,6 +254,79 @@ describe('fetchJobs', () => {
expect(result.jobs[0].priority).toBe(5)
expect(result.jobs[1].priority).toBe(4)
})

it('sends the cursor instead of offset and returns next_cursor', async () => {
const mockFetch = vi
.fn<(url: string) => Promise<Response>>()
.mockResolvedValue(
new Response(
JSON.stringify(
createMockResponse([createMockJob('job1', 'completed')], 10, {
has_more: true,
next_cursor: 'cursor-page-2'
})
),
{ status: 200 }
)
)

const result = await fetchHistoryPage(mockFetch, 200, {
after: 'cursor-page-1'
})

expect(mockFetch).toHaveBeenCalledWith(
'/jobs?status=completed,failed,cancelled&limit=200&after=cursor-page-1'
)
expect(result.nextCursor).toBe('cursor-page-2')
expect(result.hasMore).toBe(true)
})

it('uri-encodes the cursor', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(createMockResponse([]))
})

await fetchHistoryPage(mockFetch, 200, { after: 'a+b/c=' })

expect(mockFetch).toHaveBeenCalledWith(
'/jobs?status=completed,failed,cancelled&limit=200&after=a%2Bb%2Fc%3D'
)
})

it('returns next_cursor from offset-mode responses for cursor bootstrap', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve(
createMockResponse([createMockJob('job1', 'completed')], 10, {
has_more: true,
next_cursor: 'minted-in-offset-mode'
})
)
})

const result = await fetchHistoryPage(mockFetch, 200, { offset: 0 })

expect(mockFetch).toHaveBeenCalledWith(
'/jobs?status=completed,failed,cancelled&limit=200&offset=0'
)
expect(result.nextCursor).toBe('minted-in-offset-mode')
})

it('omits nextCursor when the server does not mint one', async () => {
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
json: () =>
Promise.resolve(
createMockResponse([createMockJob('job1', 'completed')])
)
})

const result = await fetchHistoryPage(mockFetch, 200, { offset: 0 })

expect(result.nextCursor).toBeUndefined()
})
})

describe('fetchQueue', () => {
Expand Down Expand Up @@ -268,12 +377,10 @@ describe('fetchJobs', () => {
)
})

it('returns empty arrays on error', async () => {
it('propagates fetch errors', async () => {
const mockFetch = vi.fn().mockRejectedValue(new Error('Network error'))

const result = await fetchQueue(mockFetch)

expect(result).toEqual({ Running: [], Pending: [] })
await expect(fetchQueue(mockFetch)).rejects.toThrow('Network error')
})
})

Expand Down
Loading
Loading