Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
696b6f9f0This file is pinned to the selected release. Download the original to open it in your design tool.
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdir, mkdtemp, rm, writeFile, chmod } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import {
resolveKicadCli,
resetKicadCliCache,
setKicadFallbackBinaries,
kicadCliVersion,
KicadCliBadOverrideError,
KicadCliMissingError,
} from '../src/kicad/cli.js';
/**
* Binary resolution is pure local logic (an env var plus filesystem probes),
* so it is testable without KiCad installed. `check` depends on it, and check
* is contractually LLM-free and network-free: nothing here reaches out.
*/
describe('kicad-cli binary resolution', () => {
const saved = process.env.COPPERHEAD_KICAD_CLI;
let dir: string;
beforeEach(async () => {
resetKicadCliCache();
dir = await mkdtemp(path.join(tmpdir(), 'copperhead-kicad-'));
});
afterEach(async () => {
if (saved === undefined) delete process.env.COPPERHEAD_KICAD_CLI;
else process.env.COPPERHEAD_KICAD_CLI = saved;
resetKicadCliCache();
setKicadFallbackBinaries();
await rm(dir, { recursive: true, force: true });
});
it('falls back to the PATH name when no override is set', () => {
delete process.env.COPPERHEAD_KICAD_CLI;
expect(resolveKicadCli()).toBe('kicad-cli');
});
it('honours COPPERHEAD_KICAD_CLI when the path exists', async () => {
const bin = path.join(dir, 'kicad-cli');
await writeFile(bin, '#!/bin/sh\nexit 0\n', 'utf8');
await chmod(bin, 0o755);
process.env.COPPERHEAD_KICAD_CLI = bin;
expect(resolveKicadCli()).toBe(bin);
});
it('trims surrounding whitespace in the override', async () => {
const bin = path.join(dir, 'kicad-cli');
await writeFile(bin, '#!/bin/sh\nexit 0\n', 'utf8');
process.env.COPPERHEAD_KICAD_CLI = ` ${bin} `;
expect(resolveKicadCli()).toBe(bin);
});
it('refuses, naming the bad path, when the override does not exist', () => {
// Regression: this used to fall through to a bare PATH lookup, so a typo
// produced "kicad-cli not found on PATH" plus advice to set the very
// variable the user had already set.
const missing = path.join(dir, 'typo', 'kicad-cli');
process.env.COPPERHEAD_KICAD_CLI = missing;
expect(() => resolveKicadCli()).toThrow(KicadCliBadOverrideError);
try {
resolveKicadCli();
} catch (err) {
const e = err as KicadCliBadOverrideError;
expect(e.message).toContain(missing);
// It must not tell you to set what you already set.
expect(e.message).not.toMatch(/not found on PATH/);
}
});
it('treats an empty or whitespace-only override as unset', () => {
process.env.COPPERHEAD_KICAD_CLI = ' ';
expect(resolveKicadCli()).toBe('kicad-cli');
});
it('reports the binary as missing when PATH has no kicad-cli and no bundle matches', async () => {
// Drives the real ENOENT chain: PATH lookup fails, fallbackAfterMissing
// finds no macOS app bundle, and the run is refused with the install
// instructions rather than a raw spawn error.
delete process.env.COPPERHEAD_KICAD_CLI;
// The probe list is emptied rather than left to the host: with the real
// list, this assertion would fail on any macOS machine that has KiCad in
// /Applications, because the fallback would resolve and the run succeed.
setKicadFallbackBinaries([]);
const savedPath = process.env.PATH;
process.env.PATH = dir; // an empty directory: nothing resolvable on it
try {
await expect(kicadCliVersion()).rejects.toBeInstanceOf(KicadCliMissingError);
} finally {
process.env.PATH = savedPath;
}
});
it('falls back to an app-bundle path when PATH has no kicad-cli', async () => {
// The macOS install case, made testable on every platform by pointing the
// probe at a fixture. Covers the retry: the first spawn ENOENTs on the
// bare PATH name, the second runs the resolved bundle binary.
delete process.env.COPPERHEAD_KICAD_CLI;
const bundle = path.join(dir, 'KiCad.app', 'Contents', 'MacOS', 'kicad-cli');
await mkdir(path.dirname(bundle), { recursive: true });
await writeFile(bundle, '#!/bin/sh\necho "9.0.1"\n', 'utf8');
await chmod(bundle, 0o755);
setKicadFallbackBinaries([path.join(dir, 'absent', 'kicad-cli'), bundle]);
const savedPath = process.env.PATH;
process.env.PATH = dir;
try {
expect(await kicadCliVersion()).toBe('9.0.1');
// ...and the bundle path is cached, so the miss is not re-paid per call.
expect(resolveKicadCli()).toBe(bundle);
} finally {
process.env.PATH = savedPath;
}
});
it('refuses when an override that existed at resolve time disappears mid-session', async () => {
// The refusal only means something if a fallback was available to take:
// a working `kicad-cli` sits on PATH under a different name from the
// override, and the bundle probes are emptied, so PATH is the one route
// that could still succeed. Reaching KicadCliMissingError therefore proves
// the fallback was never attempted, rather than attempted and also empty.
const onPath = path.join(dir, 'path-bin');
await mkdir(onPath, { recursive: true });
const pathBinary = path.join(onPath, 'kicad-cli');
await writeFile(pathBinary, '#!/bin/sh\necho "8.0.4"\n', 'utf8');
await chmod(pathBinary, 0o755);
const override = path.join(dir, 'custom-kicad'); // deliberately not "kicad-cli"
await writeFile(override, '#!/bin/sh\necho "9.9.9"\n', 'utf8');
await chmod(override, 0o755);
setKicadFallbackBinaries([]);
const savedPath = process.env.PATH;
process.env.PATH = onPath;
try {
process.env.COPPERHEAD_KICAD_CLI = override;
expect(resolveKicadCli()).toBe(override);
await rm(override, { force: true });
// An explicit override that vanished must not silently fall back, even
// though the PATH binary right there would have answered.
await expect(kicadCliVersion()).rejects.toBeInstanceOf(KicadCliMissingError);
// Control: that PATH binary really is usable, so the refusal above was a
// refusal and not a second missing binary.
delete process.env.COPPERHEAD_KICAD_CLI;
resetKicadCliCache();
expect(await kicadCliVersion()).toBe('8.0.4');
} finally {
process.env.PATH = savedPath;
}
});
it('caches the resolved binary until the cache is reset', async () => {
delete process.env.COPPERHEAD_KICAD_CLI;
expect(resolveKicadCli()).toBe('kicad-cli');
// A later override is ignored while the cache stands, then honoured after
// a reset — which is what makes these tests independent of each other.
const bin = path.join(dir, 'kicad-cli');
await writeFile(bin, '#!/bin/sh\nexit 0\n', 'utf8');
process.env.COPPERHEAD_KICAD_CLI = bin;
expect(resolveKicadCli()).toBe('kicad-cli');
resetKicadCliCache();
expect(resolveKicadCli()).toBe(bin);
});
});