From 2737eab9dbb97290fb318fca25384be0173d398c Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Mon, 1 Jun 2026 13:27:00 -0300 Subject: [PATCH 1/5] test: add pnpm test to CI and expand test coverage (COW-995) - Add pnpm test step to CI workflow after codegen - Add ponder:api mock and expand vitest.config.ts aliases - New tests: execution-summary endpoint (5), orders-by-owner endpoint (5+1 todo), erc1271Signature decoder (10) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 3 + tests/__mocks__/ponder-api.ts | 16 ++ tests/api/execution-summary.test.ts | 96 ++++++++++ tests/api/orders-by-owner.test.ts | 179 ++++++++++++++++++ .../decoders/erc1271Signature.test.ts | 162 ++++++++++++++++ vitest.config.ts | 12 ++ 6 files changed, 468 insertions(+) create mode 100644 tests/__mocks__/ponder-api.ts create mode 100644 tests/api/execution-summary.test.ts create mode 100644 tests/api/orders-by-owner.test.ts create mode 100644 tests/application/decoders/erc1271Signature.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 86f7d39..dc2ac81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,3 +35,6 @@ jobs: env: # Public RPC MAINNET_RPC_URL: https://eth.api.pocket.network + + - name: Test + run: pnpm test diff --git a/tests/__mocks__/ponder-api.ts b/tests/__mocks__/ponder-api.ts new file mode 100644 index 0000000..33c414d --- /dev/null +++ b/tests/__mocks__/ponder-api.ts @@ -0,0 +1,16 @@ +import { vi } from "vitest"; + +// Chainable select stub — each .select() call gets its own independent chain so +// tests can use mockReturnValueOnce to return different rows per query. +function makeSelectChain(rows: unknown[] = []) { + const where = vi.fn().mockResolvedValue(rows); + const from = vi.fn().mockReturnValue({ where }); + return { from }; +} + +export const db = { + execute: vi.fn().mockResolvedValue({ rows: [] }), + select: vi.fn().mockReturnValue(makeSelectChain()), + /** Helper used by tests to program per-call return values. */ + __makeSelectChain: makeSelectChain, +}; diff --git a/tests/api/execution-summary.test.ts b/tests/api/execution-summary.test.ts new file mode 100644 index 0000000..edcd80c --- /dev/null +++ b/tests/api/execution-summary.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { OpenAPIHono } from "@hono/zod-openapi"; + +// Mock virtual modules before any ponder-importing source files are loaded. +vi.mock("ponder:api", () => ({ db: { execute: vi.fn() } })); +vi.mock("ponder", () => ({ + sql: Object.assign( + (_s: TemplateStringsArray, ..._v: unknown[]) => ({}), + { raw: (_s: string) => ({}) }, + ), +})); + +import { db } from "ponder:api"; +import { executionSummaryRoute } from "../../src/api/routes"; +import { executionSummaryHandler } from "../../src/api/endpoints/execution-summary"; + +type StatusRow = { status: string; count: string }; + +function buildApp() { + const app = new OpenAPIHono(); + app.openapi(executionSummaryRoute, executionSummaryHandler); + return app; +} + +const EVENT_ID = "177991282000000000000001000000000046395020000000000000001750000000000000054"; + +function makeUrl(eventId = EVENT_ID, chainId = 1) { + return `http://localhost/generator/${eventId}/execution-summary?chainId=${chainId}`; +} + +beforeEach(() => { + vi.mocked(db.execute).mockReset(); +}); + +describe("GET /api/generator/:eventId/execution-summary", () => { + it("returns all-zero counts when no discrete orders exist", async () => { + vi.mocked(db.execute).mockResolvedValue({ rows: [] } as never); + + const res = await buildApp().request(makeUrl()); + expect(res.status).toBe(200); + + const body = await res.json() as Record; + expect(body["totalParts"]).toBe(0); + expect(body["filledParts"]).toBe(0); + expect(body["openParts"]).toBe(0); + expect(body["unfilledParts"]).toBe(0); + expect(body["expiredParts"]).toBe(0); + expect(body["cancelledParts"]).toBe(0); + }); + + it("maps fulfilled, expired, open, unfilled, cancelled to the right fields", async () => { + const rows: StatusRow[] = [ + { status: "fulfilled", count: "3" }, + { status: "expired", count: "7" }, + { status: "open", count: "2" }, + ]; + vi.mocked(db.execute).mockResolvedValue({ rows } as never); + + const body = await (await buildApp().request(makeUrl())).json() as Record; + + expect(body["filledParts"]).toBe(3); + expect(body["expiredParts"]).toBe(7); + expect(body["openParts"]).toBe(2); + expect(body["unfilledParts"]).toBe(0); + expect(body["cancelledParts"]).toBe(0); + expect(body["totalParts"]).toBe(12); + }); + + it("totalParts is the sum of all status counts", async () => { + const rows: StatusRow[] = [ + { status: "fulfilled", count: "10" }, + { status: "cancelled", count: "5" }, + { status: "unfilled", count: "3" }, + ]; + vi.mocked(db.execute).mockResolvedValue({ rows } as never); + + const body = await (await buildApp().request(makeUrl())).json() as Record; + expect(body["totalParts"]).toBe(18); + }); + + it("echoes back the generatorId and chainId", async () => { + vi.mocked(db.execute).mockResolvedValue({ rows: [] } as never); + + const body = await (await buildApp().request(makeUrl(EVENT_ID, 100))).json() as Record; + expect(body["generatorId"]).toBe(EVENT_ID); + expect(body["chainId"]).toBe(100); + }); + + it("returns 400 when chainId query param is missing", async () => { + const app = buildApp(); + const res = await app.request( + `http://localhost/generator/${EVENT_ID}/execution-summary`, + ); + expect(res.status).toBe(400); + }); +}); diff --git a/tests/api/orders-by-owner.test.ts b/tests/api/orders-by-owner.test.ts new file mode 100644 index 0000000..84e8dbf --- /dev/null +++ b/tests/api/orders-by-owner.test.ts @@ -0,0 +1,179 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; + +// Mock virtual modules before any ponder-importing source files are loaded. +vi.mock("ponder:api", () => ({ db: { execute: vi.fn(), select: vi.fn() } })); +vi.mock("ponder:schema", () => { + const ownerMapping = { owner: "owner", chainId: "chainId", address: "address" }; + const conditionalOrderGenerator = { + eventId: "eventId", chainId: "chainId", orderType: "orderType", + owner: "owner", resolvedOwner: "resolvedOwner", status: "status", + ownerAddressType: "ownerAddressType", + }; + const discreteOrder = { + conditionalOrderGeneratorId: "conditionalOrderGeneratorId", + orderUid: "orderUid", chainId: "chainId", status: "status", + sellAmount: "sellAmount", buyAmount: "buyAmount", feeAmount: "feeAmount", + validTo: "validTo", creationDate: "creationDate", + executedSellAmount: "executedSellAmount", executedBuyAmount: "executedBuyAmount", + }; + return { + default: { ownerMapping, conditionalOrderGenerator, discreteOrder }, + ownerMapping, + conditionalOrderGenerator, + discreteOrder, + }; +}); +vi.mock("ponder", () => ({ + and: (..._args: unknown[]) => ({}), + eq: (..._args: unknown[]) => ({}), + inArray: (..._args: unknown[]) => ({}), + or: (..._args: unknown[]) => ({}), +})); + +import { db } from "ponder:api"; +import { ordersByOwnerHandler } from "../../src/api/endpoints/orders-by-owner"; + +const OWNER = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const EVENT_ID = "abc123"; +const CHAIN_ID = 1; + +/** Minimal Hono context stub that satisfies orders-by-owner handler requirements. */ +function makeContext({ + owner = OWNER, + chainId = CHAIN_ID, + status, + ownerAddressType, +}: { + owner?: string; + chainId?: number; + status?: string; + ownerAddressType?: string; +} = {}) { + const responses: Array<{ body: unknown; status: number }> = []; + return { + req: { + valid: (type: "param" | "query") => { + if (type === "param") return { owner }; + return { chainId, status, ownerAddressType }; + }, + }, + json: (body: unknown, httpStatus = 200) => { + // Simulate JSON serialization to catch BigInt issues early. + const serialised = JSON.parse(JSON.stringify(body, (_k, v) => + typeof v === "bigint" ? v.toString() : v + )); + responses.push({ body: serialised, status: httpStatus }); + return { body: serialised, status: httpStatus }; + }, + _responses: responses, + }; +} + +/** Returns a db.select chain that resolves to `rows`. */ +function makeChain(rows: unknown[]) { + const where = vi.fn().mockResolvedValue(rows); + const from = vi.fn().mockReturnValue({ where }); + return { from }; +} + +const GENERATOR = { + eventId: EVENT_ID, + chainId: CHAIN_ID, + orderType: "TWAP", + owner: OWNER, + resolvedOwner: OWNER, + status: "Active", + ownerAddressType: null, +}; + +const ORDER = { + orderUid: "0x" + "bb".repeat(56), + chainId: CHAIN_ID, + status: "fulfilled", + sellAmount: "1000000000000000000", + buyAmount: "2000000000000000000", + feeAmount: "1000000000000000", + validTo: 9_999_999_999, + creationDate: BigInt("1700000000"), + executedSellAmount: "1000000000000000000", + executedBuyAmount: "2000000000000000000", + generatorId: EVENT_ID, +}; + +beforeEach(() => { + vi.mocked(db.select).mockReset(); +}); + +describe("ordersByOwnerHandler", () => { + it("returns empty orders array when no generators are found", async () => { + vi.mocked(db.select) + .mockReturnValueOnce(makeChain([]) as never) // ownerMapping → no proxies + .mockReturnValueOnce(makeChain([]) as never); // generators → none + + const ctx = makeContext(); + await ordersByOwnerHandler(ctx as never, vi.fn() as never); + const result = ctx._responses[0]!.body as { orders: unknown[] }; + expect(result.orders).toEqual([]); + }); + + it("returns empty orders when generators exist but have no discrete orders", async () => { + vi.mocked(db.select) + .mockReturnValueOnce(makeChain([]) as never) + .mockReturnValueOnce(makeChain([GENERATOR]) as never) + .mockReturnValueOnce(makeChain([]) as never); + + const ctx = makeContext(); + await ordersByOwnerHandler(ctx as never, vi.fn() as never); + const result = ctx._responses[0]!.body as { orders: unknown[] }; + expect(result.orders).toEqual([]); + }); + + it("returns enriched orders with embedded generator data", async () => { + vi.mocked(db.select) + .mockReturnValueOnce(makeChain([]) as never) + .mockReturnValueOnce(makeChain([GENERATOR]) as never) + .mockReturnValueOnce(makeChain([ORDER]) as never); + + const ctx = makeContext(); + await ordersByOwnerHandler(ctx as never, vi.fn() as never); + const result = ctx._responses[0]!.body as { orders: Array> }; + + expect(result.orders).toHaveLength(1); + const order = result.orders[0]!; + expect(order["orderUid"]).toBe(ORDER.orderUid); + expect(order["status"]).toBe("fulfilled"); + const gen = order["generator"] as Record; + expect(gen["eventId"]).toBe(EVENT_ID); + expect(gen["orderType"]).toBe("TWAP"); + }); + + it("serialises creationDate as a decimal string (BigInt scalar)", async () => { + vi.mocked(db.select) + .mockReturnValueOnce(makeChain([]) as never) + .mockReturnValueOnce(makeChain([GENERATOR]) as never) + .mockReturnValueOnce(makeChain([ORDER]) as never); + + const ctx = makeContext(); + await ordersByOwnerHandler(ctx as never, vi.fn() as never); + const result = ctx._responses[0]!.body as { orders: Array> }; + expect(result.orders[0]!["creationDate"]).toBe("1700000000"); + }); + + it("includes proxy addresses from ownerMapping in the generator lookup", async () => { + const PROXY = "0xcccccccccccccccccccccccccccccccccccccccc"; + vi.mocked(db.select) + .mockReturnValueOnce(makeChain([{ address: PROXY }]) as never) + .mockReturnValueOnce(makeChain([GENERATOR]) as never) + .mockReturnValueOnce(makeChain([ORDER]) as never); + + const ctx = makeContext(); + await ordersByOwnerHandler(ctx as never, vi.fn() as never); + const result = ctx._responses[0]!.body as { orders: unknown[] }; + expect(result.orders).toHaveLength(1); + }); + + // Regression guard for COW-993 (F15): hash must be present in generator object. + // Enable this test after the F15 fix lands (add `hash` to GeneratorSummary schema + // and select it in ordersByOwnerHandler). + it.todo("includes hash in generator object (COW-993)"); +}); diff --git a/tests/application/decoders/erc1271Signature.test.ts b/tests/application/decoders/erc1271Signature.test.ts new file mode 100644 index 0000000..35e71bf --- /dev/null +++ b/tests/application/decoders/erc1271Signature.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect } from "vitest"; +import { encodeAbiParameters, getAddress, type Hex } from "viem"; +import { decodeEip1271Signature } from "../../../src/application/decoders/erc1271Signature"; + +const HANDLER = "0xaabbccddaabbccddaabbccddaabbccddaabbccdd" as Hex; +const SALT = ("0x" + "ab".repeat(32)) as Hex; +const STATIC_INPUT = "0xdeadbeef" as Hex; +const PROOF: Hex[] = [("0x" + "11".repeat(32)) as Hex]; +const OFFCHAIN_INPUT = "0x1234" as Hex; + +// The PayloadStruct ABI as defined in the decoder — must match exactly. +const PAYLOAD_STRUCT_ABI = [ + { + type: "tuple" as const, + name: "payload", + components: [ + { name: "proof", type: "bytes32[]" as const }, + { + type: "tuple" as const, + name: "params", + components: [ + { name: "handler", type: "address" as const }, + { name: "salt", type: "bytes32" as const }, + { name: "staticInput", type: "bytes" as const }, + ], + }, + { name: "offchainInput", type: "bytes" as const }, + ], + }, +] as const; + +/** Build a Format B signature (ERC1271Forwarder / CoWShed path). */ +function buildFormatB({ + handler = HANDLER, + salt = SALT, + staticInput = STATIC_INPUT, + proof = [] as Hex[], + offchainInput = "0x" as Hex, +} = {}): Hex { + const payloadEncoded = encodeAbiParameters(PAYLOAD_STRUCT_ABI, [ + { proof, params: { handler, salt, staticInput }, offchainInput }, + ]); + // Format B: 384 zero bytes (GPv2Order.Data placeholder) + PayloadStruct ABI encoding + return ("0x" + "00".repeat(384) + payloadEncoded.slice(2)) as Hex; +} + +/** Build a Format A signature (ISafeSignatureVerifier / Safe path). */ +function buildFormatA({ + handler = HANDLER, + salt = SALT, + staticInput = STATIC_INPUT, + proof = [] as Hex[], + offchainInput = "0x" as Hex, +} = {}): Hex { + const payloadEncoded = encodeAbiParameters(PAYLOAD_STRUCT_ABI, [ + { proof, params: { handler, salt, staticInput }, offchainInput }, + ]); + const payloadBytes = payloadEncoded.slice(2); // strip "0x" + const payloadLen = payloadBytes.length / 2; + + // Format A byte layout (all offsets in bytes): + // 0–3 selector 4 bytes + // 4–35 domainSeparator 32 bytes + // 36–67 typeHash 32 bytes + // 68–99 ABI offset to encodeData (=0x80=128 from byte 68) 32 bytes + // 100–131 ABI offset to payload (=0x220=544 from byte 68) 32 bytes + // 132–163 encodeData length (= 384) 32 bytes + // 164–547 abi.encode(GPv2Order.Data) 384 bytes + // 548–579 payload length 32 bytes + // 580–N abi.encode(PayloadStruct) + const padHex = (n: number, bytes: number) => n.toString(16).padStart(bytes * 2, "0"); + const hex = [ + "5fd7e97d", // selector (no 0x prefix here) + "00".repeat(32), // domainSeparator + "00".repeat(32), // typeHash + padHex(0x80, 32), // offset to encodeData + padHex(0x220, 32), // offset to payload + padHex(384, 32), // encodeData length = 384 + "00".repeat(384), // GPv2Order.Data placeholder + padHex(payloadLen, 32), // payload length + payloadBytes, // abi.encode(PayloadStruct) + ].join(""); + return ("0x" + hex) as Hex; +} + +// ─── Format B (ERC1271Forwarder / CoWShed) ──────────────────────────────────── + +describe("decodeEip1271Signature — Format B (ERC1271Forwarder)", () => { + it("round-trips handler, salt, staticInput", () => { + const sig = buildFormatB(); + const result = decodeEip1271Signature(sig); + expect(result).not.toBeNull(); + expect(result!.handler).toBe(HANDLER.toLowerCase()); + expect(result!.salt).toBe(SALT); + expect(result!.staticInput).toBe(STATIC_INPUT); + }); + + it("normalises handler address to lowercase", () => { + // viem requires checksummed addresses for encoding; use getAddress() to checksum first, + // then verify the decoder lowercases the output regardless of the encoded casing. + const checksummed = getAddress(HANDLER); + const sig = buildFormatB({ handler: checksummed }); + const result = decodeEip1271Signature(sig); + expect(result!.handler).toBe(HANDLER.toLowerCase()); + }); + + it("round-trips a non-empty proof array", () => { + const sig = buildFormatB({ proof: PROOF }); + const result = decodeEip1271Signature(sig); + expect(result!.proof).toEqual(PROOF); + }); + + it("round-trips offchainInput", () => { + const sig = buildFormatB({ offchainInput: OFFCHAIN_INPUT }); + const result = decodeEip1271Signature(sig); + expect(result!.offchainInput).toBe(OFFCHAIN_INPUT); + }); + + it("round-trips a multi-byte staticInput", () => { + const longInput = ("0x" + "cc".repeat(64)) as Hex; + const sig = buildFormatB({ staticInput: longInput }); + const result = decodeEip1271Signature(sig); + expect(result!.staticInput).toBe(longInput); + }); +}); + +// ─── Format A (ISafeSignatureVerifier / Safe wallet) ───────────────────────── + +describe("decodeEip1271Signature — Format A (ISafeSignatureVerifier)", () => { + it("detects the 0x5fd7e97d selector and round-trips handler, salt, staticInput", () => { + const sig = buildFormatA(); + const result = decodeEip1271Signature(sig); + expect(result).not.toBeNull(); + expect(result!.handler).toBe(HANDLER.toLowerCase()); + expect(result!.salt).toBe(SALT); + expect(result!.staticInput).toBe(STATIC_INPUT); + }); + + it("round-trips a non-empty proof via Format A", () => { + const sig = buildFormatA({ proof: PROOF }); + const result = decodeEip1271Signature(sig); + expect(result!.proof).toEqual(PROOF); + }); +}); + +// ─── Error / edge cases ─────────────────────────────────────────────────────── + +describe("decodeEip1271Signature — invalid inputs", () => { + it("returns null for empty hex string", () => { + expect(decodeEip1271Signature("0x")).toBeNull(); + }); + + it("returns null for a signature that is too short to contain a payload", () => { + // Only 10 bytes — nothing to decode + expect(decodeEip1271Signature(("0x" + "aa".repeat(10)) as Hex)).toBeNull(); + }); + + it("returns null for random garbage bytes", () => { + const garbage = ("0x" + "ff".repeat(200)) as Hex; + expect(decodeEip1271Signature(garbage)).toBeNull(); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 5fa9726..21d675b 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,19 @@ import { defineConfig } from "vitest/config"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); export default defineConfig({ test: { include: ["src/**/*.test.ts", "tests/**/*.test.ts"], }, + resolve: { + alias: [ + // ponder:schema must come before ponder to avoid prefix-match shadowing. + { find: "ponder:schema", replacement: resolve(__dirname, "tests/__mocks__/ponder-schema.ts") }, + { find: /^ponder$/, replacement: resolve(__dirname, "tests/__mocks__/ponder.ts") }, + { find: "ponder:api", replacement: resolve(__dirname, "tests/__mocks__/ponder-api.ts") }, + ], + }, }); From d3b5036453557ac41a0ac048b6cfbd1f90d50130 Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Tue, 2 Jun 2026 17:08:23 -0300 Subject: [PATCH 2/5] test: fix generator mock hash field, remove duplicate makeChain, add DB-throw coverage (COW-995) Co-Authored-By: Claude Sonnet 4.6 --- tests/api/execution-summary.test.ts | 7 ++++++ tests/api/orders-by-owner.test.ts | 38 ++++++++++++----------------- 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/tests/api/execution-summary.test.ts b/tests/api/execution-summary.test.ts index edcd80c..4e725d5 100644 --- a/tests/api/execution-summary.test.ts +++ b/tests/api/execution-summary.test.ts @@ -93,4 +93,11 @@ describe("GET /api/generator/:eventId/execution-summary", () => { ); expect(res.status).toBe(400); }); + + it("returns 500 when the DB throws", async () => { + vi.mocked(db.execute).mockRejectedValueOnce(new Error("db error")); + + const res = await buildApp().request(makeUrl()); + expect(res.status).toBe(500); + }); }); diff --git a/tests/api/orders-by-owner.test.ts b/tests/api/orders-by-owner.test.ts index 84e8dbf..af5fc6c 100644 --- a/tests/api/orders-by-owner.test.ts +++ b/tests/api/orders-by-owner.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; // Mock virtual modules before any ponder-importing source files are loaded. -vi.mock("ponder:api", () => ({ db: { execute: vi.fn(), select: vi.fn() } })); +// ponder:api is resolved to tests/__mocks__/ponder-api.ts via vitest alias — no inline override needed. vi.mock("ponder:schema", () => { const ownerMapping = { owner: "owner", chainId: "chainId", address: "address" }; const conditionalOrderGenerator = { @@ -69,13 +69,6 @@ function makeContext({ }; } -/** Returns a db.select chain that resolves to `rows`. */ -function makeChain(rows: unknown[]) { - const where = vi.fn().mockResolvedValue(rows); - const from = vi.fn().mockReturnValue({ where }); - return { from }; -} - const GENERATOR = { eventId: EVENT_ID, chainId: CHAIN_ID, @@ -84,6 +77,7 @@ const GENERATOR = { resolvedOwner: OWNER, status: "Active", ownerAddressType: null, + hash: "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1", }; const ORDER = { @@ -107,8 +101,8 @@ beforeEach(() => { describe("ordersByOwnerHandler", () => { it("returns empty orders array when no generators are found", async () => { vi.mocked(db.select) - .mockReturnValueOnce(makeChain([]) as never) // ownerMapping → no proxies - .mockReturnValueOnce(makeChain([]) as never); // generators → none + .mockReturnValueOnce(db.__makeSelectChain([]) as never) // ownerMapping → no proxies + .mockReturnValueOnce(db.__makeSelectChain([]) as never); // generators → none const ctx = makeContext(); await ordersByOwnerHandler(ctx as never, vi.fn() as never); @@ -118,9 +112,9 @@ describe("ordersByOwnerHandler", () => { it("returns empty orders when generators exist but have no discrete orders", async () => { vi.mocked(db.select) - .mockReturnValueOnce(makeChain([]) as never) - .mockReturnValueOnce(makeChain([GENERATOR]) as never) - .mockReturnValueOnce(makeChain([]) as never); + .mockReturnValueOnce(db.__makeSelectChain([]) as never) + .mockReturnValueOnce(db.__makeSelectChain([GENERATOR]) as never) + .mockReturnValueOnce(db.__makeSelectChain([]) as never); const ctx = makeContext(); await ordersByOwnerHandler(ctx as never, vi.fn() as never); @@ -130,9 +124,9 @@ describe("ordersByOwnerHandler", () => { it("returns enriched orders with embedded generator data", async () => { vi.mocked(db.select) - .mockReturnValueOnce(makeChain([]) as never) - .mockReturnValueOnce(makeChain([GENERATOR]) as never) - .mockReturnValueOnce(makeChain([ORDER]) as never); + .mockReturnValueOnce(db.__makeSelectChain([]) as never) + .mockReturnValueOnce(db.__makeSelectChain([GENERATOR]) as never) + .mockReturnValueOnce(db.__makeSelectChain([ORDER]) as never); const ctx = makeContext(); await ordersByOwnerHandler(ctx as never, vi.fn() as never); @@ -149,9 +143,9 @@ describe("ordersByOwnerHandler", () => { it("serialises creationDate as a decimal string (BigInt scalar)", async () => { vi.mocked(db.select) - .mockReturnValueOnce(makeChain([]) as never) - .mockReturnValueOnce(makeChain([GENERATOR]) as never) - .mockReturnValueOnce(makeChain([ORDER]) as never); + .mockReturnValueOnce(db.__makeSelectChain([]) as never) + .mockReturnValueOnce(db.__makeSelectChain([GENERATOR]) as never) + .mockReturnValueOnce(db.__makeSelectChain([ORDER]) as never); const ctx = makeContext(); await ordersByOwnerHandler(ctx as never, vi.fn() as never); @@ -162,9 +156,9 @@ describe("ordersByOwnerHandler", () => { it("includes proxy addresses from ownerMapping in the generator lookup", async () => { const PROXY = "0xcccccccccccccccccccccccccccccccccccccccc"; vi.mocked(db.select) - .mockReturnValueOnce(makeChain([{ address: PROXY }]) as never) - .mockReturnValueOnce(makeChain([GENERATOR]) as never) - .mockReturnValueOnce(makeChain([ORDER]) as never); + .mockReturnValueOnce(db.__makeSelectChain([{ address: PROXY }]) as never) + .mockReturnValueOnce(db.__makeSelectChain([GENERATOR]) as never) + .mockReturnValueOnce(db.__makeSelectChain([ORDER]) as never); const ctx = makeContext(); await ordersByOwnerHandler(ctx as never, vi.fn() as never); From 64382af59ceb8b0f15f034da41147fa59cdc7e4d Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 17:12:19 -0300 Subject: [PATCH 3/5] refactor(tests): address review comments on PR #79 - Export PAYLOAD_STRUCT_ABI from erc1271Signature.ts so the test can import it instead of duplicating the constant inline - Use z.infer for StatusRow.status in execution-summary.test.ts instead of the loose `string` type Co-Authored-By: Claude Sonnet 4.6 --- src/application/decoders/erc1271Signature.ts | 2 +- tests/api/execution-summary.test.ts | 4 +++- .../decoders/erc1271Signature.test.ts | 23 +------------------ 3 files changed, 5 insertions(+), 24 deletions(-) diff --git a/src/application/decoders/erc1271Signature.ts b/src/application/decoders/erc1271Signature.ts index ed4eb86..7f54948 100644 --- a/src/application/decoders/erc1271Signature.ts +++ b/src/application/decoders/erc1271Signature.ts @@ -28,7 +28,7 @@ import { decodeAbiParameters, type Hex } from "viem"; // GPv2Order.Data: 12 fixed-size fields × 32 bytes = 384 bytes total (used for byte-offset math) const GPV2_ORDER_BYTES = 384; -const PAYLOAD_STRUCT_ABI = [ +export const PAYLOAD_STRUCT_ABI = [ { type: "tuple" as const, name: "payload", diff --git a/tests/api/execution-summary.test.ts b/tests/api/execution-summary.test.ts index 4e725d5..669ba60 100644 --- a/tests/api/execution-summary.test.ts +++ b/tests/api/execution-summary.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect, beforeEach, vi } from "vitest"; import { OpenAPIHono } from "@hono/zod-openapi"; +import { z } from "zod"; // Mock virtual modules before any ponder-importing source files are loaded. vi.mock("ponder:api", () => ({ db: { execute: vi.fn() } })); @@ -13,8 +14,9 @@ vi.mock("ponder", () => ({ import { db } from "ponder:api"; import { executionSummaryRoute } from "../../src/api/routes"; import { executionSummaryHandler } from "../../src/api/endpoints/execution-summary"; +import { DiscreteOrderStatusQuery } from "../../src/api/schemas/common"; -type StatusRow = { status: string; count: string }; +type StatusRow = { status: z.infer; count: string }; function buildApp() { const app = new OpenAPIHono(); diff --git a/tests/application/decoders/erc1271Signature.test.ts b/tests/application/decoders/erc1271Signature.test.ts index 35e71bf..27053d1 100644 --- a/tests/application/decoders/erc1271Signature.test.ts +++ b/tests/application/decoders/erc1271Signature.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { encodeAbiParameters, getAddress, type Hex } from "viem"; -import { decodeEip1271Signature } from "../../../src/application/decoders/erc1271Signature"; +import { decodeEip1271Signature, PAYLOAD_STRUCT_ABI } from "../../../src/application/decoders/erc1271Signature"; const HANDLER = "0xaabbccddaabbccddaabbccddaabbccddaabbccdd" as Hex; const SALT = ("0x" + "ab".repeat(32)) as Hex; @@ -8,27 +8,6 @@ const STATIC_INPUT = "0xdeadbeef" as Hex; const PROOF: Hex[] = [("0x" + "11".repeat(32)) as Hex]; const OFFCHAIN_INPUT = "0x1234" as Hex; -// The PayloadStruct ABI as defined in the decoder — must match exactly. -const PAYLOAD_STRUCT_ABI = [ - { - type: "tuple" as const, - name: "payload", - components: [ - { name: "proof", type: "bytes32[]" as const }, - { - type: "tuple" as const, - name: "params", - components: [ - { name: "handler", type: "address" as const }, - { name: "salt", type: "bytes32" as const }, - { name: "staticInput", type: "bytes" as const }, - ], - }, - { name: "offchainInput", type: "bytes" as const }, - ], - }, -] as const; - /** Build a Format B signature (ERC1271Forwarder / CoWShed path). */ function buildFormatB({ handler = HANDLER, From c4fd3b170cc563871a6b1c6add508e1857b6412b Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 17:17:51 -0300 Subject: [PATCH 4/5] refactor(tests): use DiscreteOrderStatusQuery.enum constants in test data Replace raw string literals ("fulfilled", "expired", etc.) in StatusRow test fixtures with Status.* references from the Zod enum so renames are caught at compile time. Co-Authored-By: Claude Sonnet 4.6 --- tests/api/execution-summary.test.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tests/api/execution-summary.test.ts b/tests/api/execution-summary.test.ts index 669ba60..1712208 100644 --- a/tests/api/execution-summary.test.ts +++ b/tests/api/execution-summary.test.ts @@ -16,6 +16,7 @@ import { executionSummaryRoute } from "../../src/api/routes"; import { executionSummaryHandler } from "../../src/api/endpoints/execution-summary"; import { DiscreteOrderStatusQuery } from "../../src/api/schemas/common"; +const Status = DiscreteOrderStatusQuery.enum; type StatusRow = { status: z.infer; count: string }; function buildApp() { @@ -52,9 +53,9 @@ describe("GET /api/generator/:eventId/execution-summary", () => { it("maps fulfilled, expired, open, unfilled, cancelled to the right fields", async () => { const rows: StatusRow[] = [ - { status: "fulfilled", count: "3" }, - { status: "expired", count: "7" }, - { status: "open", count: "2" }, + { status: Status.fulfilled, count: "3" }, + { status: Status.expired, count: "7" }, + { status: Status.open, count: "2" }, ]; vi.mocked(db.execute).mockResolvedValue({ rows } as never); @@ -70,9 +71,9 @@ describe("GET /api/generator/:eventId/execution-summary", () => { it("totalParts is the sum of all status counts", async () => { const rows: StatusRow[] = [ - { status: "fulfilled", count: "10" }, - { status: "cancelled", count: "5" }, - { status: "unfilled", count: "3" }, + { status: Status.fulfilled, count: "10" }, + { status: Status.cancelled, count: "5" }, + { status: Status.unfilled, count: "3" }, ]; vi.mocked(db.execute).mockResolvedValue({ rows } as never); From 6f73a139b469e68e0749697f90543c9f7996470c Mon Sep 17 00:00:00 2001 From: Luiz Gustavo Abou Hatem de Liz Date: Thu, 4 Jun 2026 17:36:10 -0300 Subject: [PATCH 5/5] fix: export makeSelectChain from mock to fix __makeSelectChain typecheck error TypeScript checked db.__makeSelectChain against the real ReadonlyDrizzle type which doesn't have that property. Fix: export makeSelectChain as a standalone named export from ponder-api.ts mock and import it directly in the test. Co-Authored-By: Claude Sonnet 4.6 --- tests/__mocks__/ponder-api.ts | 4 +--- tests/api/orders-by-owner.test.ts | 29 +++++++++++++++-------------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/tests/__mocks__/ponder-api.ts b/tests/__mocks__/ponder-api.ts index 33c414d..ab6ca87 100644 --- a/tests/__mocks__/ponder-api.ts +++ b/tests/__mocks__/ponder-api.ts @@ -2,7 +2,7 @@ import { vi } from "vitest"; // Chainable select stub — each .select() call gets its own independent chain so // tests can use mockReturnValueOnce to return different rows per query. -function makeSelectChain(rows: unknown[] = []) { +export function makeSelectChain(rows: unknown[] = []) { const where = vi.fn().mockResolvedValue(rows); const from = vi.fn().mockReturnValue({ where }); return { from }; @@ -11,6 +11,4 @@ function makeSelectChain(rows: unknown[] = []) { export const db = { execute: vi.fn().mockResolvedValue({ rows: [] }), select: vi.fn().mockReturnValue(makeSelectChain()), - /** Helper used by tests to program per-call return values. */ - __makeSelectChain: makeSelectChain, }; diff --git a/tests/api/orders-by-owner.test.ts b/tests/api/orders-by-owner.test.ts index af5fc6c..df6fd0a 100644 --- a/tests/api/orders-by-owner.test.ts +++ b/tests/api/orders-by-owner.test.ts @@ -31,6 +31,7 @@ vi.mock("ponder", () => ({ })); import { db } from "ponder:api"; +import { makeSelectChain } from "../__mocks__/ponder-api"; import { ordersByOwnerHandler } from "../../src/api/endpoints/orders-by-owner"; const OWNER = "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; @@ -101,8 +102,8 @@ beforeEach(() => { describe("ordersByOwnerHandler", () => { it("returns empty orders array when no generators are found", async () => { vi.mocked(db.select) - .mockReturnValueOnce(db.__makeSelectChain([]) as never) // ownerMapping → no proxies - .mockReturnValueOnce(db.__makeSelectChain([]) as never); // generators → none + .mockReturnValueOnce(makeSelectChain([]) as never) // ownerMapping → no proxies + .mockReturnValueOnce(makeSelectChain([]) as never); // generators → none const ctx = makeContext(); await ordersByOwnerHandler(ctx as never, vi.fn() as never); @@ -112,9 +113,9 @@ describe("ordersByOwnerHandler", () => { it("returns empty orders when generators exist but have no discrete orders", async () => { vi.mocked(db.select) - .mockReturnValueOnce(db.__makeSelectChain([]) as never) - .mockReturnValueOnce(db.__makeSelectChain([GENERATOR]) as never) - .mockReturnValueOnce(db.__makeSelectChain([]) as never); + .mockReturnValueOnce(makeSelectChain([]) as never) + .mockReturnValueOnce(makeSelectChain([GENERATOR]) as never) + .mockReturnValueOnce(makeSelectChain([]) as never); const ctx = makeContext(); await ordersByOwnerHandler(ctx as never, vi.fn() as never); @@ -124,9 +125,9 @@ describe("ordersByOwnerHandler", () => { it("returns enriched orders with embedded generator data", async () => { vi.mocked(db.select) - .mockReturnValueOnce(db.__makeSelectChain([]) as never) - .mockReturnValueOnce(db.__makeSelectChain([GENERATOR]) as never) - .mockReturnValueOnce(db.__makeSelectChain([ORDER]) as never); + .mockReturnValueOnce(makeSelectChain([]) as never) + .mockReturnValueOnce(makeSelectChain([GENERATOR]) as never) + .mockReturnValueOnce(makeSelectChain([ORDER]) as never); const ctx = makeContext(); await ordersByOwnerHandler(ctx as never, vi.fn() as never); @@ -143,9 +144,9 @@ describe("ordersByOwnerHandler", () => { it("serialises creationDate as a decimal string (BigInt scalar)", async () => { vi.mocked(db.select) - .mockReturnValueOnce(db.__makeSelectChain([]) as never) - .mockReturnValueOnce(db.__makeSelectChain([GENERATOR]) as never) - .mockReturnValueOnce(db.__makeSelectChain([ORDER]) as never); + .mockReturnValueOnce(makeSelectChain([]) as never) + .mockReturnValueOnce(makeSelectChain([GENERATOR]) as never) + .mockReturnValueOnce(makeSelectChain([ORDER]) as never); const ctx = makeContext(); await ordersByOwnerHandler(ctx as never, vi.fn() as never); @@ -156,9 +157,9 @@ describe("ordersByOwnerHandler", () => { it("includes proxy addresses from ownerMapping in the generator lookup", async () => { const PROXY = "0xcccccccccccccccccccccccccccccccccccccccc"; vi.mocked(db.select) - .mockReturnValueOnce(db.__makeSelectChain([{ address: PROXY }]) as never) - .mockReturnValueOnce(db.__makeSelectChain([GENERATOR]) as never) - .mockReturnValueOnce(db.__makeSelectChain([ORDER]) as never); + .mockReturnValueOnce(makeSelectChain([{ address: PROXY }]) as never) + .mockReturnValueOnce(makeSelectChain([GENERATOR]) as never) + .mockReturnValueOnce(makeSelectChain([ORDER]) as never); const ctx = makeContext(); await ordersByOwnerHandler(ctx as never, vi.fn() as never);