Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ jobs:
- name: Build web UI
run: bun run build:web

- name: Run tests
run: bun test --timeout 20000
if: matrix.os == 'windows-latest'

- name: Run tests
run: bun test
if: matrix.os != 'windows-latest'

- name: Run tests with coverage
if: matrix.os == 'ubuntu-latest'
Expand Down
14 changes: 11 additions & 3 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import {
import type { RegistrySourceType } from '../types/database';
import type { RegistryCapability } from '../types/registry';
import { checkForUpdates } from './utils/version-check';
import {
cliSubcommandFromArgv,
shouldSkipVersionCheck,
} from './utils/cli-subcommand';
import { VERSION } from '../version';
import { setFlags, ExitCode, error } from './ui';

Expand Down Expand Up @@ -53,8 +57,12 @@ if (process.argv[2] === '__server__') {
(async () => {
try {
// Start version check in the background while the command runs
const isUpgradeCommand = process.argv[2] === 'upgrade';
const updateCheckPromise = isUpgradeCommand ? Promise.resolve(null) : checkForUpdates();
const subcommand = cliSubcommandFromArgv(process.argv);
// Hook subprocesses (activity-ingest) must own stdout — only valid JSON for gate events.
const skipVersionCheck = shouldSkipVersionCheck(subcommand);
const updateCheckPromise = skipVersionCheck
? Promise.resolve(null)
: checkForUpdates();

const program = new Command();

Expand Down Expand Up @@ -436,7 +444,7 @@ if (process.argv[2] === '__server__') {

// Show update notice after the command completes (if a newer version is available)
const updateInfo = await updateCheckPromise;
if (updateInfo?.hasUpdate) {
if (!skipVersionCheck && updateInfo?.hasUpdate) {
console.log(`\n A new version of capa is available: ${updateInfo.latestVersion} (current: ${updateInfo.currentVersion})`);
console.log(' Run "capa upgrade" to update.\n');
}
Expand Down
36 changes: 36 additions & 0 deletions src/cli/utils/__tests__/cli-subcommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, it } from 'bun:test';
import {
cliSubcommandFromArgv,
shouldSkipVersionCheck,
} from '../cli-subcommand';

describe('cliSubcommandFromArgv', () => {
it('returns the first non-option token', () => {
expect(cliSubcommandFromArgv(['bun', 'capa', 'activity-ingest'])).toBe(
'activity-ingest',
);
expect(
cliSubcommandFromArgv([
'bun',
'capa',
'--no-color',
'activity-ingest',
'--event',
'beforeFileRead',
]),
).toBe('activity-ingest');
expect(cliSubcommandFromArgv(['bun', 'capa', '-q', 'upgrade'])).toBe(
'upgrade',
);
expect(cliSubcommandFromArgv(['bun', 'capa', 'install'])).toBe('install');
});
});

describe('shouldSkipVersionCheck', () => {
it('skips for upgrade and activity-ingest only', () => {
expect(shouldSkipVersionCheck('upgrade')).toBe(true);
expect(shouldSkipVersionCheck('activity-ingest')).toBe(true);
expect(shouldSkipVersionCheck('install')).toBe(false);
expect(shouldSkipVersionCheck(undefined)).toBe(false);
});
});
13 changes: 13 additions & 0 deletions src/cli/utils/cli-subcommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Resolve the CLI subcommand from raw argv (after `node` / `bun` and script path).
* Global root flags may precede the command token (`capa --no-color activity-ingest`).
*/
export function cliSubcommandFromArgv(argv: string[]): string | undefined {
const rest = argv.slice(2);
return rest.find((token) => token.length > 0 && !token.startsWith('-'));
}

/** Subcommands that must not append human-readable text to stdout after they run. */
export function shouldSkipVersionCheck(subcommand: string | undefined): boolean {
return subcommand === 'upgrade' || subcommand === 'activity-ingest';
}
16 changes: 16 additions & 0 deletions src/cli/utils/wrap/__tests__/sessions.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { describe, it, expect } from 'bun:test';
import {
findWrapPids,
findWrapPidsForProject,
isPidRunning,
stopAllWrapSessions,
commandLineMatchesProject,
Expand All @@ -18,6 +22,18 @@ describe('wrap process discovery', () => {
expect(pids.includes(process.pid)).toBe(false);
});

it('findWrapPidsForProject skips process scan when project has no wrap workspace', async () => {
const dir = mkdtempSync(join(tmpdir(), 'capa-wrap-scan-skip-'));
try {
const pids = await findWrapPidsForProject(dir);
expect(pids).toEqual([]);
} finally {
try {
rmSync(dir, { recursive: true, force: true });
} catch {}
}
});

it('stopAllWrapSessions is a no-op when nothing is wrapping', async () => {
// May still find unrelated wraps on the machine; just ensure it resolves.
const n = await stopAllWrapSessions();
Expand Down
6 changes: 6 additions & 0 deletions src/cli/utils/wrap/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ export async function findWrapPidsForProject(realProjectPath: string): Promise<n
const real = resolve(realProjectPath);
const fromSessions = await pidsFromSessionFiles(real);
const workspacePaths = await listWorkspacePathsForProject(real);
// No capa wrap workspace for this project — skip a full process-table scan
// (notably slow on Windows via PowerShell/CIM). Session files + workspace
// markers are the normal sources of truth once wrap has run.
if (fromSessions.length === 0 && workspacePaths.length === 0) {
return [];
}
const procs = await listWrapProcesses();
const fromArgv = procs
.filter((p) => commandLineMatchesProject(p.commandLine, real, workspacePaths))
Expand Down
26 changes: 21 additions & 5 deletions src/db/__tests__/database.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ import { mkdtempSync, rmSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';

/** Windows CI can briefly keep SQLite files open after close(). */
function removeTempDirWithRetry(dir: string, attempts = 8): void {
for (let i = 0; i < attempts; i++) {
try {
rmSync(dir, { recursive: true, force: true });
return;
} catch (error: unknown) {
const code =
error && typeof error === 'object' && 'code' in error
? String((error as { code: unknown }).code)
: '';
if (code !== 'EBUSY' && code !== 'EPERM' && code !== 'ENOTEMPTY') {
throw error;
}
if (i === attempts - 1) return;
Bun.sleepSync(50 * (i + 1));
}
}
}

describe('CapaDatabase', () => {
let db: CapaDatabase;
let tempDir: string;
Expand All @@ -17,11 +37,7 @@ describe('CapaDatabase', () => {

afterEach(() => {
db.close();
try {
rmSync(tempDir, { recursive: true, force: true });
} catch (error: any) {
if (error?.code !== 'EBUSY') throw error;
}
removeTempDirWithRetry(tempDir);
});

describe('Project operations', () => {
Expand Down
Loading