diff --git a/src/lib/local/local-chat.test.ts b/src/lib/local/local-chat.test.ts index a4cc7122..cb3dfb8f 100644 --- a/src/lib/local/local-chat.test.ts +++ b/src/lib/local/local-chat.test.ts @@ -416,6 +416,22 @@ describe('local query adapter surface', () => { } }); + it('reports contract and analytics as unavailable rather than inventing them', async () => { + const { store } = await seedConversations(1); + const adapter = createLocalQueryAdapter(store); + + // An empty contract would read as "this familiar is permitted nothing", + // which is a different claim from "nobody asked Cave". + await expect(adapter.familiarContract(LOCAL_FAMILIAR_ID)).resolves.toEqual({ + status: 'error', + code: 'service_unavailable', + }); + await expect(adapter.familiarAnalytics(LOCAL_FAMILIAR_ID)).resolves.toEqual({ + status: 'error', + code: 'service_unavailable', + }); + }); + it('never emits loading, stale, or reconcile_required', async () => { const { store } = await seedConversations(1); const adapter = createLocalQueryAdapter(store); @@ -427,6 +443,8 @@ describe('local query adapter surface', () => { adapter.listConversations(), adapter.getConversation('missing'), adapter.listMessages('missing'), + adapter.familiarContract('missing'), + adapter.familiarAnalytics('missing'), ]) ).map((result) => result.status); diff --git a/src/lib/local/local-query-adapter.ts b/src/lib/local/local-query-adapter.ts index 6b969c1e..04d3d21d 100644 --- a/src/lib/local/local-query-adapter.ts +++ b/src/lib/local/local-query-adapter.ts @@ -1,3 +1,4 @@ +import type { CaveFamiliarAnalytics, CaveFamiliarContract } from '@opencoven/cave-client'; import type { CaveCanonicalFamiliar, CaveConversation, @@ -32,6 +33,19 @@ const INVALID_REQUEST_RESULT = Object.freeze({ code: 'invalid_request', } as const); +/** + * A familiar's contract and its execution analytics are Cave's to report. They + * describe what a familiar has been granted and what it has since done, and a + * device holding only local conversations has neither record. Standalone mode + * answers `service_unavailable` rather than inventing an empty contract, which + * would read as "this familiar is permitted nothing" instead of "nobody asked + * Cave". + */ +const CAVE_ONLY_RESULT = Object.freeze({ + status: 'error', + code: 'service_unavailable', +} as const); + const EMPTY_PAGE = Object.freeze({ data: Object.freeze([]), cursor: Object.freeze({ hasMore: false }), @@ -196,6 +210,14 @@ export function createLocalQueryAdapter(store: ChatStore): QueryAdapter { }); }, + familiarContract() { + return guard(() => CAVE_ONLY_RESULT); + }, + + familiarAnalytics() { + return guard(() => CAVE_ONLY_RESULT); + }, + invalidate() { // Reads go straight to the in-memory index, so there is nothing stale to // drop. Kept to satisfy the interface the shell calls on refresh.