From 986710aa27aee2023d2946fbdf6079a8d75867b3 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Mon, 1 Jun 2026 18:14:31 +0000 Subject: [PATCH] fix(test): retype petProfileSummary chain mock to satisfy tsc --project build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI failed on PR #137 because `tsc --project .` (the build path used by the Docker image) is stricter than `pnpm typecheck` was reporting during local iteration — two TS2322 errors surfaced in the new mock: 1. `chain.from = (table: { _name: string }) => ...` was assigned through a `Record unknown>` index signature, and `{ _name: string }` is not assignable from `unknown`. 2. `chain.then = (onFulfilled?: (v: unknown[]) => unknown) => ...` was not assignable to the `PromiseLike.then` signature TS now infers for the awaitable, because TS expects `onfulfilled` to also accept `null`. Replace the proxy-based loose chain with a typed `ChainLike` interface so the build typechecker is satisfied. Behaviour is unchanged — all 7 GRO-2014 regression tests still pass. Co-Authored-By: Paperclip --- src/__tests__/petProfileSummary.test.ts | 94 ++++++++++++++----------- 1 file changed, 53 insertions(+), 41 deletions(-) diff --git a/src/__tests__/petProfileSummary.test.ts b/src/__tests__/petProfileSummary.test.ts index 872b215..8a17d43 100644 --- a/src/__tests__/petProfileSummary.test.ts +++ b/src/__tests__/petProfileSummary.test.ts @@ -81,12 +81,14 @@ function resetDb() { } // ─── @groombook/db mock ────────────────────────────────────────────────────── +// +// Each select chain needs to know which table it's targeting and which columns +// it's projecting so we can return the right mocked rows. We thread that state +// through a per-call object whose chain methods all return `this`. The chain +// is also `then`-able so any `await` position resolves to the rows. vi.mock("@groombook/db", () => { - // Each "select chain" needs to know which table it's targeting so we can - // hand back the right mocked rows. We can't tell tables apart by reference - // in Drizzle-land, so use named proxies and inspect them in `from()`. - const named = (name: string) => + const namedTable = (name: string) => new Proxy( { _name: name }, { @@ -97,27 +99,28 @@ vi.mock("@groombook/db", () => { } ); - const pets = named("pets"); - const appointments = named("appointments"); - const services = named("services"); - const staff = named("staff"); + const pets = namedTable("pets"); + const appointments = namedTable("appointments"); + const services = namedTable("services"); + const staff = namedTable("staff"); - function buildSelect(projection?: Record) { + // The full chain interface is intentionally loose — only `then` is exposed + // with a typed signature so vitest's await resolves to the right shape. + interface ChainLike { + from: (table: { _name: string }) => ChainLike; + where: (...args: unknown[]) => ChainLike; + innerJoin: (...args: unknown[]) => ChainLike; + leftJoin: (...args: unknown[]) => ChainLike; + orderBy: (...args: unknown[]) => ChainLike; + limit: (...args: unknown[]) => ChainLike; + then: ( + onfulfilled?: ((value: unknown[]) => T | PromiseLike) | null + ) => Promise; + } + + function buildSelect(projection?: Record): ChainLike { let targetTable = ""; - const chain: Record unknown> = {}; - chain.from = (table: { _name: string }) => { - targetTable = table._name; - return chain; - }; - chain.innerJoin = () => chain; - chain.leftJoin = () => chain; - chain.orderBy = () => chain; - chain.limit = () => chain; - - // .where(...) on the pets-select is the terminal call in the route — it - // is awaited directly. Other queries chain through .limit/.orderBy. We - // make every chain "thenable" so any await position resolves to rows. const resolveRows = (): unknown[] => { if (targetTable === "pets") { if (dbState.throwOnPetSelect) { @@ -126,11 +129,6 @@ vi.mock("@groombook/db", () => { return dbState.petRow ? [dbState.petRow] : []; } if (targetTable === "appointments") { - // Disambiguate by projection shape: - // - linkage check projects `{ id: appointments.id }` - // - recentHistory projects multiple columns including serviceName - // - visit count projects `{ count: ... }` - // - upcoming projects multiple columns including confirmationStatus const keys = projection ? Object.keys(projection) : []; if (projection && keys.length === 1 && keys[0] === "id") { return dbState.linkageRow ? [dbState.linkageRow] : []; @@ -145,16 +143,31 @@ vi.mock("@groombook/db", () => { } return []; }; - chain.where = (..._args: unknown[]) => { - // After .where, the chain is still awaitable. Return chain itself so - // .limit/.orderBy can follow, but also expose `then` for the case - // where .where is the last call (pets-select). - return chain; + + const chain: ChainLike = { + from(table) { + targetTable = table._name; + return chain; + }, + where() { + return chain; + }, + innerJoin() { + return chain; + }, + leftJoin() { + return chain; + }, + orderBy() { + return chain; + }, + limit() { + return chain; + }, + then(onfulfilled) { + return Promise.resolve(resolveRows()).then(onfulfilled ?? undefined); + }, }; - // Make the whole chain thenable so any await position works. - (chain as unknown as PromiseLike).then = ( - onFulfilled?: (v: unknown[]) => unknown - ) => Promise.resolve(resolveRows()).then(onFulfilled); return chain; } @@ -167,14 +180,13 @@ vi.mock("@groombook/db", () => { appointments, services, staff, - and: vi.fn((..._args: unknown[]) => ({ _op: "and" })), - or: vi.fn((..._args: unknown[]) => ({ _op: "or" })), - eq: vi.fn((..._args: unknown[]) => ({ _op: "eq" })), + and: vi.fn(() => ({ _op: "and" })), + or: vi.fn(() => ({ _op: "or" })), + eq: vi.fn(() => ({ _op: "eq" })), desc: vi.fn((arg: unknown) => arg), exists: vi.fn((arg: unknown) => arg), sql: Object.assign( - (..._args: unknown[]) => ({ _op: "sql" }), - // tag template fallback + () => ({ _op: "sql" }), { [Symbol.toPrimitive]: () => "sql" } ), };