test(sync): cover the archived session query boundary
Add coverage for the archived-only narrowing: mixed pages return archived records only, active pages stay unfiltered, a page that is full upstream but fully filtered out keeps paginating, onPage receives only accepted records, and id dedupe still stops a repeating page. Document the OpenCode `archived` flag semantics in the owning store documentation.
This commit is contained in:
@@ -56,6 +56,7 @@ User-visible session ordering is also not owned by the global cache array order.
|
||||
|
||||
Global refresh rules:
|
||||
|
||||
- The OpenCode `archived` list flag means "also include archived sessions": the server only drops its `time_archived IS NULL` condition. `listGlobalSessionPages` therefore narrows archived requests to records carrying `time.archived`, at the data boundary, so the archived cache never holds active sessions and no consumer has to re-derive that. Pagination progress stays measured on the raw response, so a page that is full upstream but filtered out here is not mistaken for the last page.
|
||||
- Per-directory refresh is bounded to two requests across callers and prioritizes the current directory.
|
||||
- Each directory is an independent completeness scope. A failed directory preserves its previous sessions while successful directories reconcile normally.
|
||||
- Fetch failure must remain distinguishable from a successful empty list; failed scopes cannot destructively clear cached sessions.
|
||||
|
||||
@@ -96,6 +96,164 @@ describe('listGlobalSessionPages', () => {
|
||||
expect(sessions.map((session) => session.id)).toEqual(['ses_root', 'ses_child_1', 'ses_child_2'])
|
||||
})
|
||||
|
||||
test('returns only archived sessions when archived pages are requested', async () => {
|
||||
const apiClient = {
|
||||
experimental: {
|
||||
session: {
|
||||
list: async () => ({
|
||||
// The server treats `archived: true` as "include archived", so the
|
||||
// response mixes active and archived records.
|
||||
data: [
|
||||
{ id: 'ses_active', time: { created: 1, updated: 20 } },
|
||||
{ id: 'ses_archived', time: { created: 1, updated: 10, archived: 15 } },
|
||||
],
|
||||
response: { headers: new Headers() },
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as unknown as OpencodeClient
|
||||
|
||||
const sessions = await listGlobalSessionPages(apiClient, { archived: true, pageSize: 500 })
|
||||
|
||||
expect(sessions.map((session) => session.id)).toEqual(['ses_archived'])
|
||||
})
|
||||
|
||||
test('keeps every record when active pages are requested', async () => {
|
||||
const apiClient = {
|
||||
experimental: {
|
||||
session: {
|
||||
list: async () => ({
|
||||
data: [
|
||||
{ id: 'ses_active_1', time: { updated: 20 } },
|
||||
{ id: 'ses_active_2', time: { updated: 10 } },
|
||||
],
|
||||
response: { headers: new Headers() },
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as unknown as OpencodeClient
|
||||
|
||||
const sessions = await listGlobalSessionPages(apiClient, { archived: false, pageSize: 500 })
|
||||
|
||||
expect(sessions.map((session) => session.id)).toEqual(['ses_active_1', 'ses_active_2'])
|
||||
})
|
||||
|
||||
test('keeps paginating archived pages that are full of non-archived records', async () => {
|
||||
const calls: Array<Record<string, unknown>> = []
|
||||
const apiClient = {
|
||||
experimental: {
|
||||
session: {
|
||||
list: async (options: Record<string, unknown>) => {
|
||||
calls.push(options)
|
||||
if (options.cursor === undefined) {
|
||||
return {
|
||||
data: [
|
||||
{ id: 'ses_active_1', time: { updated: 30 } },
|
||||
{ id: 'ses_active_2', time: { updated: 20 } },
|
||||
],
|
||||
response: { headers: new Headers({ 'x-next-cursor': '20' }) },
|
||||
}
|
||||
}
|
||||
return {
|
||||
data: [
|
||||
{ id: 'ses_archived', time: { updated: 10, archived: 12 } },
|
||||
],
|
||||
response: { headers: new Headers() },
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpencodeClient
|
||||
|
||||
const sessions = await listGlobalSessionPages(apiClient, { archived: true, pageSize: 2 })
|
||||
|
||||
// A page that is full upstream but fully filtered out here must not be
|
||||
// mistaken for the last page: pagination progress is measured on the raw
|
||||
// response, not on the accepted records.
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(sessions.map((session) => session.id)).toEqual(['ses_archived'])
|
||||
})
|
||||
|
||||
test('reports only accepted records to onPage for archived pages', async () => {
|
||||
const pages: string[][] = []
|
||||
const apiClient = {
|
||||
experimental: {
|
||||
session: {
|
||||
list: async () => ({
|
||||
data: [
|
||||
{ id: 'ses_active', time: { updated: 20 } },
|
||||
{ id: 'ses_archived', time: { updated: 10, archived: 12 } },
|
||||
],
|
||||
response: { headers: new Headers() },
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as unknown as OpencodeClient
|
||||
|
||||
await listGlobalSessionPages(apiClient, {
|
||||
archived: true,
|
||||
pageSize: 500,
|
||||
onPage: (sessions) => pages.push(sessions.map((session) => session.id)),
|
||||
})
|
||||
|
||||
expect(pages).toEqual([['ses_archived']])
|
||||
})
|
||||
|
||||
test('does not notify onPage for an archived page with no archived records', async () => {
|
||||
const pages: string[][] = []
|
||||
const apiClient = {
|
||||
experimental: {
|
||||
session: {
|
||||
list: async () => ({
|
||||
data: [{ id: 'ses_active', time: { updated: 20 } }],
|
||||
response: { headers: new Headers() },
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as unknown as OpencodeClient
|
||||
|
||||
const sessions = await listGlobalSessionPages(apiClient, {
|
||||
archived: true,
|
||||
pageSize: 500,
|
||||
onPage: (page) => pages.push(page.map((session) => session.id)),
|
||||
})
|
||||
|
||||
expect(sessions).toEqual([])
|
||||
expect(pages).toEqual([])
|
||||
})
|
||||
|
||||
test('dedupes archived records by id and stops when a page repeats known ids', async () => {
|
||||
const calls: Array<Record<string, unknown>> = []
|
||||
const page = [
|
||||
{ id: 'ses_archived_1', time: { updated: 30, archived: 31 } },
|
||||
{ id: 'ses_archived_2', time: { updated: 20, archived: 21 } },
|
||||
]
|
||||
const apiClient = {
|
||||
experimental: {
|
||||
session: {
|
||||
list: async (options: Record<string, unknown>) => {
|
||||
calls.push(options)
|
||||
return {
|
||||
data: page,
|
||||
response: {
|
||||
headers: new Headers({
|
||||
'x-next-cursor': options.cursor === undefined ? '20' : '10',
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
} as unknown as OpencodeClient
|
||||
|
||||
const sessions = await listGlobalSessionPages(apiClient, { archived: true, pageSize: 2 })
|
||||
|
||||
// The second page repeats ids already seen, so the dedupe guard stops the
|
||||
// loop and no record is returned twice.
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(sessions.map((session) => session.id)).toEqual(['ses_archived_1', 'ses_archived_2'])
|
||||
})
|
||||
|
||||
test('retries SDK error responses before treating the load as failed', async () => {
|
||||
let calls = 0
|
||||
const apiClient = {
|
||||
|
||||
Reference in New Issue
Block a user