Update expo application
Build and Push Next App / quality (push) Successful in 1m27s
Build and Push Next App / build-next (push) Successful in 3m58s

This commit is contained in:
Gabriel Brown
2026-06-22 12:13:02 -04:00
parent ddce5efb13
commit 42f95530de
78 changed files with 5315 additions and 421 deletions
+187
View File
@@ -0,0 +1,187 @@
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { describe, expect, test, vi } from 'vitest';
import { AiProviderProfileForm } from '../../src/components/settings/ai-provider-profile-form';
import { SpoonAgentSettingsForm } from '../../src/components/spoons/spoon-agent-settings-form';
import { SpoonSecretsPanel } from '../../src/components/spoons/spoon-secrets-panel';
describe('mobile forms', () => {
test('SpoonSecretsPanel previews secret names only and imports parsed env values', async () => {
const onImportSecrets = vi.fn().mockResolvedValue(undefined);
render(
<SpoonSecretsPanel
adding={false}
importing={false}
removingId={undefined}
secrets={[]}
onAddSecret={vi.fn()}
onImportSecrets={onImportSecrets}
onRemoveSecret={vi.fn()}
/>,
);
fireEvent.change(screen.getByPlaceholderText('AUTH_SECRET=...'), {
target: {
value: 'AUTH_SECRET=super-secret\nexport AUTHENTIK_CLIENT_ID=client',
},
});
expect(screen.getAllByText(/AUTH_SECRET/).length).toBeGreaterThan(0);
expect(screen.getAllByText(/AUTHENTIK_CLIENT_ID/).length).toBeGreaterThan(
0,
);
expect(screen.getByText(/valid secrets found/).textContent).not.toContain(
'super-secret',
);
fireEvent.click(screen.getByText('Import secrets'));
await waitFor(() =>
expect(onImportSecrets).toHaveBeenCalledWith([
{ name: 'AUTH_SECRET', value: 'super-secret' },
{ name: 'AUTHENTIK_CLIENT_ID', value: 'client' },
]),
);
});
test('SpoonSecretsPanel disables import with no parsed secrets', () => {
render(
<SpoonSecretsPanel
adding={false}
importing={false}
removingId={undefined}
secrets={[]}
onAddSecret={vi.fn()}
onImportSecrets={vi.fn()}
onRemoveSecret={vi.fn()}
/>,
);
expect(screen.getByText('Import secrets').closest('button')).toBeDisabled();
});
test('AiProviderProfileForm selects default model from model options', async () => {
const onSubmit = vi.fn().mockResolvedValue(undefined);
render(
<AiProviderProfileForm
saving={false}
onSubmit={onSubmit}
existing={{
_id: 'profile' as never,
authType: 'api_key',
defaultModel: 'gpt-5.1-codex',
enabled: true,
modelOptions: ['gpt-5.1-codex', 'gpt-5.5'],
name: 'OpenAI',
provider: 'openai',
reasoningEffort: 'medium',
}}
/>,
);
fireEvent.click(screen.getByText('gpt-5.1-codex'));
fireEvent.click(screen.getByText('gpt-5.5'));
fireEvent.click(screen.getByText('Save provider'));
await waitFor(() =>
expect(onSubmit).toHaveBeenCalledWith(
expect.objectContaining({ defaultModel: 'gpt-5.5' }),
),
);
});
test('AiProviderProfileForm shows Codex auth JSON instructions', () => {
render(
<AiProviderProfileForm
saving={false}
onSubmit={vi.fn()}
existing={{
_id: 'profile' as never,
authType: 'opencode_auth_json',
defaultModel: 'gpt-5.1-codex',
enabled: true,
modelOptions: ['gpt-5.1-codex'],
name: 'Codex',
provider: 'opencode_openai_login',
reasoningEffort: 'medium',
}}
/>,
);
expect(screen.getByText(/~\/.codex\/auth.json/)).toBeTruthy();
});
test('SpoonAgentSettingsForm disables provider/model controls without provider profiles', () => {
render(
<SpoonAgentSettingsForm
profiles={[]}
onUpdate={vi.fn()}
agent={{
agentModel: '',
autoDetectCommands: true,
branchPrefix: 'spoon/agent',
enabled: true,
materializeEnvFileByDefault: false,
reasoningEffort: 'medium',
}}
/>,
);
expect(
screen.getByText('Configure an AI provider in Settings'),
).toBeTruthy();
expect(
screen.getByText('No models available').closest('button'),
).toBeDisabled();
});
test('SpoonAgentSettingsForm applies selected provider defaults', async () => {
const onUpdate = vi.fn().mockResolvedValue(undefined);
render(
<SpoonAgentSettingsForm
agent={{
agentModel: 'gpt-5.1-codex',
autoDetectCommands: true,
branchPrefix: 'spoon/agent',
enabled: true,
materializeEnvFileByDefault: false,
reasoningEffort: 'high',
}}
profiles={[
{
_id: 'profile-a' as never,
defaultModel: 'gpt-5.1-codex',
enabled: true,
modelOptions: ['gpt-5.1-codex'],
name: 'OpenAI',
reasoningEffort: 'medium',
},
{
_id: 'profile-b' as never,
defaultModel: 'claude-sonnet-4-5',
enabled: true,
modelOptions: ['claude-sonnet-4-5'],
name: 'Anthropic',
reasoningEffort: 'low',
},
]}
onUpdate={onUpdate}
/>,
);
fireEvent.click(screen.getByText('OpenAI'));
fireEvent.click(screen.getByText('Anthropic'));
await waitFor(() =>
expect(onUpdate).toHaveBeenCalledWith(
expect.objectContaining({
agentModel: 'claude-sonnet-4-5',
reasoningEffort: 'low',
}),
),
);
});
});
@@ -0,0 +1,127 @@
import { render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import DashboardRoute from '../../src/app/(app)/dashboard';
import SettingsRoute from '../../src/app/(app)/settings';
import SpoonsRoute from '../../src/app/(app)/spoons';
import ThreadsRoute from '../../src/app/(app)/threads';
import WorkspaceRoute from '../../src/app/(app)/workspace/[jobId]';
import { mockedUseQuery } from '../setup';
describe('mobile route smoke tests', () => {
beforeEach(() => {
vi.clearAllMocks();
mockedUseQuery.mockReset();
});
test('Dashboard renders metrics from mocked Convex data', () => {
mockedUseQuery
.mockReturnValueOnce([
{
_id: 'spoon-1',
status: 'active',
syncStatus: 'behind',
upstreamAheadBy: 3,
},
] as never)
.mockReturnValueOnce([] as never)
.mockReturnValueOnce([
{
_id: 'thread-1',
source: 'user_request',
status: 'open',
title: 'Update auth',
updatedAt: Date.UTC(2026, 0, 1),
},
] as never);
render(<DashboardRoute />);
expect(screen.getByText('Dashboard')).toBeTruthy();
expect(screen.getByText('Update auth')).toBeTruthy();
expect(screen.getByText('Upstream commits')).toBeTruthy();
});
test('Spoons list renders empty state and one row', () => {
mockedUseQuery
.mockReturnValueOnce([
{
_id: 'spoon-1',
forkOwner: 'gib',
forkRepo: 'usesend',
name: 'usesend-authentik',
status: 'active',
syncStatus: 'up_to_date',
upstreamAheadBy: 0,
upstreamOwner: 'usesend',
upstreamRepo: 'usesend',
},
] as never)
.mockReturnValueOnce([] as never);
render(<SpoonsRoute />);
expect(screen.getByText('Spoons')).toBeTruthy();
expect(screen.getByText('usesend-authentik')).toBeTruthy();
});
test('Threads list renders filters and rows', () => {
mockedUseQuery.mockReturnValueOnce([
{
_id: 'thread-1',
source: 'upstream_update',
status: 'waiting_for_user',
title: 'Upstream auth changes landed',
updatedAt: Date.UTC(2026, 0, 1),
},
] as never);
render(<ThreadsRoute />);
expect(screen.getByText('Waiting')).toBeTruthy();
expect(screen.getByText('Upstream auth changes landed')).toBeTruthy();
});
test('Workspace route renders tabs and job status', () => {
mockedUseQuery
.mockReturnValueOnce({
_id: 'job-1',
model: 'gpt-5.1-codex',
reasoningEffort: 'medium',
status: 'running',
workBranch: 'spoon/thread/example',
workspaceStatus: 'active',
} as never)
.mockReturnValueOnce([] as never)
.mockReturnValueOnce([] as never)
.mockReturnValueOnce([] as never);
render(<WorkspaceRoute />);
expect(screen.getByText('Workspace review')).toBeTruthy();
expect(screen.getByText('Messages')).toBeTruthy();
expect(screen.getByText('running')).toBeTruthy();
});
test('Settings index renders GitHub and AI provider summaries', () => {
mockedUseQuery
.mockReturnValueOnce({ email: 'gib@example.com' } as never)
.mockReturnValueOnce({
displayName: 'gibbyb',
status: 'active',
} as never)
.mockReturnValueOnce([
{
_id: 'provider-1',
isDefault: true,
name: 'Codex',
},
] as never);
render(<SettingsRoute />);
expect(screen.getByText('gib@example.com')).toBeTruthy();
expect(screen.getByText('GitHub connected as gibbyb')).toBeTruthy();
expect(screen.getByText('1 provider, default Codex')).toBeTruthy();
});
});
@@ -0,0 +1,124 @@
import { Alert } from 'react-native';
import { fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { SpoonStatusBadge } from '../../src/components/spoons/spoon-status-badge';
import { ThreadStatusBadge } from '../../src/components/threads/thread-status-badge';
import { ConfirmButton } from '../../src/components/ui/confirm-button';
import { PillTabs } from '../../src/components/ui/pill-tabs';
import { SheetSelect } from '../../src/components/ui/sheet-select';
import { DiffPreview } from '../../src/components/workspace/diff-preview';
describe('mobile UI primitives', () => {
beforeEach(() => {
vi.clearAllMocks();
});
test('PillTabs renders labels and changes selection', () => {
const onChange = vi.fn();
render(
<PillTabs
tabs={[
{ label: 'Overview', value: 'overview' },
{ label: 'Settings', value: 'settings' },
]}
value='overview'
onChange={onChange}
/>,
);
fireEvent.click(screen.getByText('Settings'));
expect(screen.getByText('Overview')).toBeTruthy();
expect(onChange).toHaveBeenCalledWith('settings');
});
test('SheetSelect opens and chooses an option', () => {
const onChange = vi.fn();
render(
<SheetSelect
label='Provider'
options={[
{ label: 'OpenAI', value: 'openai' },
{ label: 'Anthropic', value: 'anthropic' },
]}
value='openai'
onChange={onChange}
/>,
);
fireEvent.click(screen.getByText('OpenAI'));
fireEvent.click(screen.getByText('Anthropic'));
expect(onChange).toHaveBeenCalledWith('anthropic');
});
test('SheetSelect respects disabled state', () => {
const onChange = vi.fn();
render(
<SheetSelect
disabled
label='Provider'
options={[{ label: 'OpenAI', value: 'openai' }]}
value='openai'
onChange={onChange}
/>,
);
fireEvent.click(screen.getByText('OpenAI'));
expect(onChange).not.toHaveBeenCalled();
});
test('ConfirmButton delegates confirmation to Alert', () => {
const onConfirm = vi.fn();
render(
<ConfirmButton
confirmLabel='Delete'
message='Delete this?'
title='Delete'
onConfirm={onConfirm}
>
Remove
</ConfirmButton>,
);
fireEvent.click(screen.getByText('Remove'));
const calls = vi.mocked(Alert.alert).mock.calls;
const confirm = calls[0]?.[2]?.[1];
confirm?.onPress?.();
expect(onConfirm).toHaveBeenCalledOnce();
});
test('DiffPreview truncates and expands long diffs', () => {
const diff = Array.from({ length: 125 }, (_, index) =>
index % 2 === 0 ? `+added ${index}` : `-removed ${index}`,
).join('\n');
render(<DiffPreview content={diff} initialLines={3} />);
expect(screen.getByText('+added 0')).toBeTruthy();
expect(screen.queryByText('-removed 5')).toBeNull();
fireEvent.click(screen.getByText('Show 122 more lines'));
expect(screen.getByText('-removed 5')).toBeTruthy();
});
test('status badges render readable labels', () => {
render(
<>
<SpoonStatusBadge status='up_to_date' />
<ThreadStatusBadge status='waiting_for_user' />
</>,
);
expect(screen.getByText('up to date')).toBeTruthy();
expect(screen.getByText('waiting for user')).toBeTruthy();
});
});