Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import { mkdtemp, writeFile, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { resolveLibrarySymbol } from '../src/kicad/symlib.js';
import { bomSymbolDossier } from '../src/kicad/dossier.js';
import { dispatchTool, type RunContext } from '../src/agent/tools.js';
import { ObligationsLedger } from '../src/agent/ledger.js';
// Fixture stand-ins for the installed libraries: a passive with a derived
// symbol, a single-unit and a multi-unit gate (the SN74LVC2G17 trap from the
// lemondrop run), and a codec resolvable by MPN.
const DEVICE_LIB = `(kicad_symbol_lib (version 20251024) (generator test)
(symbol "R" (pin_numbers hide) (pin_names (offset 0))
(symbol "R_0_1" (rectangle (start -1.016 -2.54) (end 1.016 2.54)))
(symbol "R_1_1"
(pin passive line (at 0 3.81 270) (length 1.27) (name "~") (number "1"))
(pin passive line (at 0 -3.81 90) (length 1.27) (name "~") (number "2"))
)
)
(symbol "R_Small" (extends "R"))
)`;
const LOGIC_LIB = `(kicad_symbol_lib (version 20251024) (generator test)
(symbol "SN74LVC1G17" (pin_names (offset 1.016))
(symbol "SN74LVC1G17_1_1"
(pin input line (at -7.62 0 0) (length 2.54) (name "A") (number "2"))
(pin output line (at 7.62 0 180) (length 2.54) (name "Y") (number "4"))
(pin power_in line (at 0 7.62 270) (length 2.54) (name "VCC") (number "5"))
(pin power_in line (at 0 -7.62 90) (length 2.54) (name "GND") (number "3"))
)
)
(symbol "SN74LVC2G17" (pin_names (offset 1.016))
(symbol "SN74LVC2G17_1_1"
(pin input line (at -7.62 0 0) (length 2.54) (name "1A") (number "1"))
(pin output line (at 7.62 0 180) (length 2.54) (name "1Y") (number "6"))
)
(symbol "SN74LVC2G17_2_1"
(pin input line (at -7.62 0 0) (length 2.54) (name "2A") (number "3"))
(pin output line (at 7.62 0 180) (length 2.54) (name "2Y") (number "4"))
)
)
)`;
const AUDIO_LIB = `(kicad_symbol_lib (version 20251024) (generator test)
(symbol "TLV320AIC3100" (pin_names (offset 1.016))
(symbol "TLV320AIC3100_1_1"
(pin power_in line (at 0 7.62 270) (length 2.54) (name "AVDD") (number "1"))
(pin bidirectional line (at -7.62 0 0) (length 2.54) (name "SDA") (number "2"))
(pin input line (at -7.62 -2.54 0) (length 2.54) (name "SCL") (number "3"))
)
)
)`;
const MCU_LIB = `(kicad_symbol_lib (version 20251024) (generator test)
(symbol "STM32F103C8Tx" (pin_names (offset 1.016))
(symbol "STM32F103C8Tx_1_1"
(pin power_in line (at 0 7.62 270) (length 2.54) (name "VDD") (number "1"))
(pin bidirectional line (at -7.62 0 0) (length 2.54) (name "PA0") (number "2"))
)
)
)`;
const BOM = `| Refdes | Value | Footprint | MPN | Rationale |
| --- | --- | --- | --- | --- |
| U1 | codec | Package_DFN_QFN:X | TLV320AIC3100 | audio codec |
| U2 | buffer | Package_TO_SOT_SMD:X | SN74LVC2G17 | dual schmitt buffer |
| U3 | ghost | Package_QFP:X | XYZQ9999ZZ | not installed anywhere |
| U4 | SN74LVC1G17 | Package_TO_SOT_SMD:X | BOGUSMPN9999 | bogus MPN over a resolvable Value |
| U5 | mcu | Package_QFP:X | STM32F103C8T6 | family variant of the stock symbol |
| SW2 | R_Small | Button:X | | value fallback row |
| Y1 | 8M | Crystal:X | | name too short to search |
| R1 | 10k | Resistor_SMD:X | UNVERIFIED | passive, omitted |
| C3 | 100n | Capacitor_SMD:X | UNVERIFIED | passive, omitted |
`;
describe('pin dossier (R14: stage-4 entry pin facts)', () => {
let libDir: string;
beforeAll(async () => {
libDir = await mkdtemp(path.join(tmpdir(), 'copperhead-dossier-test-'));
await writeFile(path.join(libDir, 'Device.kicad_sym'), DEVICE_LIB, 'utf8');
await writeFile(path.join(libDir, 'Logic.kicad_sym'), LOGIC_LIB, 'utf8');
await writeFile(path.join(libDir, 'Audio.kicad_sym'), AUDIO_LIB, 'utf8');
await writeFile(path.join(libDir, 'MCU_ST_STM32F1.kicad_sym'), MCU_LIB, 'utf8');
});
afterAll(async () => {
await rm(libDir, { recursive: true, force: true });
});
describe('resolveLibrarySymbol unit count', () => {
it('single-unit symbol reports 1', async () => {
const r = await resolveLibrarySymbol('Device:R', [libDir]);
expect(r.status).toBe('ok');
if (r.status === 'ok') expect(r.units).toBe(1);
});
it('multi-unit symbol reports its unit count', async () => {
const r = await resolveLibrarySymbol('Logic:SN74LVC2G17', [libDir]);
expect(r.status).toBe('ok');
if (r.status === 'ok') expect(r.units).toBe(2);
});
it('derived symbol reports the base unit count with the base pins', async () => {
const r = await resolveLibrarySymbol('Device:R_Small', [libDir]);
expect(r.status).toBe('ok');
if (r.status === 'ok') {
expect(r.pins).toHaveLength(2);
expect(r.units).toBe(1);
}
});
});
describe('bomSymbolDossier', () => {
it('resolves MPN rows to lib_id and full pin table', async () => {
const d = await bomSymbolDossier(BOM, [libDir]);
expect(d).toContain('U1 (TLV320AIC3100): Audio:TLV320AIC3100 — 3 pin(s)');
expect(d).toContain('1=AVDD/power_in');
expect(d).toContain('2=SDA/bidirectional');
});
it('flags multi-unit symbols as ones the engine refuses', async () => {
const d = await bomSymbolDossier(BOM, [libDir]);
expect(d).toContain('MULTI-UNIT (2 units)');
expect(d).toMatch(/U2 \(SN74LVC2G17\).*refuses/);
});
it('falls back to the Value column when the MPN is empty', async () => {
const d = await bomSymbolDossier(BOM, [libDir]);
expect(d).toContain('SW2 (R_Small): Device:R_Small — 2 pin(s)');
});
it('searches the Value as a separate fallback when the MPN finds nothing', async () => {
const d = await bomSymbolDossier(BOM, [libDir]);
// A bogus MPN over a resolvable Value must not read as NO INSTALLED SYMBOL.
expect(d).toContain('U4 (BOGUSMPN9999): Logic:SN74LVC1G17 (matched by Value "SN74LVC1G17")');
expect(d).not.toMatch(/U4 .*NO INSTALLED SYMBOL/);
});
it('resolves a family-variant MPN instead of declaring it absent', async () => {
// Finding 1 (#202 review): search lacked the edit-distance tier the
// cross-library resolver had, so STM32F103C8T6 vs the installed
// STM32F103C8Tx rendered as a false NO INSTALLED SYMBOL under a
// machine-verified heading, and the prompt told the agent to substitute
// a correctly chosen part.
const d = await bomSymbolDossier(BOM, [libDir]);
expect(d).toContain('U5 (STM32F103C8T6): MCU_ST_STM32F1:STM32F103C8Tx');
expect(d).not.toMatch(/U5 .*NO INSTALLED SYMBOL/);
});
it('discloses rows whose only name is too short to search', async () => {
const d = await bomSymbolDossier(BOM, [libDir]);
expect(d).toContain('NOT SEARCHED (name shorter than 3 chars)');
expect(d).toContain('Y1 (8M)');
expect(d).not.toMatch(/Y1 .*NO INSTALLED SYMBOL/);
});
it('names parts nothing matches instead of omitting them', async () => {
const d = await bomSymbolDossier(BOM, [libDir]);
expect(d).toMatch(/U3 \(XYZQ9999ZZ\): NO INSTALLED SYMBOL/);
});
it('omits pure-passive refdes classes', async () => {
const d = await bomSymbolDossier(BOM, [libDir]);
expect(d).not.toMatch(/^- R1[ ,(]/m);
expect(d).not.toMatch(/^- C3[ ,(]/m);
expect(d).not.toContain('(10k)');
expect(d).not.toContain('(100n)');
});
it('discloses size-cap overflow within the cap, never silently', async () => {
const d = await bomSymbolDossier(BOM, [libDir], { maxChars: 700 });
expect(d).toContain('NOT INCLUDED (size cap 700 chars)');
expect(d).toContain('symbol_pins');
// The bound covers the COMPLETE rendered block, disclosure included.
expect(d.length).toBeLessThanOrEqual(700);
// every non-passive part appears — rendered, disclosed by name, or
// covered by the explicit "…and N more" truncation count
for (const who of ['U1', 'U2', 'U3', 'U4', 'SW2']) {
if (!d.includes(who)) expect(d).toMatch(/…and \d+ more/);
}
});
it('degrades to empty on no dirs, no rows, and garbage input', async () => {
expect(await bomSymbolDossier(BOM, [])).toBe('');
expect(await bomSymbolDossier('no table here', [libDir])).toBe('');
expect(await bomSymbolDossier(BOM, [path.join(libDir, 'nonexistent')])).toBe('');
});
});
describe('symbol_pins tool', () => {
// Typed literal (only the two stub collaborators are cast), so a new
// required RunContext field fails typecheck here instead of at runtime.
const ctx = (): RunContext => ({
repoRoot: '/nonexistent',
config: {} as RunContext['config'],
transcript: { event: async () => {} } as unknown as RunContext['transcript'],
ledger: new ObligationsLedger(),
runId: 'test',
interactive: false,
confirm: async () => true,
editsUnlocked: false,
changeId: null,
proposalValidated: false,
filesTouched: new Set(),
decisions: [],
lastErc: null,
lastDrc: null,
lastLegibility: null,
lastScore: null,
repairCycles: 0,
finishRequest: null,
});
beforeAll(() => {
vi.stubEnv('KICAD_SYMBOL_DIR', libDir);
});
afterAll(() => {
vi.unstubAllEnvs();
});
it('reports pins and unit count for a resolved lib_id', async () => {
const out = await dispatchTool(ctx(), 'symbol_pins', { lib_id: 'Logic:SN74LVC1G17' });
expect(out).toContain('4 pin(s), 1 unit(s)');
expect(out).toContain('2: A · input');
expect(out).toContain('5: VCC · power_in');
expect(out).not.toContain('WARNING');
});
it('warns on a multi-unit symbol', async () => {
const out = await dispatchTool(ctx(), 'symbol_pins', { lib_id: 'Logic:SN74LVC2G17' });
expect(out).toContain('2 unit(s)');
expect(out).toContain('refuses multi-unit symbols');
});
it('redirects a wrong-library guess to where the symbol lives', async () => {
const out = await dispatchTool(ctx(), 'symbol_pins', { lib_id: 'Device:SN74LVC1G17' });
expect(out).toContain('does not resolve');
expect(out).toContain('installed as: Logic:SN74LVC1G17');
});
it('handles an uninstalled library nickname', async () => {
const out = await dispatchTool(ctx(), 'symbol_pins', { lib_id: 'Nowhere:TLV320AIC3100' });
expect(out).toContain('does not resolve');
expect(out).toContain('installed as: Audio:TLV320AIC3100');
});
it('offers same-library closest names for a near-miss with no cross-library hit', async () => {
const out = await dispatchTool(ctx(), 'symbol_pins', { lib_id: 'Device:R_Smal' });
expect(out).toContain('does not exist in that library');
expect(out).toContain('closest in that library: R_Small');
});
it('reports a nickname and name that exist nowhere without inventing a location', async () => {
const out = await dispatchTool(ctx(), 'symbol_pins', { lib_id: 'Nowhere:QQQZZZ9999' });
expect(out).toContain('no library named "Nowhere" is installed');
expect(out).toContain('no installed symbol matches "QQQZZZ9999"');
});
});
describe('search_symbols tool', () => {
beforeAll(() => {
vi.stubEnv('KICAD_SYMBOL_DIR', libDir);
});
afterAll(() => {
vi.unstubAllEnvs();
});
const ctx = (): RunContext => ({
repoRoot: '/nonexistent',
config: {} as RunContext['config'],
transcript: { event: async () => {} } as unknown as RunContext['transcript'],
ledger: new ObligationsLedger(),
runId: 'test',
interactive: false,
confirm: async () => true,
editsUnlocked: false,
changeId: null,
proposalValidated: false,
filesTouched: new Set(),
decisions: [],
lastErc: null,
lastDrc: null,
lastLegibility: null,
lastScore: null,
repairCycles: 0,
finishRequest: null,
});
it('lists ranked lib_ids for an installed part', async () => {
const out = await dispatchTool(ctx(), 'search_symbols', { query: 'SN74LVC1G17' });
expect(out).toContain('installed symbols matching "SN74LVC1G17"');
expect(out).toContain('Logic:SN74LVC1G17');
});
it('states a genuine miss with the searched directories', async () => {
const out = await dispatchTool(ctx(), 'search_symbols', { query: 'ZZZQ9999XY' });
expect(out).toContain('no installed symbol matches "ZZZQ9999XY"');
expect(out).toContain('not capturable on this machine as named');
});
});
describe('verify_symbols counts wrong-library findings as issues', () => {
beforeAll(() => {
vi.stubEnv('KICAD_SYMBOL_DIR', libDir);
});
afterAll(() => {
vi.unstubAllEnvs();
});
it('reports 1 issue(s) for a part installed under another nickname', async () => {
// Finding 3 (#202 review): the found-elsewhere finding carried kind
// 'no-library', which the mismatch counter excludes, so the summary said
// "0 issue(s) to reconcile" directly above a printed issue.
const repo = await mkdtemp(path.join(tmpdir(), 'copperhead-verifycount-'));
try {
await writeFile(
path.join(repo, 'x.kicad_sch'),
`(kicad_sch (version 20251024) (generator test)
(lib_symbols
(symbol "Nowhere:SN74LVC1G17"
(symbol "SN74LVC1G17_1_1"
(pin input line (at 0 0 0) (length 1.27) (name "A") (number "2"))
)
)
)
)`,
'utf8',
);
const ctx: RunContext = {
repoRoot: repo,
config: { schematic: 'x.kicad_sch' } as RunContext['config'],
transcript: { event: async () => {} } as unknown as RunContext['transcript'],
ledger: new ObligationsLedger(),
runId: 'test',
interactive: false,
confirm: async () => true,
editsUnlocked: false,
changeId: null,
proposalValidated: false,
filesTouched: new Set(),
decisions: [],
lastErc: null,
lastDrc: null,
lastLegibility: null,
lastScore: null,
repairCycles: 0,
finishRequest: null,
};
const out = await dispatchTool(ctx, 'verify_symbols', {});
expect(out).toContain('1 issue(s) to reconcile');
expect(out).toContain('[wrong-library]');
} finally {
await rm(repo, { recursive: true, force: true });
}
});
});
});
696b6f9f0This file is pinned to the selected release. Download the original to open it in your design tool.