From 19e7ea995e79fb006a04e653ae9a11ce97120fd8 Mon Sep 17 00:00:00 2001 From: 777genius Date: Mon, 20 Apr 2026 20:38:12 +0300 Subject: [PATCH] fix(ci): stabilize workspace test suite --- src/main/services/discovery/ProjectScanner.ts | 2 +- src/main/utils/metadataExtraction.ts | 47 ++++++++++++++++++- .../CliInstallerService.healthCheck.test.ts | 2 +- .../ConfigManager.notifications.test.ts | 12 ++++- .../TeamProvisioningServicePrepare.test.ts | 22 +++++---- .../team/dialogs/LaunchTeamDialog.test.ts | 1 + 6 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/main/services/discovery/ProjectScanner.ts b/src/main/services/discovery/ProjectScanner.ts index da24adb1..4fe892a1 100644 --- a/src/main/services/discovery/ProjectScanner.ts +++ b/src/main/services/discovery/ProjectScanner.ts @@ -240,7 +240,7 @@ export class ProjectScanner { } const ms = Date.now() - startedAt; - if (ms >= 2000) { + if (ms >= 5000) { logger.warn( `[scan] completed slow ms=${ms} projectDirs=${projectDirs.length} projects=${validProjects.length}` ); diff --git a/src/main/utils/metadataExtraction.ts b/src/main/utils/metadataExtraction.ts index f28146ff..344e565b 100644 --- a/src/main/utils/metadataExtraction.ts +++ b/src/main/utils/metadataExtraction.ts @@ -4,6 +4,7 @@ import { isCommandOutputContent, sanitizeDisplayContent } from '@shared/utils/contentSanitizer'; import { createLogger } from '@shared/utils/logger'; +import * as fs from 'fs/promises'; import * as readline from 'readline'; import { LocalFileSystemProvider } from '../services/infrastructure/LocalFileSystemProvider'; @@ -29,7 +30,7 @@ function normalizeDriveLetter(p: string): string { const defaultProvider = new LocalFileSystemProvider(); -const JSONL_HEAD_TIMEOUT_MS = 2000; +const JSONL_HEAD_TIMEOUT_MS = 5000; const JSONL_HEAD_MAX_BYTES = 256 * 1024; const JSONL_HEAD_MAX_LINES = 400; @@ -53,6 +54,41 @@ function createStreamCleanup(rl: readline.Interface, fileStream: Readable): () = }; } +function extractCwdFromBufferedText(text: string): string | null { + const lines = text.split(/\r?\n/, JSONL_HEAD_MAX_LINES); + for (const line of lines) { + if (!line.trim()) continue; + + let entry: ChatHistoryEntry; + try { + entry = JSON.parse(line) as ChatHistoryEntry; + } catch { + continue; + } + + if ('cwd' in entry && entry.cwd) { + return normalizeDriveLetter(translateWslMountPath(entry.cwd)); + } + } + + return null; +} + +async function extractCwdFromLocalFile(filePath: string): Promise { + const handle = await fs.open(filePath, 'r'); + try { + const buffer = Buffer.alloc(JSONL_HEAD_MAX_BYTES); + const { bytesRead } = await handle.read(buffer, 0, JSONL_HEAD_MAX_BYTES, 0); + if (bytesRead <= 0) { + return null; + } + + return extractCwdFromBufferedText(buffer.toString('utf8', 0, bytesRead)); + } finally { + await handle.close().catch(() => undefined); + } +} + /** * Extract CWD (current working directory) from the first entry. * Used to get the actual project path from encoded directory names. @@ -74,6 +110,15 @@ export async function extractCwd( return null; } + if (fsProvider.type === 'local') { + try { + return await extractCwdFromLocalFile(filePath); + } catch (error) { + logger.debug(`Error extracting cwd from local file ${filePath}:`, error); + return null; + } + } + const fileStream = fsProvider.createReadStream(filePath, { encoding: 'utf8' }); const rl = readline.createInterface({ input: fileStream, diff --git a/test/main/services/infrastructure/CliInstallerService.healthCheck.test.ts b/test/main/services/infrastructure/CliInstallerService.healthCheck.test.ts index fe0c4aa2..b926955c 100644 --- a/test/main/services/infrastructure/CliInstallerService.healthCheck.test.ts +++ b/test/main/services/infrastructure/CliInstallerService.healthCheck.test.ts @@ -83,7 +83,7 @@ describe('CliInstallerService health check', () => { it('does not treat a found binary as installed until --version succeeds', async () => { resolveBinaryMock.mockResolvedValue('/usr/local/bin/claude'); - execCliMock.mockRejectedValueOnce(new Error('spawn EACCES')); + execCliMock.mockRejectedValue(new Error('spawn EACCES')); const status = await service.getStatus(); diff --git a/test/main/services/infrastructure/ConfigManager.notifications.test.ts b/test/main/services/infrastructure/ConfigManager.notifications.test.ts index c839d0c7..65158c1f 100644 --- a/test/main/services/infrastructure/ConfigManager.notifications.test.ts +++ b/test/main/services/infrastructure/ConfigManager.notifications.test.ts @@ -20,11 +20,12 @@ describe('ConfigManager notification config shape', () => { vi.resetModules(); overrideRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'config-notifications-')); + const configPath = path.join(overrideRoot, 'claude-devtools-config.json'); const pathDecoder = await import('../../../../src/main/utils/pathDecoder'); pathDecoder.setClaudeBasePathOverride(overrideRoot); fs.writeFileSync( - path.join(overrideRoot, 'claude-devtools-config.json'), + configPath, JSON.stringify({ notifications: { notifyOnInboxMessages: true, @@ -42,5 +43,14 @@ describe('ConfigManager notification config shape', () => { expect(config.notifications.autoResumeOnRateLimit).toBe(true); expect(config.notifications.notifyOnTeamLaunched).toBe(false); expect('notifyOnInboxMessages' in config.notifications).toBe(false); + + await vi.waitFor(() => { + const persisted = JSON.parse(fs.readFileSync(configPath, 'utf8')) as { + notifications: Record; + }; + expect(persisted.notifications.autoResumeOnRateLimit).toBe(true); + expect(persisted.notifications.notifyOnTeamLaunched).toBe(false); + expect('notifyOnInboxMessages' in persisted.notifications).toBe(false); + }); }); }); diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts index 1cfb0a4a..2b425cd9 100644 --- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts +++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts @@ -779,16 +779,20 @@ describe('TeamProvisioningService prepare/auth behavior', () => { ); }); - it('validates the generated agent-teams MCP server directly over stdio', async () => { - const svc = new TeamProvisioningService(); - const configPath = writeMcpConfig(tempRoot, { - 'agent-teams': getRealAgentTeamsMcpLaunchSpec(), - }); + it( + 'validates the generated agent-teams MCP server directly over stdio', + async () => { + const svc = new TeamProvisioningService(); + const configPath = writeMcpConfig(tempRoot, { + 'agent-teams': getRealAgentTeamsMcpLaunchSpec(), + }); - await expect( - (svc as any).validateAgentTeamsMcpRuntime('/fake/claude', tempRoot, process.env, configPath) - ).resolves.toBeUndefined(); - }); + await expect( + (svc as any).validateAgentTeamsMcpRuntime('/fake/claude', tempRoot, process.env, configPath) + ).resolves.toBeUndefined(); + }, + 45_000 + ); it('fails validation when the generated MCP config has no agent-teams entry', async () => { const svc = new TeamProvisioningService(); diff --git a/test/renderer/components/team/dialogs/LaunchTeamDialog.test.ts b/test/renderer/components/team/dialogs/LaunchTeamDialog.test.ts index 59e152cb..bdc4ef8a 100644 --- a/test/renderer/components/team/dialogs/LaunchTeamDialog.test.ts +++ b/test/renderer/components/team/dialogs/LaunchTeamDialog.test.ts @@ -24,6 +24,7 @@ const storeState = { }; vi.mock('@renderer/api', () => ({ + isElectronMode: () => true, api: { getProjects: vi.fn(async () => [ {