From 9c03a85c2839fc4f25c8466cd33e7fe793437b64 Mon Sep 17 00:00:00 2001 From: Ayush Jhawar Date: Fri, 24 Oct 2025 23:31:03 +0530 Subject: [PATCH 1/5] Better html parse errors --- .../NormalComponent/NormalComponent.ts | 81 +++++++--- .../parts-engine-error-handling.test.tsx | 142 ++++++++++++++++++ 2 files changed, 200 insertions(+), 23 deletions(-) create mode 100644 tests/components/normal-components/parts-engine-error-handling.test.tsx diff --git a/lib/components/base-components/NormalComponent/NormalComponent.ts b/lib/components/base-components/NormalComponent/NormalComponent.ts index 99534b930..11992e2dd 100644 --- a/lib/components/base-components/NormalComponent/NormalComponent.ts +++ b/lib/components/base-components/NormalComponent/NormalComponent.ts @@ -1423,35 +1423,70 @@ export class NormalComponent< footprinterString: string | undefined, ) { if (this.props.doNotPlace) return {} - const cacheEngine = this.root?.platform?.localCacheEngine - const cacheKey = this._getPartsEngineCacheKey( - source_component, - footprinterString, - ) - if (cacheEngine) { - const cached = await cacheEngine.getItem(cacheKey) - if (cached) { - try { + + try { + const cacheEngine = this.root?.platform?.localCacheEngine + const cacheKey = this._getPartsEngineCacheKey( + source_component, + footprinterString, + ) + + if (cacheEngine) { + const cached = await cacheEngine.getItem(cacheKey) + if (cached) { return JSON.parse(cached) - } catch {} + } } - } - const result = await Promise.resolve( - partsEngine.findPart({ - sourceComponent: source_component, - footprinterString, - }), - ) - // Convert "Not found" to empty object before caching or returning - const supplierPartNumbers = result === "Not found" ? {} : result + const result = await Promise.resolve( + partsEngine.findPart({ + sourceComponent: source_component, + footprinterString, + }), + ) - if (cacheEngine) { - try { + // Validate response format + if (typeof result === "string" && result !== "Not found") { + throw new Error( + `Invalid response from parts engine: Expected JSON object but received string starting with "${result.substring(0, 50)}..."`, + ) + } + + const supplierPartNumbers = result === "Not found" ? {} : result + + if ( + typeof supplierPartNumbers !== "object" || + supplierPartNumbers === null || + Array.isArray(supplierPartNumbers) + ) { + throw new Error( + `Invalid supplier part numbers format: Expected object but received ${typeof supplierPartNumbers}`, + ) + } + + // Cache the result + if (cacheEngine) { await cacheEngine.setItem(cacheKey, JSON.stringify(supplierPartNumbers)) - } catch {} + } + + return supplierPartNumbers + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error) + debug( + `Error fetching supplier part numbers for ${source_component.name}: ${errorMessage}`, + ) + + // Insert error into Circuit JSON for user visibility + if (this.root?.db) { + this.root.db.pcb_placement_error.insert({ + error_type: "pcb_placement_error", + message: `Failed to fetch supplier part numbers: ${errorMessage}`, + } as any) + } + + return {} } - return supplierPartNumbers } doInitialPartsEngineRender(): void { diff --git a/tests/components/normal-components/parts-engine-error-handling.test.tsx b/tests/components/normal-components/parts-engine-error-handling.test.tsx new file mode 100644 index 000000000..4b8f2179f --- /dev/null +++ b/tests/components/normal-components/parts-engine-error-handling.test.tsx @@ -0,0 +1,142 @@ +import { test, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("parts engine handles JSON parsing errors", async () => { + const { circuit } = getTestFixture() + + // Mock parts engine that returns HTML instead of JSON (simulating Vercel error page) + const mockPartsEngine = { + findPart: async () => { + return "Internal Server Error" + }, + } as any + + circuit.add( + + + , + ) + + await circuit.renderUntilSettled() + + const sourceComponent = circuit.db.source_component.list()[0] + // Should return empty object instead of crashing + expect(sourceComponent.supplier_part_numbers).toEqual({}) + + // Should insert an error about the failure + const errors = circuit.db.pcb_placement_error.list() + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].message).toContain("Failed to fetch supplier part numbers") +}) + +test("parts engine handles network errors", async () => { + const { circuit } = getTestFixture() + + // Mock parts engine that throws an error + const mockPartsEngine = { + findPart: async () => { + throw new Error("Network request failed") + }, + } + + circuit.add( + + + , + ) + + await circuit.renderUntilSettled() + + const sourceComponent = circuit.db.source_component.list()[0] + // Should return empty object instead of crashing + expect(sourceComponent.supplier_part_numbers).toEqual({}) + + // Should insert an error about the failure + const errors = circuit.db.pcb_placement_error.list() + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].message).toContain("Network request failed") +}) + +test("parts engine handles invalid response format", async () => { + const { circuit } = getTestFixture() + + // Mock parts engine that returns an invalid format (array instead of object) + const mockPartsEngine = { + findPart: async () => { + return ["123-456", "789-012"] + }, + } as any + + circuit.add( + + + , + ) + + await circuit.renderUntilSettled() + + const sourceComponent = circuit.db.source_component.list()[0] + // Should return empty object instead of crashing + expect(sourceComponent.supplier_part_numbers).toEqual({}) + + // Should insert an error about the invalid format + const errors = circuit.db.pcb_placement_error.list() + expect(errors.length).toBeGreaterThan(0) + expect(errors[0].message).toContain("Invalid supplier part numbers format") +}) + +test("parts engine still works with valid responses after error handling", async () => { + const { circuit } = getTestFixture() + + // Mock parts engine that returns valid data + const mockPartsEngine = { + findPart: async () => ({ + digikey: ["123-456"], + mouser: ["789-012"], + }), + } + + circuit.add( + + + , + ) + + await circuit.renderUntilSettled() + + const sourceComponent = circuit.db.source_component.list()[0] + // Should still work correctly with valid responses + expect(sourceComponent.supplier_part_numbers).toEqual({ + digikey: ["123-456"], + mouser: ["789-012"], + }) + + // Should not have any errors + const errors = circuit.db.pcb_placement_error.list() + expect(errors.length).toBe(0) +}) + +test('parts engine handles "Not found" response correctly', async () => { + const { circuit } = getTestFixture() + + // Mock parts engine that returns "Not found" + const mockPartsEngine = { + findPart: async () => "Not found", + } as any + + circuit.add( + + + , + ) + + await circuit.renderUntilSettled() + + const sourceComponent = circuit.db.source_component.list()[0] + // "Not found" should be converted to empty object + expect(sourceComponent.supplier_part_numbers).toEqual({}) + + // Should not insert any errors for "Not found" - it's a valid response + const errors = circuit.db.pcb_placement_error.list() + expect(errors.length).toBe(0) +}) From d6648cccac0c03b80b8a779bad88f0795d760932 Mon Sep 17 00:00:00 2001 From: Ayush Jhawar Date: Fri, 24 Oct 2025 23:44:38 +0530 Subject: [PATCH 2/5] One test per file --- .../NormalComponent/NormalComponent.ts | 11 +- .../parts-engine-error-handling.test.tsx | 142 ------------------ .../parts-engine-error-handling1.test.tsx | 65 ++++++++ .../parts-engine-error-handling2.test.tsx | 46 ++++++ 4 files changed, 121 insertions(+), 143 deletions(-) delete mode 100644 tests/components/normal-components/parts-engine-error-handling.test.tsx create mode 100644 tests/components/normal-components/parts-engine-error-handling1.test.tsx create mode 100644 tests/components/normal-components/parts-engine-error-handling2.test.tsx diff --git a/lib/components/base-components/NormalComponent/NormalComponent.ts b/lib/components/base-components/NormalComponent/NormalComponent.ts index 11992e2dd..9f98b5851 100644 --- a/lib/components/base-components/NormalComponent/NormalComponent.ts +++ b/lib/components/base-components/NormalComponent/NormalComponent.ts @@ -1466,7 +1466,16 @@ export class NormalComponent< // Cache the result if (cacheEngine) { - await cacheEngine.setItem(cacheKey, JSON.stringify(supplierPartNumbers)) + try { + await cacheEngine.setItem( + cacheKey, + JSON.stringify(supplierPartNumbers), + ) + } catch (cacheError) { + debug( + `Failed to cache supplier part numbers for ${source_component.name}: ${cacheError}`, + ) + } } return supplierPartNumbers diff --git a/tests/components/normal-components/parts-engine-error-handling.test.tsx b/tests/components/normal-components/parts-engine-error-handling.test.tsx deleted file mode 100644 index 4b8f2179f..000000000 --- a/tests/components/normal-components/parts-engine-error-handling.test.tsx +++ /dev/null @@ -1,142 +0,0 @@ -import { test, expect } from "bun:test" -import { getTestFixture } from "tests/fixtures/get-test-fixture" - -test("parts engine handles JSON parsing errors", async () => { - const { circuit } = getTestFixture() - - // Mock parts engine that returns HTML instead of JSON (simulating Vercel error page) - const mockPartsEngine = { - findPart: async () => { - return "Internal Server Error" - }, - } as any - - circuit.add( - - - , - ) - - await circuit.renderUntilSettled() - - const sourceComponent = circuit.db.source_component.list()[0] - // Should return empty object instead of crashing - expect(sourceComponent.supplier_part_numbers).toEqual({}) - - // Should insert an error about the failure - const errors = circuit.db.pcb_placement_error.list() - expect(errors.length).toBeGreaterThan(0) - expect(errors[0].message).toContain("Failed to fetch supplier part numbers") -}) - -test("parts engine handles network errors", async () => { - const { circuit } = getTestFixture() - - // Mock parts engine that throws an error - const mockPartsEngine = { - findPart: async () => { - throw new Error("Network request failed") - }, - } - - circuit.add( - - - , - ) - - await circuit.renderUntilSettled() - - const sourceComponent = circuit.db.source_component.list()[0] - // Should return empty object instead of crashing - expect(sourceComponent.supplier_part_numbers).toEqual({}) - - // Should insert an error about the failure - const errors = circuit.db.pcb_placement_error.list() - expect(errors.length).toBeGreaterThan(0) - expect(errors[0].message).toContain("Network request failed") -}) - -test("parts engine handles invalid response format", async () => { - const { circuit } = getTestFixture() - - // Mock parts engine that returns an invalid format (array instead of object) - const mockPartsEngine = { - findPart: async () => { - return ["123-456", "789-012"] - }, - } as any - - circuit.add( - - - , - ) - - await circuit.renderUntilSettled() - - const sourceComponent = circuit.db.source_component.list()[0] - // Should return empty object instead of crashing - expect(sourceComponent.supplier_part_numbers).toEqual({}) - - // Should insert an error about the invalid format - const errors = circuit.db.pcb_placement_error.list() - expect(errors.length).toBeGreaterThan(0) - expect(errors[0].message).toContain("Invalid supplier part numbers format") -}) - -test("parts engine still works with valid responses after error handling", async () => { - const { circuit } = getTestFixture() - - // Mock parts engine that returns valid data - const mockPartsEngine = { - findPart: async () => ({ - digikey: ["123-456"], - mouser: ["789-012"], - }), - } - - circuit.add( - - - , - ) - - await circuit.renderUntilSettled() - - const sourceComponent = circuit.db.source_component.list()[0] - // Should still work correctly with valid responses - expect(sourceComponent.supplier_part_numbers).toEqual({ - digikey: ["123-456"], - mouser: ["789-012"], - }) - - // Should not have any errors - const errors = circuit.db.pcb_placement_error.list() - expect(errors.length).toBe(0) -}) - -test('parts engine handles "Not found" response correctly', async () => { - const { circuit } = getTestFixture() - - // Mock parts engine that returns "Not found" - const mockPartsEngine = { - findPart: async () => "Not found", - } as any - - circuit.add( - - - , - ) - - await circuit.renderUntilSettled() - - const sourceComponent = circuit.db.source_component.list()[0] - // "Not found" should be converted to empty object - expect(sourceComponent.supplier_part_numbers).toEqual({}) - - // Should not insert any errors for "Not found" - it's a valid response - const errors = circuit.db.pcb_placement_error.list() - expect(errors.length).toBe(0) -}) diff --git a/tests/components/normal-components/parts-engine-error-handling1.test.tsx b/tests/components/normal-components/parts-engine-error-handling1.test.tsx new file mode 100644 index 000000000..f3d313c0e --- /dev/null +++ b/tests/components/normal-components/parts-engine-error-handling1.test.tsx @@ -0,0 +1,65 @@ +import { test, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("parts engine handles errors gracefully and logs them to Circuit JSON", async () => { + // Test 1: HTML error page + const { circuit: circuit1 } = getTestFixture() + const htmlMock = { + findPart: async () => + "Internal Server Error", + } as any + + circuit1.add( + + + , + ) + await circuit1.renderUntilSettled() + + const sc1 = circuit1.db.source_component.list()[0] + expect(sc1.supplier_part_numbers).toEqual({}) + const errors1 = circuit1.db.pcb_placement_error.list() + expect(errors1.length).toBeGreaterThan(0) + expect(errors1[0].message).toContain("Failed to fetch supplier part numbers") + expect(errors1[0].message).toContain(" { + throw new Error("Network request failed") + }, + } + + circuit2.add( + + + , + ) + await circuit2.renderUntilSettled() + + const sc2 = circuit2.db.source_component.list()[0] + expect(sc2.supplier_part_numbers).toEqual({}) + const errors2 = circuit2.db.pcb_placement_error.list() + expect(errors2.length).toBeGreaterThan(0) + expect(errors2[0].message).toContain("Network request failed") + + // Test 3: Invalid format (array instead of object) + const { circuit: circuit3 } = getTestFixture() + const invalidMock = { + findPart: async () => ["123-456", "789-012"], + } as any + + circuit3.add( + + + , + ) + await circuit3.renderUntilSettled() + + const sc3 = circuit3.db.source_component.list()[0] + expect(sc3.supplier_part_numbers).toEqual({}) + const errors3 = circuit3.db.pcb_placement_error.list() + expect(errors3.length).toBeGreaterThan(0) + expect(errors3[0].message).toContain("Invalid supplier part numbers format") +}) diff --git a/tests/components/normal-components/parts-engine-error-handling2.test.tsx b/tests/components/normal-components/parts-engine-error-handling2.test.tsx new file mode 100644 index 000000000..a9ee84d33 --- /dev/null +++ b/tests/components/normal-components/parts-engine-error-handling2.test.tsx @@ -0,0 +1,46 @@ +import { test, expect } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("parts engine processes valid responses and special cases correctly", async () => { + // Test 1: Valid supplier part numbers + const { circuit: circuit1 } = getTestFixture() + const validMock = { + findPart: async () => ({ + digikey: ["123-456"], + mouser: ["789-012"], + }), + } + + circuit1.add( + + + , + ) + await circuit1.renderUntilSettled() + + const sc1 = circuit1.db.source_component.list()[0] + expect(sc1.supplier_part_numbers).toEqual({ + digikey: ["123-456"], + mouser: ["789-012"], + }) + const errors1 = circuit1.db.pcb_placement_error.list() + expect(errors1.length).toBe(0) + + // Test 2: "Not found" response (valid, converts to empty object) + const { circuit: circuit2 } = getTestFixture() + const notFoundMock = { + findPart: async () => "Not found", + } as any + + circuit2.add( + + + , + ) + await circuit2.renderUntilSettled() + + const sc2 = circuit2.db.source_component.list()[0] + expect(sc2.supplier_part_numbers).toEqual({}) + const errors2 = circuit2.db.pcb_placement_error.list() + expect(errors2.length).toBe(0) +}) From 5bf356fe05e9c31b2585423a4014736984a09af9 Mon Sep 17 00:00:00 2001 From: Ayush Jhawar Date: Sat, 25 Oct 2025 14:25:01 +0530 Subject: [PATCH 3/5] Fresh Starting --- .../NormalComponent/NormalComponent.ts | 92 +++++-------------- 1 file changed, 24 insertions(+), 68 deletions(-) diff --git a/lib/components/base-components/NormalComponent/NormalComponent.ts b/lib/components/base-components/NormalComponent/NormalComponent.ts index 9f98b5851..99534b930 100644 --- a/lib/components/base-components/NormalComponent/NormalComponent.ts +++ b/lib/components/base-components/NormalComponent/NormalComponent.ts @@ -1423,79 +1423,35 @@ export class NormalComponent< footprinterString: string | undefined, ) { if (this.props.doNotPlace) return {} - - try { - const cacheEngine = this.root?.platform?.localCacheEngine - const cacheKey = this._getPartsEngineCacheKey( - source_component, - footprinterString, - ) - - if (cacheEngine) { - const cached = await cacheEngine.getItem(cacheKey) - if (cached) { - return JSON.parse(cached) - } - } - - const result = await Promise.resolve( - partsEngine.findPart({ - sourceComponent: source_component, - footprinterString, - }), - ) - - // Validate response format - if (typeof result === "string" && result !== "Not found") { - throw new Error( - `Invalid response from parts engine: Expected JSON object but received string starting with "${result.substring(0, 50)}..."`, - ) - } - - const supplierPartNumbers = result === "Not found" ? {} : result - - if ( - typeof supplierPartNumbers !== "object" || - supplierPartNumbers === null || - Array.isArray(supplierPartNumbers) - ) { - throw new Error( - `Invalid supplier part numbers format: Expected object but received ${typeof supplierPartNumbers}`, - ) - } - - // Cache the result - if (cacheEngine) { + const cacheEngine = this.root?.platform?.localCacheEngine + const cacheKey = this._getPartsEngineCacheKey( + source_component, + footprinterString, + ) + if (cacheEngine) { + const cached = await cacheEngine.getItem(cacheKey) + if (cached) { try { - await cacheEngine.setItem( - cacheKey, - JSON.stringify(supplierPartNumbers), - ) - } catch (cacheError) { - debug( - `Failed to cache supplier part numbers for ${source_component.name}: ${cacheError}`, - ) - } + return JSON.parse(cached) + } catch {} } + } + const result = await Promise.resolve( + partsEngine.findPart({ + sourceComponent: source_component, + footprinterString, + }), + ) - return supplierPartNumbers - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error) - debug( - `Error fetching supplier part numbers for ${source_component.name}: ${errorMessage}`, - ) - - // Insert error into Circuit JSON for user visibility - if (this.root?.db) { - this.root.db.pcb_placement_error.insert({ - error_type: "pcb_placement_error", - message: `Failed to fetch supplier part numbers: ${errorMessage}`, - } as any) - } + // Convert "Not found" to empty object before caching or returning + const supplierPartNumbers = result === "Not found" ? {} : result - return {} + if (cacheEngine) { + try { + await cacheEngine.setItem(cacheKey, JSON.stringify(supplierPartNumbers)) + } catch {} } + return supplierPartNumbers } doInitialPartsEngineRender(): void { From 0db9d6faaaef929d8b4febae1d17a883fb3e788f Mon Sep 17 00:00:00 2001 From: Ayush Jhawar Date: Sat, 25 Oct 2025 22:59:13 +0530 Subject: [PATCH 4/5] update normalComponent logic --- .../NormalComponent/NormalComponent.ts | 47 +++++++++++++++++-- package.json | 2 +- .../parts-engine-error-handling1.test.tsx | 6 +-- .../parts-engine-error-handling2.test.tsx | 4 +- 4 files changed, 49 insertions(+), 10 deletions(-) diff --git a/lib/components/base-components/NormalComponent/NormalComponent.ts b/lib/components/base-components/NormalComponent/NormalComponent.ts index 99534b930..3fc26960f 100644 --- a/lib/components/base-components/NormalComponent/NormalComponent.ts +++ b/lib/components/base-components/NormalComponent/NormalComponent.ts @@ -18,6 +18,7 @@ import { point3, rotation, schematic_manual_edit_conflict_warning, + unknown_error_finding_part, } from "circuit-json" import { decomposeTSR } from "transformation-matrix" import Debug from "debug" @@ -1443,8 +1444,31 @@ export class NormalComponent< }), ) - // Convert "Not found" to empty object before caching or returning - const supplierPartNumbers = result === "Not found" ? {} : result + // Validate the result format + if (typeof result === "string") { + // Check if it's an HTML error page or "Not found" + if (result.includes(" { - this._asyncSupplierPartNumbers = await supplierPartNumbersMaybePromise - this._markDirty("PartsEngineRender") + await supplierPartNumbersMaybePromise + .then((supplierPartNumbers) => { + this._asyncSupplierPartNumbers = supplierPartNumbers + this._markDirty("PartsEngineRender") + }) + .catch((error: Error) => { + // Log structured error to Circuit JSON + this._asyncSupplierPartNumbers = {} + const errorObj = unknown_error_finding_part.parse({ + type: "unknown_error_finding_part", + message: `Failed to fetch supplier part numbers for ${this.getString()}: ${error.message}`, + source_component_id: this.source_component_id, + subcircuit_id: this.getSubcircuit()?.subcircuit_id, + }) + db.unknown_error_finding_part.insert(errorObj) + this._markDirty("PartsEngineRender") + }) }) } diff --git a/package.json b/package.json index ed16fde80..b7b57c287 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "bun-match-svg": "0.0.12", "calculate-elbow": "^0.0.12", "chokidar-cli": "^3.0.0", - "circuit-json": "^0.0.288", + "circuit-json": "^0.0.291", "circuit-json-to-bpc": "^0.0.13", "circuit-json-to-connectivity-map": "^0.0.22", "circuit-json-to-gltf": "^0.0.31", diff --git a/tests/components/normal-components/parts-engine-error-handling1.test.tsx b/tests/components/normal-components/parts-engine-error-handling1.test.tsx index f3d313c0e..695213117 100644 --- a/tests/components/normal-components/parts-engine-error-handling1.test.tsx +++ b/tests/components/normal-components/parts-engine-error-handling1.test.tsx @@ -18,7 +18,7 @@ test("parts engine handles errors gracefully and logs them to Circuit JSON", asy const sc1 = circuit1.db.source_component.list()[0] expect(sc1.supplier_part_numbers).toEqual({}) - const errors1 = circuit1.db.pcb_placement_error.list() + const errors1 = circuit1.db.unknown_error_finding_part.list() expect(errors1.length).toBeGreaterThan(0) expect(errors1[0].message).toContain("Failed to fetch supplier part numbers") expect(errors1[0].message).toContain(" Date: Sat, 25 Oct 2025 23:07:24 +0530 Subject: [PATCH 5/5] Improve null msg --- .../base-components/NormalComponent/NormalComponent.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/components/base-components/NormalComponent/NormalComponent.ts b/lib/components/base-components/NormalComponent/NormalComponent.ts index 3fc26960f..8d3a1753b 100644 --- a/lib/components/base-components/NormalComponent/NormalComponent.ts +++ b/lib/components/base-components/NormalComponent/NormalComponent.ts @@ -1462,9 +1462,15 @@ export class NormalComponent< } // Validate that result is an object (not array, null, etc.) - if (!result || typeof result !== "object" || Array.isArray(result)) { + if (!result || Array.isArray(result) || typeof result !== "object") { + const actualType = + result === null + ? "null" + : Array.isArray(result) + ? "array" + : typeof result throw new Error( - `Invalid supplier part numbers format: Expected object but got ${Array.isArray(result) ? "array" : typeof result}`, + `Invalid supplier part numbers format: Expected object but got ${actualType}`, ) }