diff --git a/mobile/src/api/intake.test.ts b/mobile/src/api/intake.test.ts
new file mode 100644
index 0000000..eac02eb
--- /dev/null
+++ b/mobile/src/api/intake.test.ts
@@ -0,0 +1,61 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+const fetchMock = vi.fn();
+
+vi.mock('./client', () => ({
+ getApiClient: () => ({
+ fetch: fetchMock,
+ }),
+}));
+
+import { getIntakeJob, listIntakeJobs, listIntakeRules, submitIntake } from './intake';
+
+describe('mobile intake API client', () => {
+ beforeEach(() => {
+ fetchMock.mockReset();
+ });
+
+ it('submits URL intake with optional workspace and template override', async () => {
+ fetchMock.mockResolvedValueOnce({
+ jobId: 'job-1',
+ noteId: 'note-1',
+ contentType: 'article',
+ ruleMatched: null,
+ templateSlug: 'summarize',
+ status: 'queued',
+ });
+
+ await submitIntake('https://example.com/post', 'ws-1', 'summarize');
+
+ expect(fetchMock).toHaveBeenCalledWith(
+ '/intake',
+ expect.objectContaining({
+ method: 'POST',
+ body: JSON.stringify({
+ url: 'https://example.com/post',
+ workspaceId: 'ws-1',
+ templateOverride: 'summarize',
+ }),
+ }),
+ );
+ });
+
+ it('lists active intake jobs with query filters', async () => {
+ fetchMock.mockResolvedValueOnce({ items: [{ id: 'job-1', status: 'processing' }], total: 1 });
+
+ const jobs = await listIntakeJobs({ status: 'queued,processing', since: '2026-05-05T00:00:00Z', limit: 10 });
+
+ expect(jobs).toEqual([{ id: 'job-1', status: 'processing' }]);
+ expect(fetchMock).toHaveBeenCalledWith('/intake/jobs?status=queued%2Cprocessing&since=2026-05-05T00%3A00%3A00Z&limit=10');
+ });
+
+ it('gets one intake job and lists rules', async () => {
+ fetchMock.mockResolvedValueOnce({ id: 'job-1', status: 'complete' });
+ await expect(getIntakeJob('job/1')).resolves.toEqual({ id: 'job-1', status: 'complete' });
+ expect(fetchMock).toHaveBeenCalledWith('/intake/jobs/job%2F1');
+
+ fetchMock.mockResolvedValueOnce({ items: [{ id: 'rule-1', name: 'Articles' }], total: 1 });
+ await expect(listIntakeRules()).resolves.toEqual([{ id: 'rule-1', name: 'Articles' }]);
+ expect(fetchMock).toHaveBeenCalledWith('/intake-rules');
+ });
+});
diff --git a/mobile/src/api/note-agent-actions.test.ts b/mobile/src/api/note-agent-actions.test.ts
new file mode 100644
index 0000000..2c0a293
--- /dev/null
+++ b/mobile/src/api/note-agent-actions.test.ts
@@ -0,0 +1,94 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+const { fetchMock, listWorkspacesMock } = vi.hoisted(() => ({
+ fetchMock: vi.fn(),
+ listWorkspacesMock: vi.fn(),
+}));
+
+vi.mock('./client', () => ({
+ getApiClient: () => ({
+ fetch: fetchMock,
+ }),
+}));
+
+vi.mock('./workspaces', () => ({
+ listWorkspaces: listWorkspacesMock,
+}));
+
+import { listActivityFeed, listApprovalQueue, updateApprovalState } from './note-agent-actions';
+
+const proposedAction = {
+ id: 'a1',
+ workspaceId: 'ws-1',
+ noteId: 'n1',
+ actorId: 'agent',
+ actionType: 'extract_tasks' as const,
+ state: 'proposed' as const,
+ reason: 'Extract tasks',
+ updatedAt: '2026-05-05T12:00:00Z',
+};
+
+const approvedAction = {
+ ...proposedAction,
+ id: 'a2',
+ actionType: 'attach_citation' as const,
+ state: 'approved' as const,
+ afterSummary: 'Citation attached',
+ updatedAt: '2026-05-05T10:00:00Z',
+};
+
+describe('mobile note agent actions API client', () => {
+ beforeEach(() => {
+ fetchMock.mockReset();
+ listWorkspacesMock.mockReset();
+ listWorkspacesMock.mockResolvedValue([{ id: 'ws-1', name: 'Research' }, { id: 'ws-2', name: 'Inbox' }]);
+ });
+
+ it('builds the approval queue from draft and proposed agent actions across workspaces', async () => {
+ fetchMock.mockResolvedValueOnce({ items: [approvedAction, proposedAction] });
+ fetchMock.mockResolvedValueOnce({ items: [] });
+
+ const approvals = await listApprovalQueue();
+
+ expect(approvals).toEqual([
+ {
+ id: 'a1',
+ workspaceId: 'ws-1',
+ noteId: 'n1',
+ title: 'Extract tasks',
+ summary: 'Extract tasks',
+ status: 'pending',
+ },
+ ]);
+ expect(fetchMock).toHaveBeenCalledWith('/note-agent-actions?workspaceId=ws-1');
+ expect(fetchMock).toHaveBeenCalledWith('/note-agent-actions?workspaceId=ws-2');
+ });
+
+ it('maps recent activity kinds for inbox activity feed', async () => {
+ fetchMock.mockResolvedValueOnce({ items: [proposedAction, approvedAction] });
+ fetchMock.mockResolvedValueOnce({ items: [] });
+
+ const activity = await listActivityFeed();
+
+ expect(activity.map((item) => ({ id: item.id, kind: item.kind }))).toEqual([
+ { id: 'a1', kind: 'task' },
+ { id: 'a2', kind: 'note' },
+ ]);
+ });
+
+ it('updates approval state with review note', async () => {
+ fetchMock.mockResolvedValueOnce({ ...proposedAction, state: 'rejected' });
+
+ await updateApprovalState('a/1', 'ws 1', 'rejected', 'Not ready');
+
+ expect(fetchMock).toHaveBeenCalledWith(
+ '/note-agent-actions/a%2F1?workspaceId=ws%201',
+ expect.objectContaining({
+ method: 'PATCH',
+ body: expect.stringContaining('"state":"rejected"'),
+ }),
+ );
+ const body = JSON.parse(fetchMock.mock.calls[0][1].body as string) as { reviewNote: string };
+ expect(body.reviewNote).toBe('Not ready');
+ });
+});
diff --git a/mobile/src/api/notes.test.ts b/mobile/src/api/notes.test.ts
new file mode 100644
index 0000000..f25745d
--- /dev/null
+++ b/mobile/src/api/notes.test.ts
@@ -0,0 +1,91 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+const { fetchMock, listWorkspacesMock } = vi.hoisted(() => ({
+ fetchMock: vi.fn(),
+ listWorkspacesMock: vi.fn(),
+}));
+
+vi.mock('./client', () => ({
+ getApiClient: () => ({
+ fetch: fetchMock,
+ }),
+}));
+
+vi.mock('./workspaces', () => ({
+ listWorkspaces: listWorkspacesMock,
+}));
+
+import { createNote, getNote, listNotes, updateNote } from './notes';
+
+const apiNote = {
+ id: 'n1',
+ workspaceId: 'ws 1',
+ title: 'Release note',
+ body: 'Ship it',
+ status: 'active' as const,
+ updatedAt: '2026-05-05T12:00:00.000Z',
+};
+
+describe('mobile notes API client', () => {
+ beforeEach(() => {
+ fetchMock.mockReset();
+ listWorkspacesMock.mockReset();
+ listWorkspacesMock.mockResolvedValue([{ id: 'ws 1', name: 'Research' }]);
+ });
+
+ it('lists notes with workspace names for note list cards', async () => {
+ fetchMock.mockResolvedValueOnce({ items: [apiNote] });
+
+ await expect(listNotes()).resolves.toEqual([
+ {
+ id: 'n1',
+ workspaceId: 'ws 1',
+ title: 'Release note',
+ body: 'Ship it',
+ status: 'active',
+ workspaceName: 'Research',
+ updatedAt: '2026-05-05T12:00:00.000Z',
+ },
+ ]);
+ expect(fetchMock).toHaveBeenCalledWith('/notes');
+ });
+
+ it('loads note detail with encoded workspace scope', async () => {
+ fetchMock.mockResolvedValueOnce(apiNote);
+
+ await getNote('n1', 'ws 1');
+
+ expect(fetchMock).toHaveBeenCalledWith('/notes/n1?workspaceId=ws%201');
+ });
+
+ it('creates a draft note with trimmed title and workspace scope', async () => {
+ fetchMock.mockResolvedValueOnce({ ...apiNote, title: 'New idea', status: 'draft' });
+
+ const created = await createNote('ws 1', ' New idea ', 'Body');
+
+ expect(created.title).toBe('New idea');
+ expect(fetchMock).toHaveBeenCalledWith(
+ '/notes',
+ expect.objectContaining({
+ method: 'POST',
+ }),
+ );
+ const body = JSON.parse(fetchMock.mock.calls[0][1].body as string) as { workspaceId: string; title: string };
+ expect(body.workspaceId).toBe('ws 1');
+ expect(body.title).toBe('New idea');
+ });
+
+ it('updates note detail with encoded workspace scope', async () => {
+ fetchMock.mockResolvedValueOnce({ ...apiNote, title: 'Updated' });
+
+ await updateNote('n1', 'ws 1', 'Updated', 'Body');
+
+ expect(fetchMock).toHaveBeenCalledWith(
+ '/notes/n1?workspaceId=ws%201',
+ expect.objectContaining({
+ method: 'PATCH',
+ body: JSON.stringify({ title: 'Updated', body: 'Body' }),
+ }),
+ );
+ });
+});
diff --git a/mobile/src/api/workspaces.test.ts b/mobile/src/api/workspaces.test.ts
new file mode 100644
index 0000000..4268632
--- /dev/null
+++ b/mobile/src/api/workspaces.test.ts
@@ -0,0 +1,32 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+const fetchMock = vi.fn();
+
+vi.mock('./client', () => ({
+ getApiClient: () => ({
+ fetch: fetchMock,
+ }),
+}));
+
+import { listWorkspaces } from './workspaces';
+
+describe('mobile workspaces API client', () => {
+ beforeEach(() => {
+ fetchMock.mockReset();
+ });
+
+ it('lists workspaces and preserves descriptions for selection UI', async () => {
+ fetchMock.mockResolvedValueOnce({
+ items: [
+ { id: 'ws-1', name: 'Research', description: 'AI notes' },
+ { id: 'ws-2', name: 'Inbox' },
+ ],
+ });
+
+ await expect(listWorkspaces()).resolves.toEqual([
+ { id: 'ws-1', name: 'Research', description: 'AI notes' },
+ { id: 'ws-2', name: 'Inbox', description: undefined },
+ ]);
+ expect(fetchMock).toHaveBeenCalledWith('/workspaces');
+ });
+});
diff --git a/mobile/src/app/prompt-result.test.tsx b/mobile/src/app/prompt-result.test.tsx
new file mode 100644
index 0000000..01cf045
--- /dev/null
+++ b/mobile/src/app/prompt-result.test.tsx
@@ -0,0 +1,85 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import React from 'react';
+import renderer, { act, type ReactTestRenderer } from 'react-test-renderer';
+
+const backMock = vi.fn();
+const clearResultMock = vi.fn();
+const testGlobal = globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean };
+testGlobal.IS_REACT_ACT_ENVIRONMENT = true;
+let lastResult: {
+ content: string;
+ templateSlug: string;
+ outputType: string;
+ model?: string;
+ usage?: { totalTokens: number };
+} | null = null;
+
+vi.mock('expo-router', () => ({
+ useRouter: () => ({ back: backMock }),
+ useLocalSearchParams: () => ({ templateName: 'Summarize' }),
+}));
+
+vi.mock('../store/prompt-store', () => ({
+ usePromptStore: () => ({
+ lastResult,
+ clearResult: clearResultMock,
+ }),
+}));
+
+import PromptResultScreen from './prompt-result';
+
+function renderWithAct(): ReactTestRenderer {
+ let tree: ReactTestRenderer | null = null;
+ act(() => {
+ tree = renderer.create();
+ });
+ if (!tree) {
+ throw new Error('PromptResultScreen did not render');
+ }
+ return tree;
+}
+
+function collectText(node: unknown): string {
+ if (node === null || node === undefined) return '';
+ if (typeof node === 'string') return node;
+ if (Array.isArray(node)) return node.map(collectText).join('');
+ if (typeof node === 'object' && 'children' in node) {
+ const children = (node as { children?: unknown[] }).children ?? [];
+ return children.map(collectText).join('');
+ }
+ return '';
+}
+
+describe('PromptResultScreen', () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ lastResult = null;
+ });
+
+ it('renders an empty state and back action without a result', () => {
+ const tree = renderWithAct();
+
+ expect(collectText(tree.toJSON())).toContain('No prompt result available.');
+ tree.root.findByProps({ accessibilityLabel: 'Go back' }).props.onPress();
+ expect(backMock).toHaveBeenCalled();
+ });
+
+ it('renders prompt result metadata and clears result on dismiss', () => {
+ lastResult = {
+ content: 'Summarized note content',
+ templateSlug: 'summarize',
+ outputType: 'new_note',
+ model: 'mock',
+ usage: { totalTokens: 42 },
+ };
+
+ const tree = renderWithAct();
+
+ const text = collectText(tree.toJSON());
+ expect(text).toContain('Summarized note content');
+ expect(text).toContain('mock ยท 42 tokens');
+ tree.root.findByProps({ accessibilityLabel: 'Dismiss prompt result' }).props.onPress();
+ expect(clearResultMock).toHaveBeenCalled();
+ expect(backMock).toHaveBeenCalled();
+ });
+});
diff --git a/mobile/src/lib/offline-queue.test.ts b/mobile/src/lib/offline-queue.test.ts
new file mode 100644
index 0000000..e58779a
--- /dev/null
+++ b/mobile/src/lib/offline-queue.test.ts
@@ -0,0 +1,105 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { PRODUCT_ID } from '../api/config';
+
+const { createOfflineQueueMock, fetchMock, queue } = vi.hoisted(() => {
+ const mockedQueue = {
+ enqueue: vi.fn(),
+ flush: vi.fn(),
+ length: vi.fn(),
+ };
+
+ return {
+ queue: mockedQueue,
+ createOfflineQueueMock: vi.fn(() => mockedQueue),
+ fetchMock: vi.fn(),
+ };
+});
+
+vi.mock('@bytelyst/offline-queue', () => ({
+ createOfflineQueue: createOfflineQueueMock,
+}));
+
+vi.mock('../api/client', () => ({
+ getApiClient: () => ({
+ fetch: fetchMock,
+ }),
+}));
+
+import {
+ OFFLINE_QUEUE_MAX_RETRIES,
+ OFFLINE_QUEUE_MAX_SIZE,
+ enqueueNoteCreate,
+ enqueueNoteUpdate,
+ flushNoteQueue,
+ getNoteQueueSize,
+} from './offline-queue';
+
+describe('mobile offline note queue', () => {
+ beforeEach(() => {
+ queue.enqueue.mockReset();
+ queue.flush.mockReset();
+ queue.length.mockReset();
+ fetchMock.mockReset();
+ });
+
+ it('configures the common offline queue with NoteLett storage limits', async () => {
+ expect(createOfflineQueueMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ storageKey: `${PRODUCT_ID}-offline-queue`,
+ maxRetries: OFFLINE_QUEUE_MAX_RETRIES,
+ maxQueueSize: OFFLINE_QUEUE_MAX_SIZE,
+ }),
+ );
+ expect(OFFLINE_QUEUE_MAX_RETRIES).toBe(5);
+ expect(OFFLINE_QUEUE_MAX_SIZE).toBe(50);
+ });
+
+ it('enqueues note creates and updates with product backend paths', () => {
+ enqueueNoteCreate({ id: 'n1', workspaceId: 'ws-1', title: 'Queued', body: 'Body' });
+ enqueueNoteUpdate({ id: 'n1', workspaceId: 'ws 1', title: 'Updated', body: 'Body' });
+
+ expect(queue.enqueue).toHaveBeenNthCalledWith(1, {
+ id: 'n1',
+ action: 'create',
+ path: '/notes',
+ payload: {
+ id: 'n1',
+ workspaceId: 'ws-1',
+ title: 'Queued',
+ body: 'Body',
+ tags: [],
+ links: [],
+ },
+ });
+ expect(queue.enqueue).toHaveBeenNthCalledWith(
+ 2,
+ expect.objectContaining({
+ action: 'update',
+ path: '/notes/n1?workspaceId=ws%201',
+ payload: { title: 'Updated', body: 'Body' },
+ }),
+ );
+ });
+
+ it('reports queue size and flushes queued actions through the shared API client', async () => {
+ queue.length.mockReturnValueOnce(2);
+ queue.flush.mockImplementationOnce(async (handler: (action: string, path: string, payload: unknown) => Promise) => {
+ await handler('create', '/notes', { id: 'n1' });
+ await handler('update', '/notes/n1?workspaceId=ws-1', { title: 'Updated' });
+ return { flushed: 2, failed: 0 };
+ });
+
+ expect(getNoteQueueSize()).toBe(2);
+ await expect(flushNoteQueue()).resolves.toEqual({ flushed: 2, failed: 0 });
+ expect(fetchMock).toHaveBeenNthCalledWith(
+ 1,
+ '/notes',
+ expect.objectContaining({ method: 'POST', body: JSON.stringify({ id: 'n1' }) }),
+ );
+ expect(fetchMock).toHaveBeenNthCalledWith(
+ 2,
+ '/notes/n1?workspaceId=ws-1',
+ expect.objectContaining({ method: 'PATCH', body: JSON.stringify({ title: 'Updated' }) }),
+ );
+ });
+});
diff --git a/mobile/src/lib/platform-clients.test.ts b/mobile/src/lib/platform-clients.test.ts
new file mode 100644
index 0000000..1b827b1
--- /dev/null
+++ b/mobile/src/lib/platform-clients.test.ts
@@ -0,0 +1,88 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { API_CONFIG, PRODUCT_ID } from '../api/config';
+import { APP_PLATFORM, APP_VERSION, OS_VERSION } from './app-metadata';
+
+const {
+ createFeedbackClientMock,
+ createBroadcastClientMock,
+ createSurveyClientMock,
+ getAccessTokenMock,
+} = vi.hoisted(() => ({
+ createFeedbackClientMock: vi.fn(),
+ createBroadcastClientMock: vi.fn(),
+ createSurveyClientMock: vi.fn(),
+ getAccessTokenMock: vi.fn(),
+}));
+
+vi.mock('@bytelyst/feedback-client', () => ({
+ createFeedbackClient: createFeedbackClientMock,
+}));
+
+vi.mock('@bytelyst/broadcast-client', () => ({
+ createBroadcastClient: createBroadcastClientMock,
+}));
+
+vi.mock('@bytelyst/survey-client', () => ({
+ createSurveyClient: createSurveyClientMock,
+}));
+
+vi.mock('../api/auth', () => ({
+ getAuthClient: () => ({
+ getAccessToken: getAccessTokenMock,
+ }),
+}));
+
+import { getBroadcastClient } from './broadcast-client';
+import { getFeedbackClient } from './feedback-client';
+import { getSurveyClient } from './survey-client';
+
+describe('mobile shared platform clients', () => {
+ beforeEach(() => {
+ createFeedbackClientMock.mockReset();
+ createBroadcastClientMock.mockReset();
+ createSurveyClientMock.mockReset();
+ getAccessTokenMock.mockReset();
+ getAccessTokenMock.mockReturnValue('mobile-token');
+ createFeedbackClientMock.mockReturnValue({ submitWithScreenshot: vi.fn() });
+ createBroadcastClientMock.mockReturnValue({ listMessages: vi.fn() });
+ createSurveyClientMock.mockReturnValue({ getActiveSurvey: vi.fn() });
+ });
+
+ it('configures feedback with platform base URL and auth token access', () => {
+ getFeedbackClient();
+
+ expect(createFeedbackClientMock).toHaveBeenCalledWith({
+ baseUrl: API_CONFIG.platformBaseUrl,
+ getAuthToken: expect.any(Function),
+ });
+ expect(createFeedbackClientMock.mock.calls[0][0].getAuthToken()).toBe('mobile-token');
+ });
+
+ it('configures broadcast with product identity and app metadata', () => {
+ getBroadcastClient();
+
+ expect(createBroadcastClientMock).toHaveBeenCalledWith({
+ baseUrl: API_CONFIG.platformBaseUrl,
+ productId: PRODUCT_ID,
+ getAuthToken: expect.any(Function),
+ platform: APP_PLATFORM,
+ appVersion: APP_VERSION,
+ osVersion: OS_VERSION,
+ });
+ expect(createBroadcastClientMock.mock.calls[0][0].getAuthToken()).toBe('mobile-token');
+ });
+
+ it('configures surveys with product identity and app metadata', () => {
+ getSurveyClient();
+
+ expect(createSurveyClientMock).toHaveBeenCalledWith({
+ baseUrl: API_CONFIG.platformBaseUrl,
+ productId: PRODUCT_ID,
+ getAuthToken: expect.any(Function),
+ platform: APP_PLATFORM,
+ appVersion: APP_VERSION,
+ osVersion: OS_VERSION,
+ });
+ expect(createSurveyClientMock.mock.calls[0][0].getAuthToken()).toBe('mobile-token');
+ });
+});
diff --git a/mobile/src/store/intake-store.test.ts b/mobile/src/store/intake-store.test.ts
new file mode 100644
index 0000000..d41d477
--- /dev/null
+++ b/mobile/src/store/intake-store.test.ts
@@ -0,0 +1,77 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type { IntakeJob } from '../api/intake';
+
+const { listIntakeJobsMock, getIntakeJobMock } = vi.hoisted(() => ({
+ listIntakeJobsMock: vi.fn(),
+ getIntakeJobMock: vi.fn(),
+}));
+
+vi.mock('../api/intake', () => ({
+ listIntakeJobs: (...args: unknown[]) => listIntakeJobsMock(...args),
+ getIntakeJob: (...args: unknown[]) => getIntakeJobMock(...args),
+}));
+
+import { useIntakeStore } from './intake-store';
+
+function resetStore() {
+ useIntakeStore.setState({
+ activeJobs: [],
+ completedJobIds: [],
+ isPolling: false,
+ });
+}
+
+function makeJob(overrides: Partial = {}): IntakeJob {
+ return {
+ id: 'job-1',
+ productId: 'notelett',
+ userId: 'user-1',
+ workspaceId: 'ws-1',
+ noteId: 'n1',
+ ruleId: 'rule-1',
+ url: 'https://example.com',
+ contentType: 'article',
+ templateSlug: 'summarize',
+ status: 'processing',
+ startedAt: '2026-05-05T12:00:00Z',
+ ...overrides,
+ };
+}
+
+describe('useIntakeStore', () => {
+ beforeEach(() => {
+ vi.useRealTimers();
+ vi.clearAllMocks();
+ resetStore();
+ });
+
+ it('pollActiveJobs loads active jobs and prevents duplicate concurrent polls', async () => {
+ listIntakeJobsMock.mockResolvedValueOnce([makeJob({ status: 'queued' })]);
+
+ await useIntakeStore.getState().pollActiveJobs();
+
+ expect(listIntakeJobsMock).toHaveBeenCalledWith({ status: 'queued,extracting,processing', limit: 50 });
+ expect(useIntakeStore.getState().activeJobs[0].status).toBe('queued');
+ expect(useIntakeStore.getState().isPolling).toBe(false);
+
+ useIntakeStore.setState({ isPolling: true });
+ await useIntakeStore.getState().pollActiveJobs();
+ expect(listIntakeJobsMock).toHaveBeenCalledTimes(1);
+ });
+
+ it('waitForJob calls completion handler and records completed job ids', async () => {
+ vi.useFakeTimers();
+ const onComplete = vi.fn();
+ const completeJob = makeJob({ status: 'complete' });
+ useIntakeStore.setState({ activeJobs: [makeJob()] });
+ getIntakeJobMock.mockResolvedValueOnce(completeJob);
+
+ useIntakeStore.getState().waitForJob('job-1', onComplete);
+ await vi.advanceTimersByTimeAsync(3000);
+
+ expect(onComplete).toHaveBeenCalledWith(completeJob);
+ expect(useIntakeStore.getState().activeJobs).toEqual([]);
+ expect(useIntakeStore.getState().completedJobIds).toEqual(['job-1']);
+ vi.useRealTimers();
+ });
+});