Skip to content

Architecture ​

Topology ​

text
Browser / CLI / MCP client
  │  React dashboard (localStorage: settings + keys)
  │  edge-spec CLI or local stdio MCP server (keys from env)
  │  fetch + SSE, credentials as headers
  ▼
Cloudflare Worker (Hono)
  ├─ /api/*            JSON + SSE
  ├─ /webhook          GitHub webhook receiver
  ├─ ASSETS binding    built React app (assets-first routing)
  ├─ AI binding        Workers AI
  └─ TEST_GENERATION_WORKFLOW
          │
          ▼
     TestGenerationWorkflow  (durable, deployment-owned credentials)

Outbound from the Worker:
  api.github.com                  tree, files, branches, commits, PRs
  provider endpoint               OpenAI-compatible / Anthropic / Gemini
  user sandbox runner (optional)  executes `bun test`

The Worker holds no session and no user configuration. A request carries everything needed to serve it. Run history, backlog, policies, memory, schedules, workspace settings and audit are an optional D1 binding (RUNS_DB); the raw validation bundles and drift summaries live in R2 (ARTIFACTS). Without the bindings the related routes answer enabled: false and nothing is stored. Generated test source, prompts and keys are never written to either store.

Frontend bundle ​

The dashboard is a Vite SPA served from dist/web by the ASSETS binding. The build splits it so a deploy does not invalidate the heavy parts:

ChunkWhatWhen it loads
indexthe dashboard codealways
reactreact and react-domalways
radixthe Radix primitivesalways
vendorthe restalways
iconslucidealways
trees@pierre/treeswhen a file view opens
kirarithe code highlighterwhen a generated test is shown

The two heavy ones are behind React.lazy (apps/web/src/components/path-tree.tsx and apps/web/src/components/code-view.tsx), so the file tree and the syntax highlighter are only parsed and executed when a reader actually needs them. apps/web/vite.config.ts names the vendor chunks with manualChunks, so a change in the app code does not invalidate the cached vendor files.

Modules ​

ModuleResponsibilityRules
packages/core/src/types.tsGap, Generation, Validation, RunEvent, HttpErrorNo HTTP or provider knowledge
packages/core/src/catalog.tsProvider/model catalog, resolveAi, parseRepo, parseRef, pipelineConfigSingle source of truth for validation and defaults
packages/core/src/github.tsREST client: tree, file, branch, commit, PR, PR listfetch, no SDK; path segments encoded
packages/core/src/scanner.tsclassifyTree, ancestor test discovery, OpenAPI parsing, repository overviewRegex-based, no TypeScript parser
packages/core/src/coverage.tsLCOV parsing and coverageGapsFacts from a runner; no report means "not measured"
packages/core/src/openapi.tsDeterministic contract tests, apiGaps, JUnit parsingOne test named METHOD /path per operation; no AI
packages/core/src/ci.tsReads the repository's checks, formatter and style rulesPure functions over the tree, the manifest and workflows
packages/core/src/ai.tsPrompt, code extraction, provider dispatchKey only goes into upstream request headers
packages/core/src/validate.tsStatic checks + optional sandbox executionverified is true only after a runner ran
packages/core/src/sandbox.tsv1/v2 request contract, config parsing, runner healthv1 {files, command} stays the default body
packages/core/src/api-surface.tsInfers OpenAPI from route calls when a spec is missingExplicitly inferred; regex, not an AST
packages/core/src/pipeline.tsAsync generator for scan → generate → validate → correct → PRTransport-agnostic; bounded pool + channel
apps/worker/src/index.tsHono routes, header parsing, SSE framingTranslates HTTP to core calls
apps/worker/src/workflow.tsDurable webhook automationDeployment credentials only
apps/web/src/DashboardApp.tsx owns run state; components are presentational
packages/ui/src/React primitives and brand tokensDashboard imports components; docs imports theme.css
packages/client/src/Worker API client, SSE parsing, env defaultsNo pipeline logic; CLI and MCP use it
packages/cli/src/scan, generate, run, catalog commandsSecrets from env, never flags
packages/mcp/src/Stdio MCP tools over the shared clientPR tool requires a one-time approval token

Interactive run (POST /api/run) ​

text
POST /api/run
  parse repo + ref                     400 on invalid input
  resolve AI choice                    400 if provider/key/model/base URL unusable
  open SSE stream                      200 text/event-stream
  runPipeline(...)
    step scan       GitHub tree → classifyTree (+ OpenAPI specs in api mode)
    gaps event      missing + coverage + api + unsupported, with totals
    pool            up to `concurrency` files at once
      step generate fetch source → prompt chosen model → extract code
      generation    full test file, with sourcePath
      step validate static checks, then sandbox if configured
      validation    {ok, verified, output, durationMs}, with sourcePath
      loop          up to maxRetries corrections while !ok
    api mode        deterministic contract tests → sandbox → JUnit → api event
    step PR         ready to open                    (the reviewer opens it)
    result          {ok, ready?, reason?}
  done frame

Concurrency is bounded (1–6, default 3) and also capped by the sandbox's own concurrency. Events carry sourcePath, so the dashboard keys generations and validations per file; ready is assembled in scan order, not completion order. One file that exhausts its retries fails alone and the rest of the run continues.

  1. Static checks always run. Failure stops the run before any sandbox call.
  2. Sandbox runs only when the request supplies a runner URL. The result is verified: true.
  3. No sandbox returns ok: true only when static checks pass, with verified: false and an explicit message. The UI labels this static check only.

A failed validation is fed back to the model with the previous code and runner output. The loop is bounded by maxRetries (clamped to 0–5). If it still fails, no PR is opened and the result is ok: false.

Only the first gap is attempted per run. Scanning is cheap; generating is not. The dashboard lets the user generate another gap individually via POST /api/generate.

Webhook automation (TestGenerationWorkflow) ​

text
GitHub ── POST /webhook ──▶ verify HMAC (if GH_WEBHOOK_SECRET is set)
                            parse repository.full_name + ref + installation.id
                            push:          binding.create({ repo, ref, event:"push" })
                            pull_request:  binding.create({ repo, ref:headSha,
                                            pullRequest, event:"pull_request" })
                            issue_comment: authorize association, parse /edgespec,
                                           rate limit, then binding.create({
                                             pullRequest, event:"pr_command", command })
                                                          → 202 { workflowId }
Workflow · push
  scan-repository     tree, first gap, source          (durable step)
  generate-test       Workers AI                       (2 retries, exponential)
  validate-test       static + optional sandbox
  correct-test-N      bounded by maxRetries
  create-pull-request branch + file + PR

Workflow · pull_request (advisory)
  changed-files       first page of PR files
  scan-changed        changed sources without tests
  impact-graph        test import closure (≤200 files) → dependents + reasons
  run-affected-tests  selected tests, only with a runner
  publish-check       upsert one check run + one marked comment

Workflow · pr_command (authorized /edgespec comment)
  resolve-pull        head commit
  changed-files       first page of PR files
  scan-changed        changed sources and their tests
  retest|coverage|explain
  publish-command     one reply + the same EdgeSpec check upserted

Workflow parameters are persisted by Cloudflare. Therefore the workflow:

  • never accepts a visitor's provider key,
  • uses the deployment's AI binding,
  • mints a repo-scoped GitHub App installation token when the App secrets are configured, cached only until shortly before expiry; GH_TOKEN is the fallback for deployments without an App,
  • rejects non-Workers-AI providers with a clear error.

The dashboard remains the place for bring-your-own-key runs.

Test-debt backlog ​

A full dashboard scan and the scheduled drift scan both call the same backlog sync. It upserts the current gaps, preserves accepted/rejected/ignored decisions, closes paths that disappeared, and reopens a closed path that returns. Ownership is the first CODEOWNERS owner for the path (last matching rule wins). The dashboard reads and updates the same D1 rows through /api/backlog.

Scheduled drift (scheduled handler) ​

text
cron (daily) ──▶ due schedules from D1
                 tree + OpenAPI operations
                 compare with drift_snapshots
                 changed? → webhook summary + optional R2 artifact
                 update snapshot + last_run_at

The first scan stores a baseline. Only a repository whose untested paths or contract operations changed produces one notification. The handler never generates tests or opens a pull request.

GitHub client ​

GitHub wraps the operations used by the pipeline:

  • tree(repo, ref) — recursive tree, blobs only for scanning.
  • file(repo, path, ref) — decodes Base64 content.
  • branches(repo), defaultBranch(repo) — feed the header branch dropdown.
  • headSha, createBranch, putFile, openPull — one branch and one commit.
  • pulls(repo) — 20 newest, all states.

Authentication is optional per request: a token enables private repositories, avoids anonymous rate limits, and is required to push or open PRs. Public reads work without one.

Workers detail: fetch is captured as a local before invocation. Calling it as this.fetchImpl(...) throws Illegal invocation in the Workers runtime.

Scanning rules ​

A path is a source file when it matches .ts .tsx .js .jsx .mjs .cjs and is not:

  • a test (*.test.*, *.spec.*),
  • a declaration (*.d.ts, *.d.mts, *.d.cts),
  • under generated/vendor directories (dist, build, out, coverage, node_modules, vendor, generated, __generated__, migrations),
  • under a test directory (test, tests, __tests__, __mocks__, e2e, fixtures),
  • a tool config (vite, vitest, jest, playwright, tailwind, postcss, eslint, wrangler, bun, tsconfig, setup, webpack, rollup).

A gap is a source file whose sibling <name>.test.<ext> (or .spec) is absent. Exports are extracted with regexes (best effort, capped at 12). Risk is a path heuristic: auth/token/payment/webhook keywords → high; util/helper/theme/UI keywords → low; otherwise medium, or high above 20 exports.

Security boundaries ​

BoundaryControl
Repository inputparseRepoInput accepts a folder start path in the header; the API gets owner/name only, via parseRepo, with no traversal
Branch selectorValues come from GitHub's branch list; parseRef still rejects .., leading /, unsafe characters
Provider selectionCatalog allowlist; unknown providers and models are rejected
Base URLOnly the custom provider (private id) accepts one; known providers cannot be redirected, preventing key exfiltration
Workflow paramsNever contain user keys
CLI/MCP credentialsEnvironment only; no key flags or tool arguments
WebhookHMAC SHA-256 verification when a secret is configured
Code executionGenerated code is never executed on the Worker; only an external sandbox may run it
Generated contentAlways treated as a draft; PR body says so

Keys are held in browser localStorage only; the Worker forwards them and does not log or persist them. This is still a bearer-token model: a compromised browser or deployment can read them. See Configuration.

Failure behaviour ​

  • Validation errors before streaming → JSON {error, status} with 4xx/5xx.
  • Errors after streaming starts → an error event, then result (ok: false), then done; the HTTP status stays 200 because headers are already sent.
  • Provider 401/403 → 400 with a short message (so the UI can say "check your key"); other provider failures → 502.
  • Sandbox unreachable → validation ok: false, verified: false.
  • GitHub failures include a readable message; 404 usually means private repo or a bad ref.

Why these choices ​

  • No SDKs for GitHub or AI: the surface used here is small, and fetch keeps the Worker bundle lean.
  • No server state: removes storage, GDPR and cache-invalidation questions for a tool that runs on demand.
  • Async generator pipeline: the interactive path is one implementation over SSE. The durable Workflow calls the same core scan/generate/validate functions inside its steps, so behaviour stays close without persisting user keys.
  • CLI and MCP are API clients: they reuse the Worker's scan/generate/run/PR routes instead of duplicating the pipeline, so the dashboard, terminal, and agents all see the same validation and sandbox behavior.
  • Radix primitives (shadcn-style): accessible dialog, select, tabs, switch and slider without hand-rolling focus management or keyboard behaviour.

Generated tests are a draft, not a proof.