Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
415a869
chore: add package.json
SyedHannanMehdi Mar 29, 2026
82ed15b
chore: add tsconfig.json
SyedHannanMehdi Mar 29, 2026
7e66ec3
feat: add shared types
SyedHannanMehdi Mar 29, 2026
15d1fe5
feat: add in-memory store with seed data
SyedHannanMehdi Mar 29, 2026
ebafa74
feat: add bounties router
SyedHannanMehdi Mar 29, 2026
bc43957
feat: add payments router with send endpoint
SyedHannanMehdi Mar 29, 2026
7bf477a
feat: add express app with routing
SyedHannanMehdi Mar 29, 2026
7033d84
feat: add server entry point
SyedHannanMehdi Mar 29, 2026
edf17a3
test: add bounties route tests
SyedHannanMehdi Mar 29, 2026
8465ce3
fix: widen ApiError.details to string (aligns runtime and type)
SyedHannanMehdi Mar 29, 2026
c59dbf9
fix: use result.error.message (string) for details to match ApiError …
SyedHannanMehdi Mar 29, 2026
c64f31d
fix: derive payment fields from bounty, add double-pay/no-recipient g…
SyedHannanMehdi Mar 29, 2026
9232add
feat: add updateBountyStatus helper and reset() for test isolation
SyedHannanMehdi Mar 29, 2026
0ed57a2
fix: rename misleading test; add 404, validation, and field-coverage …
SyedHannanMehdi Mar 29, 2026
de98d70
test: add comprehensive /payments and /payments/send tests covering a…
SyedHannanMehdi Mar 29, 2026
726ebbe
chore: add package.json
SyedHannanMehdi Mar 29, 2026
b6cf67e
chore: add tsconfig.json
SyedHannanMehdi Mar 29, 2026
139368b
feat: add shared TypeScript types
SyedHannanMehdi Mar 29, 2026
3bc0542
feat: add in-memory store with payment simulation
SyedHannanMehdi Mar 29, 2026
55c8a89
feat: add error handler middleware
SyedHannanMehdi Mar 29, 2026
797f0ea
feat: add payments router with send-payment endpoint
SyedHannanMehdi Mar 29, 2026
1b22413
feat: add bounties router (CRUD)
SyedHannanMehdi Mar 29, 2026
91f2510
feat: create Express app factory
SyedHannanMehdi Mar 29, 2026
fbe9f3e
feat: add server entry point
SyedHannanMehdi Mar 29, 2026
eba448e
fix(types): widen ApiError.details to string | Record and simplify Se…
SyedHannanMehdi Mar 29, 2026
d6036ad
fix(bounties): keep details as format() object — now matches widened …
SyedHannanMehdi Mar 29, 2026
0625c0d
fix(payments): derive amount/currency/recipient from bounty, guard do…
SyedHannanMehdi Mar 29, 2026
1a45086
feat(store): add updateBountyStatus helper and expose reset() for tests
SyedHannanMehdi Mar 29, 2026
c35165a
test(bounties): fix misleading test name, add coverage for 404/valida…
SyedHannanMehdi Mar 29, 2026
c072b7d
test(payments): add full coverage for GET /payments, GET /payments/:i…
SyedHannanMehdi Mar 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 22 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
{
"name": "debug-api",
"module": "index.ts",
"type": "module",
"devDependencies": {
"@biomejs/biome": "^1.8.3",
"@types/bun": "latest",
"@types/react": "18.3.4",
"next": "^14.2.5",
"redaxios": "^0.5.1"
},
"peerDependencies": {
"typescript": "^5.0.0"
"name": "fake-algora",
"version": "0.1.0",
"description": "A fake/mock Algora API server for testing bounty payment flows",
"main": "dist/index.js",
"scripts": {
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "vitest run",
"test:watch": "vitest"
Comment on lines +10 to +11
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test script runs vitest, but the repo still contains existing tests/**/*.test.ts that import bun:test (and other Bun-only tooling). With the current setup, vitest run will try to execute those files and fail. Either migrate/replace the Bun tests, or configure Vitest's include/exclude patterns so it only runs the intended Vitest/Supertest tests.

Suggested change
"test": "vitest run",
"test:watch": "vitest"
"test": "vitest run --exclude tests/**/*.test.ts",
"test:watch": "vitest --exclude tests/**/*.test.ts"

Copilot uses AI. Check for mistakes.
},
"dependencies": {
"winterspec": "^0.0.86",
"zod": "^3.23.8",
"zustand": "^4.5.5",
"zustand-hoist": "^2.0.1"
"express": "^4.18.2",
"zod": "^3.22.4",
"uuid": "^9.0.0"
},
"scripts": {
"start": "bun run dev",
"dev": "winterspec dev",
"build": "winterspec bundle -o dist/bundle.js",
"next:dev": "next dev"
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.5",
"@types/uuid": "^9.0.7",
"ts-node-dev": "^2.0.0",
"typescript": "^5.3.3",
"vitest": "^1.2.2",
"supertest": "^6.3.4",
"@types/supertest": "^6.0.2"
}
}
29 changes: 29 additions & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import express from "express";
import bountiesRouter from "./routes/bounties";
import paymentsRouter from "./routes/payments";

const app = express();

app.use(express.json());

// ---------------------------------------------------------------------------
// Health check
// ---------------------------------------------------------------------------
app.get("/health", (_req, res) => {
res.json({ status: "ok", service: "fake-algora" });
});

// ---------------------------------------------------------------------------
// Routes
// ---------------------------------------------------------------------------
app.use("/bounties", bountiesRouter);
app.use("/payments", paymentsRouter);

// ---------------------------------------------------------------------------
// 404 fallthrough
// ---------------------------------------------------------------------------
app.use((_req, res) => {
res.status(404).json({ error: "Not found" });
});

export default app;
14 changes: 14 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import app from "./app";

const PORT = process.env.PORT ?? 3000;
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

process.env.PORT is a string when set; passing it directly to app.listen can make Node treat it as a pipe name (e.g. "3000") instead of port 3000. Parse/coerce to a number (and validate it) before calling listen to avoid binding to the wrong endpoint.

Suggested change
const PORT = process.env.PORT ?? 3000;
const PORT = Number(process.env.PORT) || 3000;

Copilot uses AI. Check for mistakes.

app.listen(PORT, () => {
console.log(`🚀 fake-algora running on http://localhost:${PORT}`);
console.log(` GET /health`);
console.log(` GET /bounties`);
console.log(` POST /bounties`);
console.log(` GET /bounties/:id`);
console.log(` GET /payments`);
console.log(` GET /payments/:id`);
console.log(` POST /payments/send`);
});
70 changes: 70 additions & 0 deletions src/routes/bounties.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { describe, it, expect, beforeEach } from "vitest";
import request from "supertest";
import app from "../app";
import { store } from "../store";

beforeEach(() => {
store.reset();
});

describe("GET /bounties", () => {
it("returns an empty list when no bounties exist", async () => {
const res = await request(app).get("/bounties");
expect(res.status).toBe(200);
expect(res.body.bounties).toEqual([]);
});

it("returns seeded bounties when store is populated", async () => {
store.createBounty({
issue_number: 1,
repo: "tscircuit/fake-algora",
amount_usd: 10,
currency: "USD",
status: "open",
recipient_username: null,
});
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test name is misleading: the store is reset in beforeEach, so this test isn't using seeded bounties; it's creating one explicitly. Rename the test (or adjust setup) so the description matches the behavior.

Copilot uses AI. Check for mistakes.
const res = await request(app).get("/bounties");
expect(res.status).toBe(200);
expect(res.body.bounties).toHaveLength(1);
});
});

describe("POST /bounties", () => {
it("creates a bounty with valid body", async () => {
const res = await request(app).post("/bounties").send({
issue_number: 5,
repo: "tscircuit/core",
amount_usd: 25,
});
expect(res.status).toBe(201);
expect(res.body.bounty.amount_usd).toBe(25);
expect(res.body.bounty.status).toBe("open");
});

it("returns 400 for invalid body", async () => {
const res = await request(app).post("/bounties").send({});
expect(res.status).toBe(400);
expect(res.body.error).toMatch(/invalid/i);
});
});

describe("GET /bounties/:id", () => {
it("returns 404 for unknown id", async () => {
const res = await request(app).get("/bounties/nonexistent");
expect(res.status).toBe(404);
});

it("returns the bounty for a known id", async () => {
const bounty = store.createBounty({
issue_number: 10,
repo: "tscircuit/fake-algora",
amount_usd: 100,
currency: "USD",
status: "open",
recipient_username: null,
});
const res = await request(app).get(`/bounties/${bounty.id}`);
expect(res.status).toBe(200);
expect(res.body.bounty.id).toBe(bounty.id);
});
});
44 changes: 44 additions & 0 deletions src/routes/bounties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Router } from "express";
import { z } from "zod";
import { store } from "../store";

