From e52c7f9276b4dd27ed60602f3d76ab27da37af56 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 14:20:40 +0000 Subject: [PATCH] fix(ui): accept the header_mapping log step, so imports no longer show as Failed #186 made the API return a header_mapping step in an upload's logs. The React app's response schema did not list it, and the React app sends a header mapping with every dynamic upload. zod rejected the whole response ("Unexpected response shape ... logs.3.step"), so an import that had succeeded on the server was shown as Failed. Add the step, export the schema, and test that it accepts every step the API's loaders can pass to _log(), so the next new step fails a test, not the upload screen. Found by the browser end-to-end tests. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_011T6Wqi8yaiaYWUVH8C4QAY --- .../src/__tests__/log-step-contract.test.ts | 41 +++++++++++++++++++ csv-table-hub-main/src/lib/csv.functions.ts | 3 +- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 csv-table-hub-main/src/__tests__/log-step-contract.test.ts diff --git a/csv-table-hub-main/src/__tests__/log-step-contract.test.ts b/csv-table-hub-main/src/__tests__/log-step-contract.test.ts new file mode 100644 index 0000000..669532b --- /dev/null +++ b/csv-table-hub-main/src/__tests__/log-step-contract.test.ts @@ -0,0 +1,41 @@ +// The upload response is validated against a schema. A log step the API emits but the schema does not +// list makes zod reject the WHOLE response, so an import that succeeded on the server is reported as +// "Failed: Unexpected response shape" (what #186 did when it added the header_mapping step). + +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { processingLogSchema } from "@/lib/csv.functions"; + +const API = join(__dirname, "../../../api"); + +/** Every step name the loaders pass to _log(logs, "", ...). */ +function stepsTheApiEmits(): Set { + const steps = new Set(); + for (const dir of ["services", "routers"]) { + for (const file of readdirSync(join(API, dir)).filter((f) => f.endsWith(".py"))) { + const source = readFileSync(join(API, dir, file), "utf8"); + for (const m of source.matchAll(/_log\(\s*logs,\s*"([a-z_]+)"/g)) steps.add(m[1]); + } + } + return steps; +} + +describe("processing log steps", () => { + it("finds the steps the API emits (guards the scan itself)", () => { + const steps = stepsTheApiEmits(); + expect(steps.size).toBeGreaterThan(8); + expect(steps.has("receive")).toBe(true); + }); + + it("the schema accepts every step the API can emit", () => { + const known = new Set(processingLogSchema.shape.step.options); + const unknown = [...stepsTheApiEmits()].filter((s) => !known.has(s)); + expect(unknown).toEqual([]); + }); + + it("accepts the header_mapping entry a dynamic upload with a header mapping returns", () => { + const entry = { ts: 1, step: "header_mapping", level: "info", message: "Applied 2 header renames" }; + expect(processingLogSchema.safeParse(entry).success).toBe(true); + }); +}); diff --git a/csv-table-hub-main/src/lib/csv.functions.ts b/csv-table-hub-main/src/lib/csv.functions.ts index bd07654..6b65bec 100644 --- a/csv-table-hub-main/src/lib/csv.functions.ts +++ b/csv-table-hub-main/src/lib/csv.functions.ts @@ -13,13 +13,14 @@ const rowErrorSchema = z.object({ }); export type RowError = z.infer; -const processingLogSchema = z.object({ +export const processingLogSchema = z.object({ ts: z.number(), step: z.enum([ "receive", "hash", "duplicate_check", "parse", + "header_mapping", "validate_columns", "create_table", "cast_rows",