From f5da5793c714edaa3a6883bf80f4845a882415ed Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Tue, 6 Oct 2026 18:04:37 +0200 Subject: [PATCH 1/2] Show actionable SysConfig pin errors without Bun stack traces --- README.md | 3 +- cli/sysconfig/convert.mjs | 37 ++++++++++--------- npm/README.md | 3 +- package.json | 2 +- scripts/test-npm-package.mjs | 12 ++++++- tests/cli/sysconfig-from-circuit.test.mjs | 44 +++++++++++++++++++---- tests/cli/sysconfig.test.mjs | 4 +++ 7 files changed, 78 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 6598c5d..e9c3203 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,8 @@ Use tscircuit 0.0.2745 or newer, which exports these attributes as `is_output`, `is_using_internal_pullup`, and `is_configured_for_i2c_sda` / `is_configured_for_i2c_scl`. Capability flags such as `isGpio` alone do not select a pin function. Connected pins with missing or unsupported functions produce an error identifying the -MCU, physical pins, source ports and required TSX attributes. Missing MCU port +MCU name/part number, physical pin numbers/labels and required TSX attributes, +without generated record IDs or a converter stack trace. Missing MCU port records or traces referencing absent source ports also fail before a new SysConfig file is written; no behavior is inferred from signal names. If more than one supported MCU is present, select one with `--component U1` diff --git a/cli/sysconfig/convert.mjs b/cli/sysconfig/convert.mjs index e2f0263..5bbd451 100644 --- a/cli/sysconfig/convert.mjs +++ b/cli/sysconfig/convert.mjs @@ -11,20 +11,25 @@ if (!circuitJsonPath || !optionsPath || !outputPath || !configurationPath) { ); } -const circuitJson = JSON.parse(await readFile(circuitJsonPath, "utf8")); -const options = JSON.parse(await readFile(optionsPath, "utf8")); -const converter = new CircuitJsonToSysConfigConverter(circuitJson, options); -converter.runUntilFinished(); -const configuration = converter.getResolvedConfiguration(); -const cc2340 = configuration.cc2340; -const converterOptions = getValidationOptions({ configuration, circuitJson }); +try { + const circuitJson = JSON.parse(await readFile(circuitJsonPath, "utf8")); + const options = JSON.parse(await readFile(optionsPath, "utf8")); + const converter = new CircuitJsonToSysConfigConverter(circuitJson, options); + converter.runUntilFinished(); + const configuration = converter.getResolvedConfiguration(); + const cc2340 = configuration.cc2340; + const converterOptions = getValidationOptions({ configuration, circuitJson }); -await writeFile(outputPath, converter.getOutput().getString()); -await writeFile( - configurationPath, - JSON.stringify({ - target: cc2340 ? "cc2340" : "am2434", - component: configuration.component, - converterOptions, - }), -); + await writeFile(outputPath, converter.getOutput().getString()); + await writeFile( + configurationPath, + JSON.stringify({ + target: cc2340 ? "cc2340" : "am2434", + component: configuration.component, + converterOptions, + }), + ); +} catch (error) { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +} diff --git a/npm/README.md b/npm/README.md index 2690e96..c8e98c6 100644 --- a/npm/README.md +++ b/npm/README.md @@ -59,7 +59,8 @@ ti check-sysconfig ./board.circuit.tsx Use tscircuit 0.0.2745 or newer for TSX export. For supported CC2340 circuits, GPIO/I2C choices come from existing MCU `pinAttributes` exported to Circuit JSON. No separate request JSON is required or read implicitly. Missing connected-pin functions fail with physical pin and -source-port details and the required TSX attributes. Missing MCU port records +signal labels and the required TSX attributes, without generated record IDs +or a converter stack trace. Missing MCU port records or connected source-port records also fail before a new `.syscfg` is written. Use `--component U1` when selecting between multiple MCUs. Connected 32768 Hz two-terminal crystals on MCU pins 14/15 select the external diff --git a/package.json b/package.json index bd35be8..e258003 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@types/bun": "^1.4.0", "@types/react": "^19.2.17", "bun-match-svg": "^0.0.17", - "circuit-json-to-sysconfig": "github:tscircuit/circuit-json-to-sysconfig#cc3e6f3fa7fbfab99bf60db1c19ca1858995bdd0", + "circuit-json-to-sysconfig": "github:tscircuit/circuit-json-to-sysconfig#6b3383b5b6e8afd45d1b1aaa2979f5f73c98b814", "circuit-to-svg": "0.0.410", "tscircuit": "0.0.2745", "typescript": "^5.0.0" diff --git a/scripts/test-npm-package.mjs b/scripts/test-npm-package.mjs index fae50ca..26a7fc9 100644 --- a/scripts/test-npm-package.mjs +++ b/scripts/test-npm-package.mjs @@ -218,7 +218,17 @@ async function testCli({ command, cwd }) { args: ["generate-sysconfig", "incomplete-pins.circuit.json"], cwd, }), - /input_trace: missing source_port missing_input/, + (error) => { + assert.match( + error.message, + /Incomplete Circuit JSON pin records: 1 connection\(s\) reference missing pins[\s\S]*Rebuild the circuit/, + ); + assert.doesNotMatch( + error.message, + /input_trace|missing_input|throw new Error|\bat \S+ \(/, + ); + return true; + }, ); await assert.rejects(readFile(join(cwd, "incomplete-pins.syscfg")), { code: "ENOENT", diff --git a/tests/cli/sysconfig-from-circuit.test.mjs b/tests/cli/sysconfig-from-circuit.test.mjs index aeb672a..7f73a92 100644 --- a/tests/cli/sysconfig-from-circuit.test.mjs +++ b/tests/cli/sysconfig-from-circuit.test.mjs @@ -103,8 +103,13 @@ test("connected pins with missing roles fail without a request-file suggestion", writeFileSync(join(f.cwd, "board.circuit.json"), JSON.stringify(incomplete)); assert.equal(await f.run(["generate-sysconfig", "board.circuit.json"]), 1); assert.match(f.stderr.join("\n"), /U1.*CC2340R52E0RGER/); - assert.match(f.stderr.join("\n"), /pin 4.*pinAttributes/); - assert.doesNotMatch(f.stderr.join("\n"), /request file is missing/); + assert.match(f.stderr.join("\n"), /pin 4[\s\S]*pinAttributes/); + assert.doesNotMatch( + f.stderr.join("\n"), + /request file is missing|source_component_id|source_port_id|\bat \S+|throw new Error|convert\.mjs:\d/, + ); + assert.doesNotMatch(f.stderr.join("\n"), /\(mcu,|\(output,/); + assert.match(f.stderr.join("\n"), /isInput: true or isOutput: true/); assert.equal(existsSync(join(f.cwd, "board.syscfg")), false); }); @@ -121,7 +126,7 @@ test("a missing connected pin record fails instead of writing partial SysConfig" }); writeFileSync(join(f.cwd, "board.circuit.json"), JSON.stringify(incomplete)); assert.equal(await f.run(["generate-sysconfig", "board.circuit.json"]), 1); - assert.match(f.stderr.join("\n"), /input_trace: missing source_port input/); + assert.match(f.stderr.join("\n"), /1 connection\(s\) reference missing pins/); assert.match(f.stderr.join("\n"), /Rebuild the circuit/); assert.equal(existsSync(join(f.cwd, "board.syscfg")), false); }); @@ -133,10 +138,7 @@ test("an MCU without pin records reports the component and required attributes", JSON.stringify([circuit()[0]]), ); assert.equal(await f.run(["generate-sysconfig", "board.circuit.json"]), 1); - assert.match( - f.stderr.join("\n"), - /U1.*CC2340R52E0RGER.*no source_port records/, - ); + assert.match(f.stderr.join("\n"), /U1.*CC2340R52E0RGER.*no MCU pin records/); assert.match(f.stderr.join("\n"), /pinAttributes/); assert.equal(existsSync(join(f.cwd, "board.syscfg")), false); }); @@ -211,3 +213,31 @@ test("TSX pinAttributes export selected GPIO roles without a request file", asyn true, ); }); + +test("a TSX pin failure prints actionable labels without the converter stack", async (t) => { + const f = fixture(t); + writeFileSync( + join(f.cwd, "board.circuit.tsx"), + `export default () => ( + + + + + + + );`, + ); + assert.equal(await f.run(["generate-sysconfig", "board.circuit.tsx"]), 1); + const message = f.stderr.join("\n"); + assert.match(message, /U1 \(CC2340R52E0RGER\)/); + assert.match(message, /U1 pin 4 \(DIO11\)/); + assert.match(message, /U1 pin 6 \(DIO13\)/); + assert.match(message, /Update U1's TSX pinAttributes/); + assert.doesNotMatch( + message, + /source_component_\d|source_port_\d|\bat \S+|throw new Error/, + ); + assert.equal(existsSync(join(f.cwd, "board.syscfg")), false); +}); diff --git a/tests/cli/sysconfig.test.mjs b/tests/cli/sysconfig.test.mjs index aa4b3e0..fc525f7 100644 --- a/tests/cli/sysconfig.test.mjs +++ b/tests/cli/sysconfig.test.mjs @@ -323,6 +323,10 @@ test("missing pin functions are reported without requiring a firmware request fi const message = f.stderr.join("\n"); assert.match(message, /Unresolved CC2340 pin configuration/); assert.match(message, /pinAttributes/); + assert.doesNotMatch( + message, + /source_component_\d|source_port_\d|throw new Error|\bat \S+/, + ); assert.match(message, /pin 9/); assert.doesNotMatch( message, From 9ade9aaea9182709906745ebe3aaa1d014f5e6f0 Mon Sep 17 00:00:00 2001 From: AnasSarkiz Date: Tue, 6 Oct 2026 19:48:04 +0200 Subject: [PATCH 2/2] Snapshot actionable SysConfig CLI errors inline --- package.json | 4 +- tests/cli/sysconfig-errors-snapshot.test.ts | 130 ++++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 tests/cli/sysconfig-errors-snapshot.test.ts diff --git a/package.json b/package.json index e258003..2fc22d0 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "snapshot:update": "tsci snapshot --update", "start": "tsci dev", "test": "bun test", - "test:cli": "node --test tests/cli/*.test.mjs && bun test tests/cli/import-snapshot.test.tsx", + "test:cli": "node --test tests/cli/*.test.mjs && bun test tests/cli/import-snapshot.test.tsx tests/cli/sysconfig-errors-snapshot.test.ts", "typecheck": "tsc --noEmit", "format": "biome format --write", "format:check": "biome format .", @@ -35,7 +35,7 @@ "@types/bun": "^1.4.0", "@types/react": "^19.2.17", "bun-match-svg": "^0.0.17", - "circuit-json-to-sysconfig": "github:tscircuit/circuit-json-to-sysconfig#6b3383b5b6e8afd45d1b1aaa2979f5f73c98b814", + "circuit-json-to-sysconfig": "github:tscircuit/circuit-json-to-sysconfig#3da9ac4b464a84eac13615dc684da7105065c685", "circuit-to-svg": "0.0.410", "tscircuit": "0.0.2745", "typescript": "^5.0.0" diff --git a/tests/cli/sysconfig-errors-snapshot.test.ts b/tests/cli/sysconfig-errors-snapshot.test.ts new file mode 100644 index 0000000..0b85437 --- /dev/null +++ b/tests/cli/sysconfig-errors-snapshot.test.ts @@ -0,0 +1,130 @@ +import { expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { + existsSync, + mkdtempSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import type { CircuitJson } from "circuit-json"; + +function generateSysconfig({ + inputName, + source, +}: { + inputName: string; + source: string; +}) { + const cwd = mkdtempSync(join(tmpdir(), "ti-sysconfig-snapshot-")); + try { + symlinkSync( + new URL("../../node_modules/", import.meta.url), + join(cwd, "node_modules"), + "junction", + ); + writeFileSync( + join(cwd, "package.json"), + '{"private":true,"type":"module"}', + ); + writeFileSync(join(cwd, inputName), source); + const result = spawnSync( + "node", + [ + fileURLToPath(new URL("../../cli/ti.mjs", import.meta.url)), + "generate-sysconfig", + inputName, + ], + { cwd, encoding: "utf8" }, + ); + if (result.error) throw result.error; + return { + status: result.status, + stdout: result.stdout, + stderr: result.stderr, + sysconfigWritten: existsSync(join(cwd, "board.syscfg")), + }; + } finally { + rmSync(cwd, { recursive: true, force: true }); + } +} + +test("Circuit JSON error output names unresolved pins without IDs or a stack", () => { + const circuitJson: CircuitJson = [ + { + type: "source_component", + ftype: "simple_chip", + source_component_id: "source_component_123", + name: "U1", + manufacturer_part_number: "CC2340R52E0RGER", + }, + { + type: "source_port", + source_component_id: "source_component_123", + source_port_id: "source_port_456", + name: "DIO11", + pin_number: 4, + is_gpio: true, + }, + { + type: "source_port", + source_component_id: "source_component_123", + source_port_id: "source_port_789", + name: "DIO13", + pin_number: 6, + is_gpio: true, + }, + { + type: "source_trace", + source_trace_id: "source_trace_321", + connected_source_port_ids: ["source_port_456", "source_port_789"], + connected_source_net_ids: [], + }, + ]; + const { stderr, ...result } = generateSysconfig({ + inputName: "board.circuit.json", + source: JSON.stringify(circuitJson), + }); + expect(result).toEqual({ status: 1, stdout: "", sysconfigWritten: false }); + expect(stderr).toMatchInlineSnapshot(` + "Failed to generate SysConfig: SysConfig conversion failed: + Unresolved CC2340 pin configuration for U1 (CC2340R52E0RGER): + - U1 pin 4 (DIO11): no GPIO direction or supported peripheral selected + - U1 pin 6 (DIO13): no GPIO direction or supported peripheral selected + Update U1's TSX pinAttributes with the intended function for each listed pin: set exactly one of isInput: true or isOutput: true for GPIO, or activeCapability: "i2c_sda" / "i2c_scl" for I2C. + Datasheet capabilities such as isGpio describe what a pin supports; they do not select how this board uses it. + " + `); +}); + +test("TSX error output explains board choices without a request file or stack", () => { + const { stderr, ...result } = generateSysconfig({ + inputName: "board.circuit.tsx", + source: `export default () => ( + + + + + + + );`, + }); + expect(result).toEqual({ status: 1, stdout: "", sysconfigWritten: false }); + expect(stderr).toMatchInlineSnapshot(` + "Failed to generate SysConfig: SysConfig conversion failed: + Unresolved CC2340 pin configuration for U1 (CC2340R52E0RGER): + - U1 pin 4 (DIO11): no GPIO direction or supported peripheral selected + - U1 pin 6 (DIO13): no GPIO direction or supported peripheral selected + Update U1's TSX pinAttributes with the intended function for each listed pin: set exactly one of isInput: true or isOutput: true for GPIO, or activeCapability: "i2c_sda" / "i2c_scl" for I2C. + Datasheet capabilities such as isGpio describe what a pin supports; they do not select how this board uses it. + " + `); +});