-
Notifications
You must be signed in to change notification settings - Fork 0
test: add pnpm test to CI and expand test coverage (COW-995) #79
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
lgahdl
merged 6 commits into
develop
from
luizhatem/cow-995-tests-add-pnpm-test-to-ci-workflow-and-expand-integration
Jun 4, 2026
+440
−1
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
2737eab
test: add pnpm test to CI and expand test coverage (COW-995)
lgahdl d3b5036
test: fix generator mock hash field, remove duplicate makeChain, add …
lgahdl 64382af
refactor(tests): address review comments on PR #79
lgahdl c4fd3b1
refactor(tests): use DiscreteOrderStatusQuery.enum constants in test …
lgahdl 22ca1eb
Merge origin/develop into cow-995 — keep ponder:api vitest alias
lgahdl 6f73a13
fix: export makeSelectChain from mock to fix __makeSelectChain typech…
lgahdl File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,3 +35,6 @@ jobs: | |
| env: | ||
| # Public RPC | ||
| MAINNET_RPC_URL: https://eth.api.pocket.network | ||
|
|
||
| - name: Test | ||
| run: pnpm test | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| 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. | ||
| export 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()), | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| 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() } })); | ||
| 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"; | ||
| import { DiscreteOrderStatusQuery } from "../../src/api/schemas/common"; | ||
|
|
||
| const Status = DiscreteOrderStatusQuery.enum; | ||
| type StatusRow = { status: z.infer<typeof DiscreteOrderStatusQuery>; 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<string, unknown>; | ||
| 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: Status.fulfilled, count: "3" }, | ||
| { status: Status.expired, count: "7" }, | ||
| { status: Status.open, count: "2" }, | ||
| ]; | ||
| vi.mocked(db.execute).mockResolvedValue({ rows } as never); | ||
|
|
||
| const body = await (await buildApp().request(makeUrl())).json() as Record<string, unknown>; | ||
|
|
||
| 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: Status.fulfilled, count: "10" }, | ||
| { status: Status.cancelled, count: "5" }, | ||
| { status: Status.unfilled, count: "3" }, | ||
| ]; | ||
| vi.mocked(db.execute).mockResolvedValue({ rows } as never); | ||
|
|
||
| const body = await (await buildApp().request(makeUrl())).json() as Record<string, unknown>; | ||
| 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<string, unknown>; | ||
| 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); | ||
| }); | ||
|
|
||
| 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); | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| import { describe, it, expect, beforeEach, vi } from "vitest"; | ||
|
|
||
| // Mock virtual modules before any ponder-importing source files are loaded. | ||
| // 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 = { | ||
| 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 { makeSelectChain } from "../__mocks__/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, | ||
| }; | ||
| } | ||
|
|
||
| const GENERATOR = { | ||
| eventId: EVENT_ID, | ||
| chainId: CHAIN_ID, | ||
| orderType: "TWAP", | ||
| owner: OWNER, | ||
| resolvedOwner: OWNER, | ||
| status: "Active", | ||
| ownerAddressType: null, | ||
| hash: "0xabc123def456abc123def456abc123def456abc123def456abc123def456abc1", | ||
| }; | ||
|
|
||
| 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(makeSelectChain([]) as never) // ownerMapping → no proxies | ||
| .mockReturnValueOnce(makeSelectChain([]) 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(makeSelectChain([]) as never) | ||
| .mockReturnValueOnce(makeSelectChain([GENERATOR]) as never) | ||
| .mockReturnValueOnce(makeSelectChain([]) 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(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); | ||
| const result = ctx._responses[0]!.body as { orders: Array<Record<string, unknown>> }; | ||
|
|
||
| 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<string, unknown>; | ||
| 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(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); | ||
| const result = ctx._responses[0]!.body as { orders: Array<Record<string, unknown>> }; | ||
| 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(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); | ||
| 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)"); | ||
| }); |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
couldn't this status type be a enum and imported?