const mockCreateMeetingWorkspace = jest.fn() const mockGetMeetingDetail = jest.fn() const mockCreateMeetingMemo = jest.fn() const mockSearchKnowledge = jest.fn() jest.mock('../src/features/meetings/meetings-service', () => { class MeetingServiceError extends Error { public readonly code: string constructor(errorCode: string, message?: string) { super(message ?? errorCode) this.code = errorCode this.name = 'MeetingServiceError' } } return { createMeetingWorkspace: (...args: unknown[]) => mockCreateMeetingWorkspace(...args), getMeetingDetail: (...args: unknown[]) => mockGetMeetingDetail(...args), createMeetingMemo: (...args: unknown[]) => mockCreateMeetingMemo(...args), MeetingServiceError, } }) jest.mock('../src/features/knowledge/knowledge-service', () => { class KnowledgeServiceError extends Error { public readonly code: string public readonly retryable: boolean public readonly status: number | null constructor( errorCode: string, retryable: boolean, status: number | null = null, ) { super(errorCode) this.code = errorCode this.retryable = retryable this.status = status this.name = 'KnowledgeServiceError' } } return { searchKnowledge: (...args: unknown[]) => mockSearchKnowledge(...args), KnowledgeServiceError, } }) import AsyncStorage from '@react-native-async-storage/async-storage' import { ActionServiceError, executeConfirmedAction, listActionHistory, parseActionCommand, parseActionJson, } from '../src/features/actions/action-service' const USER_ID = '11111111-1111-4111-8111-111111111111' const MEETING_ID = 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa' const MEMO_ID = 'bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb' beforeEach(async () => { jest.restoreAllMocks() mockCreateMeetingWorkspace.mockReset() mockGetMeetingDetail.mockReset() mockCreateMeetingMemo.mockReset() mockSearchKnowledge.mockReset() await AsyncStorage.clear() }) describe('action parser contract', () => { it('accepts only strictly validated allowlisted JSON', () => { expect(parseActionJson(JSON.stringify({ type: 'create_meeting', args: { title: ' Product review ' }, rationale: 'The user explicitly requested a meeting workspace.', }))).toEqual({ type: 'create_meeting', args: { title: 'Product review' }, rationale: 'The user explicitly requested a meeting workspace.', executable: true, }) expect(parseActionJson(JSON.stringify({ type: 'search_knowledge', args: { query: 'release policy' }, rationale: 'This is a read-only knowledge search.', }))).toMatchObject({ type: 'search_knowledge', executable: true }) }) it('blocks external invitations and unknown commands after parsing', () => { expect(parseActionJson(JSON.stringify({ type: 'send_team_invite', args: { team_id: MEETING_ID, email: 'member@example.org', role: 'member' }, rationale: 'The user asked for a team invitation.', }))).toMatchObject({ type: 'send_team_invite', executable: false, blockReason: 'external-side-effect', }) expect(parseActionJson(JSON.stringify({ type: 'unknown', args: {}, rationale: 'The request lacks a supported action.', }))).toMatchObject({ executable: false, blockReason: 'unknown-action' }) }) it('rejects markdown fences, prose, extra keys, and invented missing identifiers', () => { const validObject = '{"type":"unknown","args":{},"rationale":"Unsupported"}' expect(() => parseActionJson(`\`\`\`json\n${validObject}\n\`\`\``)) .toThrow(ActionServiceError) expect(() => parseActionJson(`${validObject}\nThis is the result.`)) .toThrow(ActionServiceError) expect(() => parseActionJson(JSON.stringify({ type: 'create_meeting', args: { title: 'Review', hidden: 'side effect' }, rationale: 'Create it.', }))).toThrow(ActionServiceError) expect(() => parseActionJson(JSON.stringify({ type: 'create_memo', args: { content: 'Memo without meeting id' }, rationale: 'Missing the required ID.', }))).toThrow(ActionServiceError) }) it('calls authenticated llm-proxy and rejects a multi-block response', async () => { const fetchMock = jest.spyOn(global, 'fetch').mockResolvedValue(new Response(JSON.stringify({ content: [ { type: 'text', text: '{"type":"unknown","args":{},"rationale":"one"}' }, { type: 'text', text: '{"type":"unknown","args":{},"rationale":"two"}' }, ], }), { status: 200, headers: { 'X-D3RO-Generation-Id': '22222222-2222-4222-8222-222222222222' }, })) await expect(parseActionCommand('Do something', { accessToken: 'access-token' })) .rejects.toMatchObject({ code: 'INVALID_RESPONSE' }) expect(fetchMock).toHaveBeenCalledWith( expect.stringContaining('/functions/v1/llm-proxy'), expect.objectContaining({ method: 'POST' }), ) const request = fetchMock.mock.calls[0][1] as RequestInit expect((request.headers as Record).Authorization).toBe('Bearer access-token') expect((request.headers as Record)['X-D3RO-Generation-Purpose']) .toBe('action_response') expect(JSON.parse(String(request.body))).toEqual(expect.objectContaining({ stream: false, max_tokens: 512, })) }) it('returns a reportable generation receipt with the exact parsed action snapshot', async () => { const snapshot = JSON.stringify({ type: 'unknown', args: {}, rationale: 'This action is not supported.', }) jest.spyOn(global, 'fetch').mockResolvedValue(new Response(JSON.stringify({ content: [{ type: 'text', text: snapshot }], }), { status: 200, headers: { 'X-D3RO-Generation-Id': '22222222-2222-4222-8222-222222222222' }, })) await expect(parseActionCommand('Do something', { accessToken: 'access-token' })) .resolves.toMatchObject({ generationId: '22222222-2222-4222-8222-222222222222', snapshot, action: { type: 'unknown', executable: false }, }) }) }) describe('confirmed action execution and local audit history', () => { it('never executes an allowed action without an explicit confirmation flag', async () => { const action = parseActionJson(JSON.stringify({ type: 'create_meeting', args: { title: 'Review' }, rationale: 'Create a meeting.', })) await expect(executeConfirmedAction({ userId: USER_ID, accessToken: 'access-token', input: 'Create a review meeting', action, confirmed: false, })).rejects.toMatchObject({ code: 'ACTION_BLOCKED' }) expect(mockCreateMeetingWorkspace).not.toHaveBeenCalled() }) it('revalidates a caller-forged action object before any side effect', async () => { const forged = { type: 'create_meeting', args: { title: 'Review', hidden: 'unreviewed' }, rationale: 'Create a meeting.', executable: true, } as unknown as ReturnType await expect(executeConfirmedAction({ userId: USER_ID, accessToken: 'access-token', input: 'Create a review meeting', action: forged, confirmed: true, })).rejects.toMatchObject({ code: 'INVALID_REQUEST' }) expect(mockCreateMeetingWorkspace).not.toHaveBeenCalled() }) it('creates a real meeting and records the confirmed result per user', async () => { mockCreateMeetingWorkspace.mockResolvedValue({ id: MEETING_ID, title: 'Review', }) const action = parseActionJson(JSON.stringify({ type: 'create_meeting', args: { title: 'Review' }, rationale: 'Create a meeting workspace.', })) const outcome = await executeConfirmedAction({ userId: USER_ID, accessToken: 'access-token', input: 'Create a review meeting', action, confirmed: true, nowMs: 1_777_000_000_000, }) expect(mockCreateMeetingWorkspace).toHaveBeenCalledWith(USER_ID, 'Review') expect(outcome).toMatchObject({ historyRecorded: true, result: { kind: 'meeting-created', meetingId: MEETING_ID, title: 'Review' }, record: { status: 'succeeded', type: 'create_meeting' }, }) await expect(listActionHistory(USER_ID)).resolves.toEqual([ expect.objectContaining({ status: 'succeeded', type: 'create_meeting' }), ]) }) it('executes knowledge search through the server contract and records only the count', async () => { mockSearchKnowledge.mockResolvedValue([{ id: 'cccccccc-cccc-4ccc-8ccc-cccccccccccc', documentId: 'dddddddd-dddd-4ddd-8ddd-dddddddddddd', chunkIndex: 0, content: 'Private knowledge result', similarity: 0.91, }]) const action = parseActionJson(JSON.stringify({ type: 'search_knowledge', args: { query: 'release policy' }, rationale: 'Search is read only.', })) const outcome = await executeConfirmedAction({ userId: USER_ID, accessToken: 'access-token', input: 'Search release policy', action, confirmed: true, nowMs: 1_777_000_000_001, }) expect(outcome.result).toMatchObject({ kind: 'knowledge-searched' }) expect(outcome.record.result).toEqual({ kind: 'knowledge-searched', query: 'release policy', result_count: 1, }) expect(JSON.stringify(outcome.record.result)).not.toContain('Private knowledge result') }) it('persists a failed memo execution without reporting success', async () => { mockGetMeetingDetail.mockRejectedValue(new Error('server refused meeting lookup')) const action = parseActionJson(JSON.stringify({ type: 'create_memo', args: { meeting_id: MEETING_ID, content: 'Decision recorded' }, rationale: 'Save the decision to this meeting.', })) await expect(executeConfirmedAction({ userId: USER_ID, accessToken: 'access-token', input: 'Save the decision', action, confirmed: true, nowMs: 1_777_000_000_002, })).rejects.toMatchObject({ code: 'SERVER_ERROR', historyRecorded: true }) await expect(listActionHistory(USER_ID)).resolves.toEqual([ expect.objectContaining({ status: 'failed', type: 'create_memo', result: null, errorCode: 'SERVER_ERROR', }), ]) expect(mockCreateMeetingMemo).not.toHaveBeenCalled() }) it('returns a completed side effect with an explicit history failure instead of inviting a duplicate retry', async () => { mockCreateMeetingWorkspace.mockResolvedValue({ id: MEETING_ID, title: 'Review' }) jest.spyOn(AsyncStorage, 'setItem').mockRejectedValueOnce(new Error('disk unavailable')) const action = parseActionJson(JSON.stringify({ type: 'create_meeting', args: { title: 'Review' }, rationale: 'Create a meeting workspace.', })) await expect(executeConfirmedAction({ userId: USER_ID, accessToken: 'access-token', input: 'Create a review meeting', action, confirmed: true, nowMs: 1_777_000_000_003, })).resolves.toMatchObject({ historyRecorded: false, result: { kind: 'meeting-created', meetingId: MEETING_ID }, }) expect(mockCreateMeetingWorkspace).toHaveBeenCalledTimes(1) }) it('creates a memo only after loading the RLS-visible meeting', async () => { const meeting = { id: MEETING_ID, started_at: '2026-08-21T00:00:00.000Z' } mockGetMeetingDetail.mockResolvedValue({ meeting }) mockCreateMeetingMemo.mockResolvedValue({ id: MEMO_ID, meeting_id: MEETING_ID, content: 'Decision recorded', }) const action = parseActionJson(JSON.stringify({ type: 'create_memo', args: { meeting_id: MEETING_ID, content: 'Decision recorded' }, rationale: 'Save the decision to this meeting.', })) await expect(executeConfirmedAction({ userId: USER_ID, accessToken: 'access-token', input: 'Save the decision', action, confirmed: true, nowMs: 1_777_000_000_004, })).resolves.toMatchObject({ result: { kind: 'memo-created', memoId: MEMO_ID }, }) expect(mockGetMeetingDetail).toHaveBeenCalledWith(USER_ID, MEETING_ID) expect(mockCreateMeetingMemo).toHaveBeenCalledWith( USER_ID, meeting, 'Decision recorded', 1_777_000_000_004, ) }) })