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
24 changes: 23 additions & 1 deletion packages/cli/src/artifact.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,12 @@ describe('built public artifact', () => {
expect(readFileSync(join(cliRoot, 'runtime', 'web', 'index.html'), 'utf8')).toContain('<!doctype html>');
expect(readFileSync(join(cliRoot, 'packaging', 'systemd', 'codor.service'), 'utf8'))
.toContain('ExecStart=');
expect(statSync(join(outDir, 'bin', 'codor.mjs')).mode & 0o111).not.toBe(0);
// Windows has no POSIX executable permission bit — writeFileSync's `mode`
// option cannot set one there, so this property only exists to check on
// platforms where the filesystem tracks it.
if (process.platform !== 'win32') {
expect(statSync(join(outDir, 'bin', 'codor.mjs')).mode & 0o111).not.toBe(0);
}
});

it('contains no source, specs, fixtures, symlinks, or workspace protocols', () => {
Expand Down Expand Up @@ -210,4 +215,21 @@ describe('CLI test environment isolation', () => {
expect(String(failure.stderr)).not.toContain('never-print-this-value');
}
});

it('spawns a bare npm bin name by resolving its real entrypoint', () => {
// vitest in node_modules/.bin is a .CMD shim on Windows; this proves the
// wrapper resolves and runs it rather than spawning the bare name as-is.
const output = execFileSync(process.execPath, [wrapper, 'vitest', '--version'], { encoding: 'utf8' });
expect(output).toMatch(/^vitest\//);
});

it('falls back to spawning a bare non-package executable directly', () => {
// `node` names no npm package in this workspace; the wrapper must still
// spawn it directly rather than requiring a package.json bin entry.
const output = execFileSync(process.execPath, [wrapper, 'node', '-e', 'process.stdout.write("ok")'], {
encoding: 'utf8',
env: poisoned,
});
expect(output).toBe('ok');
});
});
11 changes: 8 additions & 3 deletions packages/cli/src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1264,12 +1264,14 @@ describe('@codor/cli', () => {
const fixtures = fileURLToPath(new URL('../fixtures/', import.meta.url));
const claudeTranscript = join(dir, 'claude-transcript.jsonl');
writeFileSync(
JSON.stringify(claudeTranscript).slice(1, -1),
claudeTranscript,
readFileSync(join(fixtures, 'claude-transcript.jsonl'), 'utf8'),
);
// JSON-escape the path before embedding it in a JSON string value — a raw
// Windows path's single backslashes are not valid JSON escape sequences.
const claudeRaw = readFileSync(join(fixtures, 'claude-stop.json'), 'utf8').replace(
'CLAUDE_TRANSCRIPT_PATH',
claudeTranscript,
JSON.stringify(claudeTranscript).slice(1, -1),
);
expect(parseMirrorHook('claude', claudeRaw)).toMatchObject({
type: 'mirror_turn',
Expand Down Expand Up @@ -1314,6 +1316,9 @@ describe('@codor/cli', () => {

// harn:assume empty-database-desk-uses-service-home ref=bootstrap-service-home-regression
// harn:assume empty-database-desk-seeds-tutorial-atomically ref=bootstrap-tutorial-regression
// Two full startCodor/close cycles plus native module load routinely exceed
// vitest's 5s default on Windows, where process and filesystem overhead is
// higher than on POSIX; this does not indicate a hang (observed ~6s).
it('bootstraps one home-rooted Desk and one durable Tutorial message', async () => {
const homeDir = join(dir, 'service-home');
mkdirSync(homeDir);
Expand Down Expand Up @@ -1407,7 +1412,7 @@ describe('@codor/cli', () => {
} finally {
await restarted.close();
}
});
}, 15000);
// harn:end empty-database-desk-seeds-tutorial-atomically
// harn:end empty-database-desk-uses-service-home

Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/runtime-install.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, sep } from 'node:path';
import { join, resolve, sep } from 'node:path';

import { afterEach, describe, expect, it } from 'vitest';

Expand All @@ -12,7 +12,10 @@ import {
} from './runtime-install.js';
import { type RuntimePaths } from './runtime-paths.js';

const HOME = '/home/u';
// Built through `resolve()`, the same call the code under test makes on a
// durable install root, so this fixture already carries a Windows drive
// prefix where the implementation's own normalization would add one.
const HOME = resolve(sep, 'home', 'u');
const DATA = join(HOME, '.codor');
const LOCATION = join(DATA, 'runtime');

Expand Down
7 changes: 6 additions & 1 deletion packages/cli/src/setup-tailscale.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { resolve, sep } from 'node:path';

import { describe, expect, it } from 'vitest';

import { configureTailscaleServe, resolveTailscale, tailscaleServeSupported } from './setup.js';
Expand All @@ -6,7 +8,10 @@ const APP_CLI = '/Applications/Tailscale.app/Contents/MacOS/Tailscale';

describe('resolveTailscale', () => {
it('returns the PATH hit when present', () => {
expect(resolveTailscale(() => '/usr/bin/tailscale', 'darwin', () => true)).toBe('/usr/bin/tailscale');
// resolveTailscale normalizes the PATH hit through resolve(); build the
// fixture the same way so the expectation matches on every platform.
const pathHit = resolve(sep, 'usr', 'bin', 'tailscale');
expect(resolveTailscale(() => pathHit, 'darwin', () => true)).toBe(pathHit);
});

it('falls back to the macOS app location when PATH misses but the app exists', () => {
Expand Down
32 changes: 31 additions & 1 deletion scripts/test-env.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,38 @@
#!/usr/bin/env node

import { spawn } from 'node:child_process';
import { createRequire } from 'node:module';
import { dirname, isAbsolute, join } from 'node:path';

const require = createRequire(import.meta.url);

const [command, ...args] = process.argv.slice(2);
if (command === undefined) {
process.stderr.write('usage: test-env.mjs <command> [args...]\n');
process.exit(2);
}

// Resolve a bare package bin name (e.g. "vitest") to its real entrypoint so
// it can run under `node` directly, bypassing the platform-specific
// node_modules/.bin shim (a .CMD file on Windows, which child_process.spawn
// refuses to execute without `shell: true`). Returns undefined for a bare
// command that names no such package (e.g. a system executable like `node`
// or `git`) — the caller falls back to spawning it directly, exactly as
// before this resolution existed. A command that already looks like a path
// (e.g. an absolute executable path a caller resolved itself) is spawned
// unchanged below and never reaches this function.
function resolveBinEntry(name) {
let packageJsonPath;
try {
packageJsonPath = require.resolve(`${name}/package.json`);
} catch {
return undefined;
}
const pkg = require(packageJsonPath);
const bin = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.[name];
return bin === undefined ? undefined : join(dirname(packageJsonPath), bin);
}

// harn:assume cli-tests-delete-inherited-codor-environment ref=cli-test-environment-wrapper
const env = { ...process.env };
const removed = Object.keys(env).filter((key) => key.startsWith('CODOR_')).sort();
Expand All @@ -16,7 +41,12 @@ if (removed.length > 0) {
process.stderr.write(`test-env: removed ${String(removed.length)} inherited CODOR_ variable(s): ${removed.join(', ')}\n`);
}

const child = spawn(command, args, { stdio: 'inherit', env });
const looksLikePath = command.includes('/') || command.includes('\\') || isAbsolute(command);
const binEntry = looksLikePath ? undefined : resolveBinEntry(command);
const [spawnCommand, spawnArgs] = binEntry === undefined
? [command, args]
: [process.execPath, [binEntry, ...args]];
const child = spawn(spawnCommand, spawnArgs, { stdio: 'inherit', env });
child.on('error', (error) => {
process.stderr.write(`test-env: failed to spawn ${command}: ${error.message}\n`);
process.exitCode = 1;
Expand Down