Skip to content
Draft
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
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pnpm-lock.yaml
dist/
node_modules/
CHANGELOG.md
CLAUDE.md
10 changes: 8 additions & 2 deletions __tests__/e2e/system-check-failure.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSession } from './testing-framework/index.js';
import { IS_WINDOWS } from './testing-framework/env.js';
import { dirname } from 'node:path';

describe('when system check fails', () => {
Expand All @@ -18,8 +19,13 @@ describe('when system check fails', () => {
expect(session.snapshot()).toMatchSnapshot('system-check-failure');
});

it('shows error when node is not on PATH', async () => {
// PATH with git but not node — CLI still runs via absolute path
/**
* @todo Make this test work on Windows. It currently relies on `/usr/bin`
* as a PATH that contains git but not node, which has no direct Windows
* equivalent. Constructing one portably is non-trivial because git's
* install location varies across Windows setups.
*/
it.skipIf(IS_WINDOWS)('shows error when node is not on PATH', async () => {
using session = createSession({ systemPath: '/usr/bin' });

await session.waitForText('Start setup');
Expand Down
3 changes: 3 additions & 0 deletions __tests__/e2e/testing-framework/env.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
export { AUTH_CALLBACK_PORT } from '@lib/auth.js';

/** `true` when the test suite is running on Windows. */
export const IS_WINDOWS = process.platform === 'win32';

/**
* Baseline environment variables injected into every e2e terminal session.
*
Expand Down
21 changes: 21 additions & 0 deletions __tests__/e2e/testing-framework/mocks/binaries/fs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { writeFileSync, chmodSync } from 'node:fs';
import { join } from 'node:path';
import { IS_WINDOWS } from '../../env.js';

export const writeMockBinary = IS_WINDOWS ? writeWindowsBinary : writeUnixBinary;

function writeUnixBinary(dir: string, name: string, script: string): void {
const filePath = join(dir, name);
writeFileSync(filePath, script, 'utf-8');
chmodSync(filePath, 0o755);
}

function writeWindowsBinary(dir: string, name: string, script: string): void {
const jsPath = join(dir, `${name}.js`);
writeFileSync(jsPath, script.replace(/^#!.*\n/, ''), 'utf-8');
writeFileSync(join(dir, `${name}.cmd`), `@node "%~dp0${name}.js" %*\r\n`, 'utf-8');
}

export const writeMockOpenStub = IS_WINDOWS
? (dir: string) => writeFileSync(join(dir, 'open.cmd'), '@exit /b 0\r\n', 'utf-8')
: (dir: string) => writeUnixBinary(dir, 'open', '#!/bin/sh\nexit 0\n');
12 changes: 4 additions & 8 deletions __tests__/e2e/testing-framework/mocks/binaries/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { writeFileSync, mkdirSync, chmodSync } from 'node:fs';
import { mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { CLAUDE_SCRIPT } from './claude.js';
import { CURSOR_SCRIPT } from './cursor.js';
import { CODEX_SCRIPT } from './codex.js';
import { writeMockBinary, writeMockOpenStub } from './fs.js';

/** Filename the mock IDE binary writes the chat prompt to. */
export const CHAT_PROMPT_FILE = '.e2e-chat-prompt';

/** Filename the mock IDE binary writes the onboarding invocation JSON to. */
export const ONBOARDING_INVOCATION_FILE = '.e2e-onboarding-invocation';

function writeMockBinary(dir: string, name: string, script: string): void {
const filePath = join(dir, name);
writeFileSync(filePath, script, 'utf-8');
chmodSync(filePath, 0o755);
}

/**
* Creates a directory of executable mock IDE binaries (`claude`, `cursor`,
* `codex`, `open`) that the wizard will find on `PATH` during e2e tests.
Expand All @@ -34,7 +29,8 @@ export function createMockBinDir(dir: string): string {
writeMockBinary(binDir, 'claude', CLAUDE_SCRIPT);
writeMockBinary(binDir, 'cursor', CURSOR_SCRIPT);
writeMockBinary(binDir, 'codex', CODEX_SCRIPT);
writeMockBinary(binDir, 'open', '#!/bin/sh\nexit 0\n');

writeMockOpenStub(binDir);

return binDir;
}
4 changes: 2 additions & 2 deletions __tests__/e2e/testing-framework/session-factory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { mkdtempSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { delimiter, join } from 'node:path';
import { TerminalSession } from './terminal/index.js';
import { createProjectDir, type ProjectType } from '../../shared/project-scaffold/index.js';

Expand Down Expand Up @@ -56,7 +56,7 @@ export function createSession({
const { path: projectDir } = createProjectDir(project);

const sessionEnv: Record<string, string> = {
PATH: `${mockBinDir}:${systemPath ?? process.env.PATH}`,
PATH: `${mockBinDir}${delimiter}${systemPath ?? process.env.PATH}`,
...env,
};

Expand Down
9 changes: 8 additions & 1 deletion __tests__/e2e/testing-framework/terminal/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { stripAnsi } from './strip-ansi.js';
import { renderScreen, normalizeSnapshot } from './screen-buffer.js';
import { E2E_BASE_ENV } from '../env.js';
import { E2E_BASE_ENV, IS_WINDOWS } from '../env.js';
import { resolveKey, type Modifiers } from '../../../shared/key-map.js';

const CLI_PATH = resolve(import.meta.dirname, '../../../../dist/bin/cli.js');
Expand Down Expand Up @@ -85,8 +85,15 @@ export class TerminalSession {
...process.env,
...E2E_BASE_ENV,
...env,

HOME: isolatedTmpDir,
TMPDIR: isolatedTmpDir,

...(IS_WINDOWS && {
USERPROFILE: isolatedTmpDir,
TEMP: isolatedTmpDir,
TMP: isolatedTmpDir,
}),
},
});

Expand Down
5 changes: 5 additions & 0 deletions __tests__/shared/platform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export const isWindows = process.platform === 'win32';

export function perPlatform<T>(options: { windows: T; unix: T }): T {
return isWindows ? options.windows : options.unix;
}
23 changes: 18 additions & 5 deletions __tests__/shared/project-scaffold/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { perPlatform } from '../platform.js';
import { SCAFFOLDS } from './scaffolds.js';
import type { ProjectType } from './types.js';

Expand All @@ -9,10 +12,15 @@ export type { ProjectType };
* project scaffold. Supports `Symbol.dispose` for automatic cleanup.
*
* @remarks
* Uses a hardcoded `/tmp/` prefix instead of `os.tmpdir()`. On macOS
* `tmpdir()` returns `/var/folders/…` which is longer than Linux's `/tmp/`,
* shifting column alignment in the VT100 screen buffer and breaking e2e
* snapshot assertions across platforms.
* Uses `/tmp/` on Unix instead of `os.tmpdir()`. On macOS `tmpdir()`
* returns `/var/folders/…` which is longer than Linux's `/tmp/`, shifting
* column alignment in the VT100 screen buffer and breaking e2e snapshot
* assertions across platforms. On Windows, `/tmp/` doesn't exist so we
* fall back to `os.tmpdir()`.
*
* @todo The Windows `tmpdir()` path is longer than `/tmp/`, producing
* different VT100 column alignment and incompatible e2e snapshots. When
* Windows CI is added, use a fixed-length prefix on all platforms.
*
* @param type - A named scaffold, or `null` for an empty directory.
* @defaultValue `'react'`
Expand All @@ -26,7 +34,12 @@ export type { ProjectType };
* ```
*/
export function createProjectDir(type: ProjectType = 'react') {
const dir = mkdtempSync('/tmp/wizard-test-');
const prefix = perPlatform({
windows: join(tmpdir(), 'wizard-test-'),
unix: '/tmp/wizard-test-',
});

const dir = mkdtempSync(prefix);
SCAFFOLDS[type](dir);

return {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"typecheck": "tsc --noEmit",
"qa": "pnpm typecheck && pnpm lint && pnpm test",
"clear": "./scripts/clean-dev-env.sh",
"prepare": "husky"
"prepare": "git config --local core.symlinks true && husky"
},
"dependencies": {
"@inkjs/ui": "^2.0.0",
Expand Down
13 changes: 7 additions & 6 deletions vitest.config.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';

export default defineConfig({
resolve: {
alias: {
'@commands': new URL('./src/commands', import.meta.url).pathname,
'@frameworks': new URL('./src/frameworks', import.meta.url).pathname,
'@integrations': new URL('./src/integrations', import.meta.url).pathname,
'@providers': new URL('./src/providers', import.meta.url).pathname,
'@ui': new URL('./src/ui', import.meta.url).pathname,
'@lib': new URL('./src/lib', import.meta.url).pathname,
'@commands': fileURLToPath(new URL('./src/commands', import.meta.url)),
'@frameworks': fileURLToPath(new URL('./src/frameworks', import.meta.url)),
'@integrations': fileURLToPath(new URL('./src/integrations', import.meta.url)),
'@providers': fileURLToPath(new URL('./src/providers', import.meta.url)),
'@ui': fileURLToPath(new URL('./src/ui', import.meta.url)),
'@lib': fileURLToPath(new URL('./src/lib', import.meta.url)),
},
},
test: {
Expand Down
17 changes: 9 additions & 8 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vitest/config';

const isCI = !!process.env.CI;

export default defineConfig({
resolve: {
alias: {
'@commands': new URL('./src/commands', import.meta.url).pathname,
'@features': new URL('./src/features', import.meta.url).pathname,
'@frameworks': new URL('./src/frameworks', import.meta.url).pathname,
'@integrations': new URL('./src/integrations', import.meta.url).pathname,
'@providers': new URL('./src/providers', import.meta.url).pathname,
'@shared-kernel': new URL('./src/shared-kernel', import.meta.url).pathname,
'@ui': new URL('./src/ui', import.meta.url).pathname,
'@lib': new URL('./src/lib', import.meta.url).pathname,
'@commands': fileURLToPath(new URL('./src/commands', import.meta.url)),
'@features': fileURLToPath(new URL('./src/features', import.meta.url)),
'@frameworks': fileURLToPath(new URL('./src/frameworks', import.meta.url)),
'@integrations': fileURLToPath(new URL('./src/integrations', import.meta.url)),
'@providers': fileURLToPath(new URL('./src/providers', import.meta.url)),
'@shared-kernel': fileURLToPath(new URL('./src/shared-kernel', import.meta.url)),
'@ui': fileURLToPath(new URL('./src/ui', import.meta.url)),
'@lib': fileURLToPath(new URL('./src/lib', import.meta.url)),
},
},
test: {
Expand Down