From 4df7d960206120585a193a732bb8d58b77ff6aaf Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Fri, 29 May 2026 15:24:33 +0000 Subject: [PATCH 1/6] fix(seed): use typeof on enum.enumValues for db build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TS2749: enumValues is a value, not a type — wrap with typeof before indexing. Also extends Lint & Typecheck CI job to run pnpm --filter @groombook/db typecheck so this class of error is caught at lint time rather than Docker build time. Co-Authored-By: Claude Opus 4.7 --- .gitea/workflows/ci.yml | 4 +++- packages/db/src/seed.ts | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index dfb785b..0f05f7c 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -32,7 +32,9 @@ jobs: run: pnpm install --frozen-lockfile - name: Typecheck - run: pnpm --filter @groombook/api typecheck + run: | + pnpm --filter @groombook/api typecheck + pnpm --filter @groombook/db typecheck - name: Lint run: pnpm --filter @groombook/api lint diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index d2d274b..5e33c05 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -287,8 +287,8 @@ const preferredCutPool: string[] = [ "Full Groom", ]; -type CoatType = schema.coatTypeEnum.enumValues[number]; -type PetSizeCategory = schema.petSizeCategoryEnum.enumValues[number]; +type CoatType = (typeof schema.coatTypeEnum.enumValues)[number]; +type PetSizeCategory = (typeof schema.petSizeCategoryEnum.enumValues)[number]; const coatTypePool: CoatType[] = ["short", "medium", "long", "double", "wire", "silky", "curly", "hairless"]; const petSizeCategoryPool: PetSizeCategory[] = ["small", "medium", "large", "extra_large"]; -- 2.52.0 From bf064b3adade67160a61602c81aab0fc3dd43282 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Fri, 29 May 2026 16:34:33 +0000 Subject: [PATCH 2/6] fix(test): mock db to handle sql count(*) queries and async iteration The petProfileSummary mock's sql tag returned a plain string instead of a proper Drizzle SQL object, so count(*) queries via .as("count") failed. Also added Symbol.asyncIterator support for for-await-of patterns used in the pets router. Fixes: GRO-1917 Co-Authored-By: Paperclip --- .../src/__tests__/petProfileSummary.test.ts | 69 +++++++++++++++---- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/apps/api/src/__tests__/petProfileSummary.test.ts b/apps/api/src/__tests__/petProfileSummary.test.ts index 38b138c..f7e5686 100644 --- a/apps/api/src/__tests__/petProfileSummary.test.ts +++ b/apps/api/src/__tests__/petProfileSummary.test.ts @@ -178,6 +178,9 @@ vi.mock("../db/index.js", () => { const staff = new Proxy({ _name: "staff" }, { get: (t, p) => p === "_name" ? "staff" : {} }); const services = new Proxy({ _name: "services" }, { get: (t, p) => p === "_name" ? "services" : {} }); + // Tracks { [tableName]: { [alias]: SQLExpression } } for the current select() call + let selectedColumns: Record> = {}; + function makeChainable(rows: unknown[]) { const arr = rows as unknown[]; return new Proxy(arr, { @@ -188,25 +191,67 @@ vi.mock("../db/index.js", () => { if (prop === Symbol.iterator) { return function* () { for (const v of target) yield v; }; } + if (prop === Symbol.asyncIterator) { + return async function* () { for (const v of target) yield v; }; + } // @ts-expect-error proxy return target[prop]; }, }); } + // sql mock: returns an object with .as() so drizzle's select() can alias it + function sqlMock(_strings: TemplateStringsArray, ..._params: unknown[]) { + const queryString = _strings[0]; + const asFn = (alias: string) => ({ + sql: { queryChunks: [_strings[0]] }, + fieldAlias: alias, + getSQL() { return this.sql; }, + }); + return { queryChunks: [queryString], as: asFn }; + } + return { getDb: () => ({ - select: () => ({ - from: (table: unknown) => { - const name = (table as { _name?: string })._name; - if (name === "pets") return makeChainable(mock.pets); - if (name === "appointments") return makeChainable(mock.appointments); - if (name === "groomingVisitLogs") return makeChainable(mock.groomingLogs); - if (name === "staff") return makeChainable(mock.staffMembers); - if (name === "services") return makeChainable(mock.services); - return makeChainable([]); - }, - }), + select: (cols?: Record) => { + selectedColumns = {}; + if (cols) { + // Inspect cols to find sql-aliased expressions and their aliases + for (const [alias, expr] of Object.entries(cols)) { + if (expr && typeof expr === "object" && "as" in expr && typeof (expr as Record).as === "function") { + const aliased = (expr as { as: (a: string) => { fieldAlias: string; sql: unknown } }).as(alias); + // Detect count(*) queries + if (typeof aliased.sql === "object" && aliased.sql !== null && "queryChunks" in (aliased.sql as Record) && String((aliased.sql as { queryChunks?: unknown[] }).queryChunks).includes("count")) { + // Store count query intent — we'll resolve it in from() + if (!selectedColumns["appointments"]) selectedColumns["appointments"] = {}; + selectedColumns["appointments"][alias] = { _isCountQuery: true }; + } + } + } + } + return { + from: (table: unknown) => { + const name = (table as { _name?: string })._name; + const tableCols = selectedColumns[name] || {}; + // If this table has a count query, return computed count result + const countQueryEntry = Object.entries(tableCols).find(([, v]) => + typeof v === "object" && v !== null && "_isCountQuery" in v + ); + if (countQueryEntry) { + const [countAlias] = countQueryEntry; + const count = (name === "appointments" ? mock.appointments : []) + .filter((row: Record) => row.status === "completed").length; + return makeChainable([{ [countAlias]: count }]); + } + if (name === "pets") return makeChainable(mock.pets); + if (name === "appointments") return makeChainable(mock.appointments); + if (name === "groomingVisitLogs") return makeChainable(mock.groomingLogs); + if (name === "staff") return makeChainable(mock.staffMembers); + if (name === "services") return makeChainable(mock.services); + return makeChainable([]); + }, + }; + }, insert: () => ({ values: () => ({ returning: () => [{}] }) }), update: () => ({ set: () => ({ where: () => ({ returning: () => [{}] }) }) }), delete: () => ({ where: () => ({ returning: () => [{}] }) }), @@ -222,7 +267,7 @@ vi.mock("../db/index.js", () => { exists: vi.fn(() => true), gte: vi.fn((a: unknown, b: unknown) => ({ col: a, val: b })), or: vi.fn((a: unknown, b: unknown) => [a, b]), - sql: vi.fn((str: string) => str), + sql: sqlMock, }; }); -- 2.52.0 From 4a0dd5ed2a15dffc7757a3b571725422831d3488 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Sat, 30 May 2026 02:52:33 +0000 Subject: [PATCH 3/6] fix(seed): add uat-customer client record for SSO bridge UAT (GRO-1935) - Add UAT Customer client row (id: c0000001-0000-0000-0000-000000000001) with email uat-customer@groombook.dev in seedKnownUsers() - Add two UAT Customer pets (UAT Pup Alpha, UAT Pup Beta) with stable IDs - Add test case covering 201 response with correct clientId/clientName for uat-customer SSO bridge flow - Explicit comment clarifying uat-groomer/uat-super are staff, not clients Co-Authored-By: Paperclip --- packages/db/src/seed.ts | 57 +++++++++++++++++++++ src/__tests__/portalSessionFromAuth.test.ts | 31 +++++++++++ 2 files changed, 88 insertions(+) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 5e33c05..3e90a9d 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -624,6 +624,63 @@ async function seedKnownUsers() { } } + // ── Client: UAT Customer ───────────────────────────────────────────────────── + // Only uat-customer is a real end-user who needs a clients row. + // uat-groomer and uat-super are staff — they have staff records, not client records. + const UAT_CUSTOMER_ID = "c0000001-0000-0000-0000-000000000001"; + const [uatCustomerRow] = await db + .select() + .from(schema.clients) + .where(eq(schema.clients.email, "uat-customer@groombook.dev")) + .limit(1); + + let uatCustomerClientId: string; + if (uatCustomerRow) { + uatCustomerClientId = uatCustomerRow.id; + console.log(`✓ UAT Customer client record already exists — skipping`); + } else { + const [created] = await db + .insert(schema.clients) + .values({ + id: UAT_CUSTOMER_ID, + email: "uat-customer@groombook.dev", + name: "UAT Customer", + phone: "555-0102", + address: "1 UAT Lane, Test City, CA 90210", + }) + .returning(); + uatCustomerClientId = created!.id; + console.log(`✓ Created client 'UAT Customer' for SSO bridge`); + } + + // ── Pets: UAT Customer's dogs ──────────────────────────────────────────────── + const uatCustomerPets = [ + { id: "c0000001-0000-0000-0000-000000000002", name: "UAT Pup Alpha", species: "Dog", breed: "Beagle", weight: "12.00", dob: "2022-03-10", image: "/demo-pets/dog-beagle.png" }, + { id: "c0000001-0000-0000-0000-000000000003", name: "UAT Pup Beta", species: "Dog", breed: "Labrador", weight: "28.00", dob: "2021-07-22", image: "/demo-pets/dog-labrador.png" }, + ]; + for (const pet of uatCustomerPets) { + const [existing] = await db + .select() + .from(schema.pets) + .where(eq(schema.pets.id, pet.id)) + .limit(1); + if (existing) { + console.log(`✓ UAT Pet '${existing.name}' already exists — skipping`); + } else { + await db.insert(schema.pets).values({ + id: pet.id, + clientId: uatCustomerClientId, + name: pet.name, + species: pet.species, + breed: pet.breed, + weightKg: pet.weight, + dateOfBirth: new Date(`${pet.dob}T00:00:00Z`), + image: pet.image, + }); + console.log(`✓ Created UAT pet '${pet.name}'`); + } + } + // ── Services: idempotent upsert using name as unique key ───────────────────── // UNIQUE constraint on services.name (migration 0020) must exist first. // Uses b0000001-... IDs to match main seed servicesDef for same-named services. diff --git a/src/__tests__/portalSessionFromAuth.test.ts b/src/__tests__/portalSessionFromAuth.test.ts index 8448803..960e61e 100644 --- a/src/__tests__/portalSessionFromAuth.test.ts +++ b/src/__tests__/portalSessionFromAuth.test.ts @@ -6,6 +6,10 @@ const CLIENT_ID = "550e8400-e29b-41d4-a716-446655440001"; const CLIENT_EMAIL = "alice@example.com"; const CLIENT_NAME = "Alice Smith"; +const UAT_CUSTOMER_ID = "c0000001-0000-0000-0000-000000000001"; +const UAT_CUSTOMER_EMAIL = "uat-customer@groombook.dev"; +const UAT_CUSTOMER_NAME = "UAT Customer"; + const BETTER_AUTH_SESSION = { user: { id: "auth-user-001", @@ -163,6 +167,33 @@ describe("POST /portal/session-from-auth", () => { expect((insertedSession as Record).reason).toBe("sso-bridge"); }); + it("returns 201 for uat-customer SSO bridge with correct clientId and clientName", async () => { + const uatAuthSession = { + user: { + id: "auth-user-uat-customer", + email: UAT_CUSTOMER_EMAIL, + name: UAT_CUSTOMER_NAME, + }, + session: { + id: "ba-session-uat-customer", + expiresAt: new Date(Date.now() + 60 * 60 * 1000), + }, + }; + mockGetSession.mockResolvedValue(uatAuthSession); + mockClientRow = { id: UAT_CUSTOMER_ID, email: UAT_CUSTOMER_EMAIL, name: UAT_CUSTOMER_NAME }; + mockStaffRow = { id: "00000000-0000-0000-0000-000000000001" }; + const res = await app.request("/portal/session-from-auth", { + method: "POST", + }); + expect(res.status).toBe(201); + const body = await res.json(); + expect(body).toHaveProperty("sessionId"); + expect(body.clientId).toBe(UAT_CUSTOMER_ID); + expect(body.clientName).toBe(UAT_CUSTOMER_NAME); + expect(insertedSession).not.toBeNull(); + expect((insertedSession as Record).reason).toBe("sso-bridge"); + }); + it("returns 503 when auth is not configured", async () => { mockGetAuth.mockImplementation(() => { throw new Error("Auth not initialized"); -- 2.52.0 From 473868579b05738ddf1246bc814d09d1138e32ea Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Sat, 30 May 2026 03:07:21 +0000 Subject: [PATCH 4/6] =?UTF-8?q?GRO-1921:=20seedUatStaffAccounts()=20shared?= =?UTF-8?q?=20fn=20=E2=80=94=20full=20UAT=20seed=20honors=20numeric=20OIDC?= =?UTF-8?q?=20subs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract UAT staff account seeding into a shared async function so it runs in both seedKnownUsers() and the full seed() UAT branch. Before this change the full seed() UAT path never created the deterministic UAT staff (UAT Super/Staff/Groomer) with their numeric oidcSub values from SEED_UAT_*_OIDC_SUB env vars — seedKnownUsers() had that logic but was bypassed by SEED_KNOWN_USERS_ONLY=true in the UAT reset CronJob. seedUatStaffAccounts() handles: - UAT Super Staff (SEED_UAT_SUPER_OIDC_SUB) - UAT Staff Groomer (SEED_UAT_STAFF_OIDC_SUB) - UAT Groomer Personas (SEED_UAT_GROOMER_EMAILS + _NAMES) - Better Auth email+password credentials (SEED_UAT_*_PASSWORD) - UAT Customer client + 2 pets Co-Authored-By: Paperclip --- packages/db/src/seed.ts | 185 ++++++++++++++++++++-------------------- 1 file changed, 92 insertions(+), 93 deletions(-) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 3e90a9d..375ffe1 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -385,78 +385,19 @@ const servicesDef = [ { id: "b0000001-0000-0000-0000-00000000000a", name: "Sanitary Trim", desc: "Hygienic trim of paw pads, face, and sanitary areas", price: 2500, dur: 20 }, ]; -// ── Known-users-only seed (prod/demo) ─────────────────────────────────────── +// ── UAT staff account seeding (shared between seed paths) ───────────────────── /** - * Seeds only the minimal known users for prod/demo environments. - * Creates: Demo Manager staff + Demo Client + Demo Dog + basic services. - * Idempotent: skips creation if records already exist. + * Seeds or upserts the deterministic UAT staff accounts with numeric OIDC subs + * from SEED_UAT_*_OIDC_SUB / SEED_UAT_GROOMER_OIDC_SUBS env vars. + * + * In the full seed path this must run AFTER random staff are created so the + * deterministic upserts land on the correct rows (groomers referenced by the + * UAT test-client appointment logic use groomers[0] etc.). + * + * In seedKnownUsers() this replaces the inline UAT-staff block. */ -async function seedKnownUsers() { - const url = process.env.DATABASE_URL; - if (!url) { - console.error("DATABASE_URL is not set"); - process.exit(1); - } - - const client = postgres(url, { max: 5 }); - const db = drizzle(client, { schema }); - - console.log("Seeding known users (prod/demo mode)...\n"); - - const KNOWN_STAFF_ID = "00000000-0000-0000-0000-000000000001"; - const DEMO_CLIENT_ID = "00000000-0000-0000-0000-000000000002"; - const DEMO_PET_ID = "00000000-0000-0000-0000-000000000003"; - - // ── Staff: Demo Manager ── - const [existingStaff] = await db - .select() - .from(schema.staff) - .where(eq(schema.staff.email, "demo-manager@groombook.dev")) - .limit(1); - - if (existingStaff) { - console.log(`✓ Staff '${existingStaff.name}' already exists — skipping`); - } else { - await db.insert(schema.staff).values({ - id: KNOWN_STAFF_ID, - name: "Demo Manager", - email: "demo-manager@groombook.dev", - oidcSub: "demo-manager-001", - role: "manager", - isSuperUser: true, - active: true, - }); - console.log("✓ Created staff 'Demo Manager' (oidcSub: demo-manager-001)"); - } - - // ── Staff: SEED_ADMIN_EMAIL admin ── - const adminEmail = process.env.SEED_ADMIN_EMAIL; - if (adminEmail) { - const adminName = process.env.SEED_ADMIN_NAME ?? "Admin"; - const ADMIN_STAFF_ID = "00000000-0000-0000-0000-000000000002"; - const [existingAdmin] = await db - .select() - .from(schema.staff) - .where(eq(schema.staff.email, adminEmail)) - .limit(1); - - if (existingAdmin) { - console.log(`✓ Staff admin '${existingAdmin.name}' already exists — skipping`); - } else { - await db.insert(schema.staff).values({ - id: ADMIN_STAFF_ID, - name: adminName, - email: adminEmail, - oidcSub: adminEmail, - role: "manager", - isSuperUser: true, - active: true, - }); - console.log(`✓ Created staff admin '${adminName}' (${adminEmail})`); - } - } - +async function seedUatStaffAccounts(db: ReturnType) { // ── Staff: UAT Super User (oidcSub from SEED_UAT_SUPER_OIDC_SUB env var) ── const uatSuperOidcSub = process.env.SEED_UAT_SUPER_OIDC_SUB; if (uatSuperOidcSub) { @@ -680,6 +621,84 @@ async function seedKnownUsers() { console.log(`✓ Created UAT pet '${pet.name}'`); } } +} + +// ── Known-users-only seed (prod/demo) ─────────────────────────────────────── + +/** + * Seeds only the minimal known users for prod/demo environments. + * Creates: Demo Manager staff + Demo Client + Demo Dog + basic services. + * Idempotent: skips creation if records already exist. + */ +async function seedKnownUsers() { + const url = process.env.DATABASE_URL; + if (!url) { + console.error("DATABASE_URL is not set"); + process.exit(1); + } + + const client = postgres(url, { max: 5 }); + const db = drizzle(client, { schema }); + + console.log("Seeding known users (prod/demo mode)...\n"); + + const KNOWN_STAFF_ID = "00000000-0000-0000-0000-000000000001"; + const DEMO_CLIENT_ID = "00000000-0000-0000-0000-000000000002"; + const DEMO_PET_ID = "00000000-0000-0000-0000-000000000003"; + + // ── Staff: Demo Manager ── + const [existingStaff] = await db + .select() + .from(schema.staff) + .where(eq(schema.staff.email, "demo-manager@groombook.dev")) + .limit(1); + + if (existingStaff) { + console.log(`✓ Staff '${existingStaff.name}' already exists — skipping`); + } else { + await db.insert(schema.staff).values({ + id: KNOWN_STAFF_ID, + name: "Demo Manager", + email: "demo-manager@groombook.dev", + oidcSub: "demo-manager-001", + role: "manager", + isSuperUser: true, + active: true, + }); + console.log("✓ Created staff 'Demo Manager' (oidcSub: demo-manager-001)"); + } + + // ── Staff: SEED_ADMIN_EMAIL admin ── + const adminEmail = process.env.SEED_ADMIN_EMAIL; + if (adminEmail) { + const adminName = process.env.SEED_ADMIN_NAME ?? "Admin"; + const ADMIN_STAFF_ID = "00000000-0000-0000-0000-000000000002"; + const [existingAdmin] = await db + .select() + .from(schema.staff) + .where(eq(schema.staff.email, adminEmail)) + .limit(1); + + if (existingAdmin) { + console.log(`✓ Staff admin '${existingAdmin.name}' already exists — skipping`); + } else { + await db.insert(schema.staff).values({ + id: ADMIN_STAFF_ID, + name: adminName, + email: adminEmail, + oidcSub: adminEmail, + role: "manager", + isSuperUser: true, + active: true, + }); + console.log(`✓ Created staff admin '${adminName}' (${adminEmail})`); + } + } + + // ── UAT staff accounts + Better Auth credentials (shared impl) ────────────── + // Extracted into seedUatStaffAccounts() so it runs in both seedKnownUsers() + // and the full seed() UAT branch. + await seedUatStaffAccounts(db); // ── Services: idempotent upsert using name as unique key ───────────────────── // UNIQUE constraint on services.name (migration 0020) must exist first. @@ -847,30 +866,10 @@ async function seed() { console.log(`✓ Upserted admin staff '${adminName}' (${adminEmail})`); } - // ── UAT Groomer Personas (SEED_UAT_GROOMER_EMAILS + SEED_UAT_GROOMER_NAMES) ── - const groomerEmails = process.env.SEED_UAT_GROOMER_EMAILS?.split(",").map((e) => e.trim()).filter(Boolean) ?? []; - const groomerNames = process.env.SEED_UAT_GROOMER_NAMES?.split(",").map((n) => n.trim()).filter(Boolean) ?? []; - const groomerCount = Math.min(groomerEmails.length, groomerNames.length); - for (let i = 0; i < groomerCount; i++) { - const email = groomerEmails[i]!; - const name = groomerNames[i]!; - const staffId = `00000000-0000-0000-0000-${String(5 + i).padStart(12, "0")}`; - await db.insert(schema.staff) - .values({ - id: staffId, - name, - email, - oidcSub: email, - role: "groomer", - isSuperUser: false, - active: true, - }) - .onConflictDoUpdate({ - target: schema.staff.email, - set: { id: staffId, name, role: "groomer", isSuperUser: false, active: true }, - }); - console.log(`✓ Upserted groomer '${name}' (${email})`); - } + // ── UAT staff accounts + Better Auth credentials (shared impl) ────────────── + // Seeds deterministic UAT staff with numeric OIDC subs and Better Auth credentials. + // Must run AFTER random staff are created so upserts land correctly. + await seedUatStaffAccounts(db); // ── Services ── // Upsert services using name as unique key. With deterministic IDs in -- 2.52.0 From cc63299415b96bbc89d64838557ba86c1099904d Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Sat, 30 May 2026 04:10:55 +0000 Subject: [PATCH 5/6] GRO-1949: add behavioral and skin medicalAlertPool types, deterministic seeding for TestCooper/TestRocky - Extend medicalAlertPool with 4 new entries: 2 behavioral, 2 skin - Make TestCooper (uat-charlie) get a deterministic behavioral alert - Make TestRocky (uat-delta) get a deterministic skin alert - Other UAT pets retain random alert assignment Co-Authored-By: Paperclip --- packages/db/src/seed.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 375ffe1..7460870 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -270,6 +270,10 @@ const medicalAlertPool: MedicalAlert[] = [ { id: "", type: "other", description: "Seizure history — avoid flashing lights", severity: "high" }, { id: "", type: "other", description: "Luxating patella — short walks only", severity: "medium" }, { id: "", type: "other", description: "Ear infections — dry thoroughly after bath", severity: "low" }, + { id: "", type: "behavioral", description: "Anxiety — calm environment preferred", severity: "medium" }, + { id: "", type: "behavioral", description: "Fear-based aggression — approach with caution", severity: "high" }, + { id: "", type: "skin", description: "Contact dermatitis — avoid harsh chemicals", severity: "medium" }, + { id: "", type: "skin", description: "Hot spots — monitor and report any worsening", severity: "high" }, ]; const preferredCutPool: string[] = [ @@ -962,6 +966,14 @@ async function seed() { temperamentScore: randInt(1, 5), temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)), medicalAlerts: (() => { + // Deterministic alerts for UAT AC pets + if (uc.petName === "TestCooper") { + return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() })); + } + if (uc.petName === "TestRocky") { + return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() })); + } + // Other UAT pets: random if (rand() < 0.3) { const count = rand() < 0.7 ? 1 : 2; return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() })); @@ -1058,6 +1070,14 @@ async function seed() { temperamentScore: randInt(1, 5), temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)), medicalAlerts: (() => { + // Deterministic alerts for UAT AC pets + if (uc.petName === "TestCooper") { + return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() })); + } + if (uc.petName === "TestRocky") { + return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() })); + } + // Other UAT pets: random if (rand() < 0.3) { const count = rand() < 0.7 ? 1 : 2; return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() })); @@ -1081,6 +1101,14 @@ async function seed() { temperamentScore: randInt(1, 5), temperamentFlags: pickN(temperamentFlagPool, randInt(1, 3)), medicalAlerts: (() => { + // Deterministic alerts for UAT AC pets + if (uc.petName === "TestCooper") { + return pickN(medicalAlertPool.filter((a) => a.type === "behavioral"), 1).map((a) => ({ ...a, id: uuid() })); + } + if (uc.petName === "TestRocky") { + return pickN(medicalAlertPool.filter((a) => a.type === "skin"), 1).map((a) => ({ ...a, id: uuid() })); + } + // Other UAT pets: random if (rand() < 0.3) { const count = rand() < 0.7 ? 1 : 2; return pickN(medicalAlertPool, count).map((a) => ({ ...a, id: uuid() })); -- 2.52.0 From 558c51b3577aef41ccd1269d40b8b27834d18178 Mon Sep 17 00:00:00 2001 From: Flea Flicker Date: Sat, 30 May 2026 04:11:24 +0000 Subject: [PATCH 6/6] fix: correct behavioral alert severity to low (matches TC-API-3.23 expectation) Co-Authored-By: Paperclip --- packages/db/src/seed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/db/src/seed.ts b/packages/db/src/seed.ts index 7460870..985ecf0 100644 --- a/packages/db/src/seed.ts +++ b/packages/db/src/seed.ts @@ -270,7 +270,7 @@ const medicalAlertPool: MedicalAlert[] = [ { id: "", type: "other", description: "Seizure history — avoid flashing lights", severity: "high" }, { id: "", type: "other", description: "Luxating patella — short walks only", severity: "medium" }, { id: "", type: "other", description: "Ear infections — dry thoroughly after bath", severity: "low" }, - { id: "", type: "behavioral", description: "Anxiety — calm environment preferred", severity: "medium" }, + { id: "", type: "behavioral", description: "Anxiety — calm environment preferred", severity: "low" }, { id: "", type: "behavioral", description: "Fear-based aggression — approach with caution", severity: "high" }, { id: "", type: "skin", description: "Contact dermatitis — avoid harsh chemicals", severity: "medium" }, { id: "", type: "skin", description: "Hot spots — monitor and report any worsening", severity: "high" }, -- 2.52.0