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:
|
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.
|
- 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.
|
- 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.
|
- 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'])
|
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 () => {
|
test('retries SDK error responses before treating the load as failed', async () => {
|
||||||
let calls = 0
|
let calls = 0
|
||||||
const apiClient = {
|
const apiClient = {
|
||||||
|
|||||||
Reference in New Issue
Block a user