const router = Router();

// GET /bounties — list all bounties
router.get("/", (_req, res) => {
res.json({ bounties: store.listBounties() });
});

// GET /bounties/:id — get a single bounty
router.get("/:id", (req, res) => {
const bounty = store.getBounty(req.params.id);
if (!bounty) {
return res.status(404).json({ error: "Bounty not found" });
}
res.json({ bounty });
});

// POST /bounties — create a new bounty
const CreateBountySchema = z.object({
issue_number: z.number().int().positive(),
repo: z.string().min(1),
amount_usd: z.number().positive(),
currency: z.string().default("USD"),
recipient_username: z.string().nullable().default(null),
status: z
.enum(["open", "in_progress", "completed", "payment_pending", "paid"])
.default("open"),
});

router.post("/", (req, res) => {
const result = CreateBountySchema.safeParse(req.body);
if (!result.success) {
return res
.status(400)
.json({ error: "Invalid request body", details: result.error.format() });
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This route also returns details: result.error.format() (an object), which doesn't match the ApiError.details?: string type in src/types.ts. Align the runtime response shape and the exported types (either return a string message or widen the details type).

Suggested change
.json({ error: "Invalid request body", details: result.error.format() });
.json({
error: "Invalid request body",
details: JSON.stringify(result.error.format()),
});

Copilot uses AI. Check for mistakes.
}
const bounty = store.createBounty(result.data);
res.status(201).json({ bounty });
});

