Skip to content

feat(dev): local dev API harness — runnable createInboxApi with seeded data (HT-24) - #23

Merged
zaridan merged 2 commits into
mainfrom
feat/ht-24-dev-api-harness
Jul 12, 2026
Merged

feat(dev): local dev API harness — runnable createInboxApi with seeded data (HT-24)#23
zaridan merged 2 commits into
mainfrom
feat/ht-24-dev-api-harness

Conversation

@zaridan

@zaridan zaridan commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Purpose

HT-24: a minimal, runnable local dev server so the upcoming Agent Inbox UI (HT-23) has a real API to integrate against, and the engine gets a standing dogfood surface. Not a deployment — dev tooling only.

What it wires

  • Entry point: npm run dev:apitsx scripts/dev-api.ts (tsx added as a devDependency — no native-Node TS execution path works here, since this codebase's NodeNext-style .js-suffixed imports resolve to .ts files, which plain node --experimental-strip-types does not remap; tsx does).
  • scripts/dev-api.ts — the entry point. Deliberately outside tsconfig.json's include (src/**, tests/** only) and outside package.json's package surface (no files/main/exports touched) — this is the literal "dev tooling, not shipped" boundary the ticket calls for. It still typechecks cleanly under the project's strict settings (verified ad hoc against the real tsconfig.json, then reverted — see verification below).
  • src/dev/http-adapter.ts — a small hand-rolled node:httpRequest/Response bridge (Node's own Readable.toWeb/global fetch types; zero new runtime dependencies). This one lives under src/ and IS part of the normal typecheck/lint/test project, since it's genuine reusable infra, not the entry script itself.
  • src/dev/dev-sender.ts — a dev-only EmailSender that delivers nothing and logs the full OutboundEmail (To, Cc if present, Subject, Message-ID) to stdout. Declares maxSendMs: 5_000, comfortably under DEFAULT_LEASE_MS (120_000) so it never trips assertLeaseExceedsSenderBound — the base branch (PR feat(mail): couple the delivery lease to the sender's enforced timeout (HT-22) #22, merged) already requires every EmailSender to declare this bound.
  • src/dev/seed.ts — seeds 6 conversations on every in-memory boot, reusing the real ConversationStore/sendReply paths (not raw SQL) wherever practical:
    1. inbound-only (no reply yet)
    2. threaded back-and-forth (inbound → outbound sent → inbound → outbound sent)
    3. outbound delivery state: sent
    4. outbound delivery state: failed (via a one-off throwing sender, so sendReply's own failure handling produces the real failed row — not a hand-set status)
    5. outbound delivery state: stale pending (persisted via store.appendThread to model a crash between persist-and-send, since sendReply itself always resolves to sent/failed; its created_at is backdated with one deliberate, narrowly-scoped raw UPDATE — there's no store API for backdating a timestamp, and this is the only raw SQL in the seed script)
    6. a closed conversation
      All names/content are invented, never real data.
  • Auth: HT_DEV_TOKEN env var, default helpthread-dev-token (see judgment call below). Port: HT_DEV_PORT, default 8787. Persistence: optional HT_DEV_DB_PATH (file-backed PGlite instead of in-memory; seeding is skipped in that mode since the point is that data survives restarts).
  • Startup log line prints the base URL, token, seeded count, and an example curl for both the list endpoint and a reply (including the required Idempotency-Key header).

Judgment calls

  • Default token length: the ticket suggested a value "like dev-token", but createInboxApi enforces a 16-char minimum (MIN_API_TOKEN_LENGTH, fail-closed at construction). dev-token (10 chars) crashes the server at boot — caught by actually running it. Changed the default to helpthread-dev-token (20 chars), still unambiguously dev-only.
  • Where the entry point lives: chose a top-level scripts/ directory (new) rather than src/dev/ for the actual executable, so it falls outside tsconfig.json's existing include globs with zero config changes needed. The reusable pieces (http-adapter.ts, dev-sender.ts, seed.ts) live under src/dev/ and stay normally typechecked/linted/tested — only the literal "runnable script" is excluded from the checked project, per the ticket's framing.
  • Small smoke test: added src/dev/seed.test.ts (asserts the 6-conversation count and that sent/failed/stale-pending all appear) since it was cheap and guards against silent seed regressions — the curl evidence below is still the primary proof per the ticket.

Verification (real output, not asserted)

All three gates exit 0:

$ npm run typecheck   → tsc --noEmit -p tsconfig.json    (exit 0)
$ npm run lint        → biome check .                     Checked 55 files in 67ms. No fixes applied. (exit 0)
$ npm test            → vitest run                        Test Files  18 passed (18) / Tests  332 passed (332)  (exit 0)

Server started for real (npm run dev:api), then exercised with curl:

(a) GET conversations, valid token → 200, seeded list (5 open by default; the closed one is excluded, confirmed separately via ?status=closed)

HTTP:200
5 conversations, first: "Question about API rate limits"

(b) GET one conversation → 200

HTTP:200
{"id":"0f3b799b-...","subject":"Can't log into my account", ... ,"threads":[{"direction":"inbound", ...

(c) POST reply WITH Idempotency-Key → 201, dev-sender logs the send

HTTP:201
{"id":"ffbebe39-...","direction":"outbound","bodyText":"Verification reply.","deliveryStatus":"sent", ...}

[dev-sender] would send (nothing actually delivered):
  To:         mia.chen@example.test
  Subject:    Re: Can't log into my account
  Message-ID: <ht.dev.0f3b799b-....@mail.dev.localhost>

(d) REPLAY same Idempotency-Key, different body → 201 with the IDENTICAL original response, no second sender log line

HTTP:201
{"id":"ffbebe39-...", ...}   ← byte-identical to (c)
log lines before=40 after=40  (unchanged — sender not invoked again)

(e) Bad token → 401

{"error":{"code":"unauthorized","message":"Missing or invalid credentials."}}
HTTP:401

Also spot-checked the failed and stale-pending seed rows directly: deliveryStatus":"failed" on the shipping-delay demo, and deliveryStatus":"pending" with a createdAt an hour in the past on the API-rate-limits demo (older than the delivery worker's 5-minute staleAfterMs).

Not a deployment

This is dev-only tooling: an in-memory (by default) PGlite database, a dev-token default, and a sender that logs instead of delivering. Nothing here is wired into any deploy path.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added a dev-only HTTP API server with configurable port, auth token, and optional file-backed database storage.
    • Added seeded demo conversation scenarios to exercise inbox and email-delivery states.
    • Added a dev email sender that logs outbound message details instead of sending.
    • Added local HTTP request/response bridging for fetch-standard handling.
  • Tests
    • Added coverage validating the seeded conversation counts, open/closed split, delivery statuses, and stale pending behavior.
  • Chores
    • Added a new dev:api script and included the tsx runner for development.

…d data (HT-24)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c0ea536-4bdc-4dc6-846a-e06f12ed681c

📥 Commits

Reviewing files that changed from the base of the PR and between b0304d0 and 01979d6.

📒 Files selected for processing (3)
  • scripts/dev-api.ts
  • src/dev/seed.test.ts
  • src/dev/seed.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/dev/seed.test.ts
  • scripts/dev-api.ts
  • src/dev/seed.ts

📝 Walkthrough

Walkthrough

Adds a local development API server with PGlite persistence, seeded conversation scenarios, a non-delivering email sender, and a Node-to-Fetch HTTP adapter. The harness supports in-memory or file-backed databases, graceful shutdown, runtime logging, and delivery-state tests.

Changes

Development harness

Layer / File(s) Summary
Seeded conversation and delivery scenarios
src/dev/seed.ts, src/dev/seed.test.ts
Seeds six conversations covering inbound, sent, failed, stale-pending, and closed states, with tests validating counts, statuses, and stale timing.
Node HTTP and Fetch adapter
src/dev/http-adapter.ts
Bridges Node HTTP requests and responses to Fetch-standard Request and Response objects, including streaming requests and 500-error handling.
Development API startup and lifecycle
scripts/dev-api.ts, src/dev/dev-sender.ts, package.json
Adds the runnable dev:api script, a logging-only email sender, PGlite migration and seeding setup, API/server wiring, runtime diagnostics, and graceful shutdown.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Developer
  participant devApi
  participant PGlite
  participant createInboxApi
  participant createHttpBridge
  Developer->>devApi: start dev:api
  devApi->>PGlite: create database and run migrations
  devApi->>createInboxApi: construct configured API
  devApi->>createHttpBridge: attach HTTP bridge
  Developer->>createHttpBridge: send API request
  createHttpBridge->>createInboxApi: pass Fetch Request
  createInboxApi-->>createHttpBridge: return Fetch Response
  createHttpBridge-->>Developer: send HTTP response
Loading

Possibly related PRs

  • Helpthread/helpthread#6: Establishes project scripts and tooling dependencies extended by this development API entrypoint.
  • Helpthread/helpthread#12: Provides the outbound mail flow and delivery-state lifecycle exercised by the development seed and tests.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: a local dev API harness with runnable createInboxApi and seeded data.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ht-24-dev-api-harness

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/dev-api.ts`:
- Around line 87-91: Update the dev server setup around createHttpBridge and
server.listen to use 127.0.0.1 in both baseUrl and the listen binding, ensuring
the token-protected API is reachable only through loopback.
- Around line 114-118: Update the shutdown function to await completion of
server.close before invoking db.close, wrapping the server callback in a Promise
if needed. Preserve the existing shutdown log and process.exit flow, ensuring
in-flight HTTP requests drain before the database is closed.

In `@src/dev/seed.test.ts`:
- Around line 71-82: The seed test currently verifies only that delivery
statuses are present, allowing incorrect seeded counts to pass. Update the
assertions around deliveryStatuses to validate the exact expected count for each
status, including sent, failed, and pending, while preserving the existing
stale-pending age check for pendingThread.

In `@src/dev/seed.ts`:
- Around line 147-159: Update the seed reply calls in the relevant sent and
closed-conversation setup, including sendReply, to check their results and fail
seeding when the injected sender reports an error. Apply the same
required-result validation to the closed-conversation reply while preserving the
existing six-conversation seed flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: bb7a1f55-74f9-466c-bca5-41e8aacdc832

📥 Commits

Reviewing files that changed from the base of the PR and between 25024e6 and b0304d0.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • package.json
  • scripts/dev-api.ts
  • src/dev/dev-sender.ts
  • src/dev/http-adapter.ts
  • src/dev/seed.test.ts
  • src/dev/seed.ts

Comment thread scripts/dev-api.ts Outdated
Comment thread scripts/dev-api.ts
Comment thread src/dev/seed.test.ts Outdated
Comment thread src/dev/seed.ts Outdated
…strict seeding (CodeRabbit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@zaridan
zaridan merged commit 5a23c33 into main Jul 12, 2026
5 checks passed
@zaridan
zaridan deleted the feat/ht-24-dev-api-harness branch July 12, 2026 01:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant