fix(test): retype petProfileSummary chain mock to satisfy tsc --project build
CI / Test (pull_request) Successful in 11s
CI / Lint & Typecheck (pull_request) Successful in 17s
CI / Build & Push Docker Images (pull_request) Successful in 1m8s

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<string, (...args: unknown[]) => unknown>` index signature,
     and `{ _name: string }` is not assignable from `unknown`.
  2. `chain.then = (onFulfilled?: (v: unknown[]) => unknown) => ...` was
     not assignable to the `PromiseLike<T>.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 <noreply@paperclip.ing>
This commit is contained in:
Flea Flicker
2026-06-01 18:14:31 +00:00
parent 762d263016
commit 986710aa27
+53 -41
View File
@@ -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<string, unknown>) {
// 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: <T = unknown[]>(
onfulfilled?: ((value: unknown[]) => T | PromiseLike<T>) | null
) => Promise<T>;
}
function buildSelect(projection?: Record<string, unknown>): ChainLike {
let targetTable = "";
const chain: Record<string, (...args: unknown[]) => 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<unknown[]>).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" }
),
};