export default router;
87 changes: 87 additions & 0 deletions src/routes/payments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Router } from "express";
import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { store } from "../store";

const router = Router();

// ---------------------------------------------------------------------------
// Schema
// ---------------------------------------------------------------------------
const SendPaymentSchema = z.object({
bounty_id: z.string().min(1, "bounty_id is required"),
recipient_username: z.string().min(1, "recipient_username is required"),
amount_usd: z.number().positive("amount_usd must be positive"),
currency: z.string().default("USD"),
});

// ---------------------------------------------------------------------------
// GET /payments — list all payments
// ---------------------------------------------------------------------------
router.get("/", (_req, res) => {
res.json({ payments: store.listPayments() });
});

// ---------------------------------------------------------------------------
// GET /payments/:id — get a single payment
// ---------------------------------------------------------------------------
router.get("/:id", (req, res) => {
const payment = store.getPayment(req.params.id);
if (!payment) {
return res.status(404).json({ error: "Payment not found" });
}
res.json({ payment });
});

// ---------------------------------------------------------------------------
// POST /payments/send — send a payment for a bounty
// ---------------------------------------------------------------------------
router.post("/send", (req, res) => {
const result = SendPaymentSchema.safeParse(req.body);
if (!result.success) {
return res
.status(400)
.json({ error: "Invalid request body", details: result.error.format() });
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This endpoint returns details: result.error.format() on validation failure, but ApiError.details is typed as string in src/types.ts. Either change the response to a string (e.g. result.error.message) or widen the type of details to match what the API actually returns.

Suggested change
.json({ error: "Invalid request body", details: result.error.format() });
.json({ error: "Invalid request body", details: result.error.message });

Copilot uses AI. Check for mistakes.
}

const { bounty_id, recipient_username, amount_usd, currency } = result.data;

// Validate the bounty exists
const bounty = store.getBounty(bounty_id);
if (!bounty) {
return res
.status(404)
.json({ error: "Bounty not found", details: `No bounty with id "${bounty_id}"` });
}

// Guard: don't allow double-payment
if (bounty.status === "paid") {
return res.status(409).json({
error: "Bounty already paid",
details: `Bounty "${bounty_id}" has already been marked as paid`,
});
}

// Create payment record (simulate async processing → immediately complete)
const payment = store.createPayment({
bounty_id,
recipient_username,
amount_usd,
currency,
status: "completed",
transaction_id: `txn_${uuidv4().replace(/-/g, "").slice(0, 16)}`,
});
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

amount_usd, currency, and recipient_username are accepted from the request body even though the bounty already has amount/currency (and possibly an assigned recipient). This can create inconsistent records (paying a bounty for the wrong amount/currency/recipient). Consider deriving these fields from the bounty (or at least validating they match) before creating the payment and marking the bounty as paid.

Copilot uses AI. Check for mistakes.

// Update bounty status
store.updateBounty(bounty_id, {
status: "paid",
recipient_username,
});

res.status(201).json({
payment,
message: `Payment of $${amount_usd} ${currency} sent to @${recipient_username} for bounty ${bounty_id}`,
});
});
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POST /payments/send introduces new behavior (payment creation, double-payment guard, bounty status update) but there are no corresponding Vitest/Supertest tests. Add tests for success, invalid body (400), unknown bounty (404), and already-paid bounty (409) to prevent regressions.

Copilot uses AI. Check for mistakes.

export default router;
Loading
Loading