feat: enhance OpenCode provider tests and UI setup

- Updated unit tests for OpenCode provider to include new authentication indicators.
- Refactored ProvidersSetupStep component by removing unnecessary UI elements for better clarity.
- Improved board background persistence tests by utilizing a setup function for initializing app state.
- Enhanced settings synchronization tests to ensure proper handling of login and app state.

These changes improve the testing framework and user interface for OpenCode integration, ensuring a smoother setup and authentication process.
This commit is contained in:
webdevcody
2026-01-09 10:08:38 -05:00
parent 87c3d766c9
commit a695d0db7b
5 changed files with 111 additions and 80 deletions

View File

@@ -5,7 +5,7 @@ import {
} from '../../../src/providers/opencode-provider.js'; } from '../../../src/providers/opencode-provider.js';
import type { ProviderMessage } from '@automaker/types'; import type { ProviderMessage } from '@automaker/types';
import { collectAsyncGenerator } from '../../utils/helpers.js'; import { collectAsyncGenerator } from '../../utils/helpers.js';
import { spawnJSONLProcess } from '@automaker/platform'; import { spawnJSONLProcess, getOpenCodeAuthIndicators } from '@automaker/platform';
vi.mock('@automaker/platform', () => ({ vi.mock('@automaker/platform', () => ({
spawnJSONLProcess: vi.fn(), spawnJSONLProcess: vi.fn(),
@@ -13,6 +13,11 @@ vi.mock('@automaker/platform', () => ({
findCliInWsl: vi.fn().mockReturnValue(null), findCliInWsl: vi.fn().mockReturnValue(null),
createWslCommand: vi.fn(), createWslCommand: vi.fn(),
windowsToWslPath: vi.fn(), windowsToWslPath: vi.fn(),
getOpenCodeAuthIndicators: vi.fn().mockResolvedValue({
hasAuthFile: false,
hasOAuthToken: false,
hasApiKey: false,
}),
})); }));
describe('opencode-provider.ts', () => { describe('opencode-provider.ts', () => {
@@ -25,7 +30,8 @@ describe('opencode-provider.ts', () => {
}); });
afterEach(() => { afterEach(() => {
vi.restoreAllMocks(); // Note: Don't use vi.restoreAllMocks() here as it would undo the module-level
// mock implementations (like getOpenCodeAuthIndicators) set up with vi.mock()
}); });
// ========================================================================== // ==========================================================================
@@ -815,6 +821,15 @@ describe('opencode-provider.ts', () => {
// ========================================================================== // ==========================================================================
describe('detectInstallation', () => { describe('detectInstallation', () => {
beforeEach(() => {
// Ensure the mock implementation is set up for each test
vi.mocked(getOpenCodeAuthIndicators).mockResolvedValue({
hasAuthFile: false,
hasOAuthToken: false,
hasApiKey: false,
});
});
it('should return installed true when CLI is found', async () => { it('should return installed true when CLI is found', async () => {
(provider as unknown as { cliPath: string }).cliPath = '/usr/local/bin/opencode'; (provider as unknown as { cliPath: string }).cliPath = '/usr/local/bin/opencode';
(provider as unknown as { detectedStrategy: string }).detectedStrategy = 'native'; (provider as unknown as { detectedStrategy: string }).detectedStrategy = 'native';

View File

@@ -1271,25 +1271,6 @@ export function ProvidersSetupStep({ onNext, onBack }: ProvidersSetupStepProps)
</div> </div>
</Tabs> </Tabs>
<div className="flex items-center justify-center gap-4 py-2 text-sm">
{providers.map((provider) => (
<div
key={provider.id}
className={cn(
'flex items-center gap-1.5',
provider.configured ? 'text-green-500' : 'text-muted-foreground'
)}
>
{provider.configured ? (
<CheckCircle2 className="w-4 h-4" />
) : (
<div className="w-4 h-4 rounded-full border border-current" />
)}
<span>{provider.label}</span>
</div>
))}
</div>
<div className="flex justify-between pt-4"> <div className="flex justify-between pt-4">
<Button variant="ghost" onClick={onBack} className="text-muted-foreground"> <Button variant="ghost" onClick={onBack} className="text-muted-foreground">
<ArrowLeft className="w-4 h-4 mr-2" /> <ArrowLeft className="w-4 h-4 mr-2" />

View File

@@ -20,6 +20,7 @@ import {
cleanupTempDir, cleanupTempDir,
authenticateForTests, authenticateForTests,
handleLoginScreenIfPresent, handleLoginScreenIfPresent,
setupWelcomeView,
} from '../utils'; } from '../utils';
// Create unique temp dirs for this test run // Create unique temp dirs for this test run
@@ -102,53 +103,53 @@ test.describe('Board Background Persistence', () => {
JSON.stringify({ version: 1 }, null, 2) JSON.stringify({ version: 1 }, null, 2)
); );
// Set up app state with both projects in the list (not recent, but in projects list) // Set up welcome view with both projects in the list
await page.addInitScript( await setupWelcomeView(page, {
({ projects }: { projects: string[] }) => { workspaceDir: TEST_TEMP_DIR,
const appState = { recentProjects: [
state: { {
projects: [ id: projectAId,
{ name: projectAName,
id: projects[0], path: projectAPath,
name: projects[1], lastOpened: new Date(Date.now() - 86400000).toISOString(),
path: projects[2], },
lastOpened: new Date(Date.now() - 86400000).toISOString(), {
theme: 'red', id: projectBId,
}, name: projectBName,
{ path: projectBPath,
id: projects[3], lastOpened: new Date(Date.now() - 172800000).toISOString(),
name: projects[4], },
path: projects[5], ],
lastOpened: new Date(Date.now() - 172800000).toISOString(), });
theme: 'red',
},
],
currentProject: null,
currentView: 'welcome',
theme: 'red',
sidebarOpen: true,
apiKeys: { anthropic: '', google: '' },
chatSessions: [],
chatHistoryOpen: false,
maxConcurrency: 3,
boardBackgroundByProject: {},
},
version: 2,
};
localStorage.setItem('automaker-storage', JSON.stringify(appState));
// Setup complete await authenticateForTests(page);
const setupState = {
state: { // Intercept settings API to use our test projects and clear currentProjectId
setupComplete: true, // This ensures the app shows the welcome view with our test projects
workspaceDir: '/tmp', await page.route('**/api/settings/global', async (route) => {
const response = await route.fetch();
const json = await response.json();
if (json.settings) {
// Clear currentProjectId to show welcome view
json.settings.currentProjectId = null;
// Include our test projects so they appear in the recent projects list
json.settings.projects = [
{
id: projectAId,
name: projectAName,
path: projectAPath,
lastOpened: new Date(Date.now() - 86400000).toISOString(),
}, },
version: 0, {
}; id: projectBId,
localStorage.setItem('setup-storage', JSON.stringify(setupState)); name: projectBName,
}, path: projectBPath,
{ projects: [projectAId, projectAName, projectAPath, projectBId, projectBName, projectBPath] } lastOpened: new Date(Date.now() - 172800000).toISOString(),
); },
];
}
await route.fulfill({ response, json });
});
// Track API calls to /api/settings/project to verify settings are being loaded // Track API calls to /api/settings/project to verify settings are being loaded
const settingsApiCalls: Array<{ url: string; method: string; body: string }> = []; const settingsApiCalls: Array<{ url: string; method: string; body: string }> = [];
@@ -163,7 +164,6 @@ test.describe('Board Background Persistence', () => {
}); });
// Navigate to the app // Navigate to the app
await authenticateForTests(page);
await page.goto('/'); await page.goto('/');
await page.waitForLoadState('load'); await page.waitForLoadState('load');
await handleLoginScreenIfPresent(page); await handleLoginScreenIfPresent(page);
@@ -179,10 +179,10 @@ test.describe('Board Background Persistence', () => {
// Wait for board view // Wait for board view
await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 }); await expect(page.locator('[data-testid="board-view"]')).toBeVisible({ timeout: 15000 });
// Verify project A is current // Verify project A is current (check header paragraph which is always visible)
await expect( await expect(page.locator('[data-testid="board-view"]').getByText(projectAName)).toBeVisible({
page.locator('[data-testid="project-selector"]').getByText(projectAName) timeout: 5000,
).toBeVisible({ timeout: 5000 }); });
// CRITICAL: Wait for settings to be loaded (useProjectSettingsLoader hook) // CRITICAL: Wait for settings to be loaded (useProjectSettingsLoader hook)
// This ensures the background settings are fetched from the server // This ensures the background settings are fetched from the server
@@ -196,8 +196,16 @@ test.describe('Board Background Persistence', () => {
// Wait for initial project load to stabilize // Wait for initial project load to stabilize
await page.waitForTimeout(500); await page.waitForTimeout(500);
// Ensure sidebar is expanded before interacting with project selector
const expandSidebarButton = page.locator('button:has-text("Expand sidebar")');
if (await expandSidebarButton.isVisible()) {
await expandSidebarButton.click();
await page.waitForTimeout(300);
}
// Switch to project B (no background) // Switch to project B (no background)
const projectSelector = page.locator('[data-testid="project-selector"]'); const projectSelector = page.locator('[data-testid="project-selector"]');
await expect(projectSelector).toBeVisible({ timeout: 5000 });
await projectSelector.click(); await projectSelector.click();
// Wait for dropdown to be visible // Wait for dropdown to be visible
@@ -315,7 +323,6 @@ test.describe('Board Background Persistence', () => {
name: project[1], name: project[1],
path: project[2], path: project[2],
lastOpened: new Date().toISOString(), lastOpened: new Date().toISOString(),
theme: 'red',
}; };
const appState = { const appState = {
@@ -323,8 +330,9 @@ test.describe('Board Background Persistence', () => {
projects: [projectObj], projects: [projectObj],
currentProject: projectObj, currentProject: projectObj,
currentView: 'board', currentView: 'board',
theme: 'red', theme: 'dark',
sidebarOpen: true, sidebarOpen: true,
skipSandboxWarning: true,
apiKeys: { anthropic: '', google: '' }, apiKeys: { anthropic: '', google: '' },
chatSessions: [], chatSessions: [],
chatHistoryOpen: false, chatHistoryOpen: false,
@@ -335,19 +343,44 @@ test.describe('Board Background Persistence', () => {
}; };
localStorage.setItem('automaker-storage', JSON.stringify(appState)); localStorage.setItem('automaker-storage', JSON.stringify(appState));
// Setup complete // Setup complete - use correct key name
const setupState = { const setupState = {
state: { state: {
isFirstRun: false,
setupComplete: true, setupComplete: true,
workspaceDir: '/tmp', skipClaudeSetup: false,
}, },
version: 0, version: 1,
}; };
localStorage.setItem('setup-storage', JSON.stringify(setupState)); localStorage.setItem('automaker-setup', JSON.stringify(setupState));
// Disable splash screen in tests
sessionStorage.setItem('automaker-splash-shown', 'true');
}, },
{ project: [projectId, projectName, projectPath] } { project: [projectId, projectName, projectPath] }
); );
await authenticateForTests(page);
// Intercept settings API to use our test project instead of the E2E fixture
await page.route('**/api/settings/global', async (route) => {
const response = await route.fetch();
const json = await response.json();
// Override to use our test project
if (json.settings) {
json.settings.currentProjectId = projectId;
json.settings.projects = [
{
id: projectId,
name: projectName,
path: projectPath,
lastOpened: new Date().toISOString(),
},
];
}
await route.fulfill({ response, json });
});
// Track API calls to /api/settings/project to verify settings are being loaded // Track API calls to /api/settings/project to verify settings are being loaded
const settingsApiCalls: Array<{ url: string; method: string; body: string }> = []; const settingsApiCalls: Array<{ url: string; method: string; body: string }> = [];
page.on('request', (request) => { page.on('request', (request) => {
@@ -360,8 +393,7 @@ test.describe('Board Background Persistence', () => {
} }
}); });
// Navigate and authenticate // Navigate to the app
await authenticateForTests(page);
await page.goto('/'); await page.goto('/');
await page.waitForLoadState('load'); await page.waitForLoadState('load');
await handleLoginScreenIfPresent(page); await handleLoginScreenIfPresent(page);

View File

@@ -14,7 +14,7 @@
import { test, expect } from '@playwright/test'; import { test, expect } from '@playwright/test';
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { authenticateForTests } from '../utils'; import { authenticateForTests, handleLoginScreenIfPresent } from '../utils';
const SETTINGS_PATH = path.resolve(process.cwd(), '../server/data/settings.json'); const SETTINGS_PATH = path.resolve(process.cwd(), '../server/data/settings.json');
const WORKSPACE_ROOT = path.resolve(process.cwd(), '../..'); const WORKSPACE_ROOT = path.resolve(process.cwd(), '../..');
@@ -109,6 +109,8 @@ test.describe('Settings startup sync race', () => {
// Ensure authenticated and app is loaded at least to welcome/board. // Ensure authenticated and app is loaded at least to welcome/board.
await authenticateForTests(page); await authenticateForTests(page);
await page.goto('/'); await page.goto('/');
await page.waitForLoadState('load');
await handleLoginScreenIfPresent(page);
await page await page
.locator('[data-testid="welcome-view"], [data-testid="board-view"]') .locator('[data-testid="welcome-view"], [data-testid="board-view"]')
.first() .first()

View File

@@ -202,6 +202,7 @@ export interface InstallationStatus {
*/ */
method?: 'cli' | 'wsl' | 'npm' | 'brew' | 'sdk'; method?: 'cli' | 'wsl' | 'npm' | 'brew' | 'sdk';
hasApiKey?: boolean; hasApiKey?: boolean;
hasOAuthToken?: boolean;
authenticated?: boolean; authenticated?: boolean;
error?: string; error?: string;
} }