From 724aba42164acc92b4dbc8694215dc1cab17b665 Mon Sep 17 00:00:00 2001 From: Val Alexander Date: Mon, 6 Jul 2026 10:37:00 -0500 Subject: [PATCH] test: add local app route smoke Signed-off-by: Val Alexander --- .github/workflows/ci.yml | 3 + README.md | 6 ++ docs/coven-github-connection.md | 17 ++++++ package.json | 1 + scripts/demo-app-smoke.mjs | 102 ++++++++++++++++++++++++++++++++ src/adapter.ts | 48 +++++++++++++++ tests/webhook-adapter.test.ts | 61 ++++++++++++++++++- 7 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 scripts/demo-app-smoke.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df2820b..5af7c3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,3 +31,6 @@ jobs: - name: Build run: npm run build + + - name: App route smoke + run: node scripts/demo-app-smoke.mjs diff --git a/README.md b/README.md index 1a0bc81..28fb495 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ reproduced, and changed through PRs instead of server-only edits. connects a GitHub App install to a familiar route. - `docs/coven-github-connection.md` - operator guide for connecting this TypeScript deployment bundle to the canonical `coven-github` app manifest. +- `scripts/demo-app-smoke.mjs` - local signed-delivery demo for the example + policy route. - `scripts/smoke-webhook.sh` - local HMAC signature smoke test for a running webhook endpoint. @@ -77,6 +79,7 @@ while a correctly HMAC-signed GitHub `ping` delivery is accepted without needing ```bash npm test npm run build +npm run smoke:app ``` ## Policy @@ -98,6 +101,9 @@ connection guide in ## Current behavior - Emits headless contract v2 session briefs. +- Supports explicit `COVEN_GITHUB_DEMO_MODE=1` local app smoke runs that verify + signed delivery -> policy route -> delivery/task/result state without calling + GitHub or `coven-code`. - Captures PR checkout metadata and changed-file patches before invoking `coven-code`. - Publishes visible structured review evidence, including `reviewed_files`, diff --git a/docs/coven-github-connection.md b/docs/coven-github-connection.md index 6eb9e9c..cdad21e 100644 --- a/docs/coven-github-connection.md +++ b/docs/coven-github-connection.md @@ -91,6 +91,23 @@ WEBHOOK_SECRET="$GITHUB_WEBHOOK_SECRET" \ That proves the HTTP endpoint and HMAC signature path before any GitHub token or runtime work is attempted. +## Local App Demo + +Before creating a real GitHub App, run the self-contained demo: + +```bash +npm run smoke:app +``` + +The demo starts the built Node server on localhost, signs an `issues.labeled` +payload with the same `sha256=` HMAC format GitHub uses, loads +`config/example-policy.json`, and prints the resulting delivery, task, session +brief, and result paths. + +It runs with `COVEN_GITHUB_DEMO_MODE=1`. That mode is intentionally explicit: +it verifies the app ingress and routing path, but does not mint GitHub +installation tokens, clone repositories, run `coven-code`, or publish comments. + ## Functional App Smoke On a repository where the App is installed: diff --git a/package.json b/package.json index aba7bab..2082721 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build": "tsc -p tsconfig.json", "start": "node dist/src/server.js", "dev": "tsx src/server.ts", + "smoke:app": "npm run build && node scripts/demo-app-smoke.mjs", "test": "node --import tsx --test tests/*.test.ts" }, "devDependencies": { diff --git a/scripts/demo-app-smoke.mjs b/scripts/demo-app-smoke.mjs new file mode 100644 index 0000000..a7d27a2 --- /dev/null +++ b/scripts/demo-app-smoke.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node +import {createHmac} from "node:crypto"; +import {mkdtempSync, readFileSync, writeFileSync} from "node:fs"; +import {tmpdir} from "node:os"; +import {join} from "node:path"; + +import {createConfig} from "../dist/src/adapter.js"; +import {createWebhookServer} from "../dist/src/server.js"; + +const root = new URL("..", import.meta.url).pathname; +const stateDir = mkdtempSync(join(tmpdir(), "coven-github-demo-")); +const policyPath = join(stateDir, "policy.json"); +const secret = "local-demo-webhook-secret"; +const deliveryId = "demo-delivery-issues-labeled"; +const port = Number.parseInt(process.env.PORT || "3137", 10); + +writeFileSync( + policyPath, + readFileSync(new URL("../config/example-policy.json", import.meta.url)), +); + +const payload = { + action: "labeled", + installation: {id: 123456}, + repository: { + id: 987654321, + full_name: "OpenCoven/example", + clone_url: "https://github.com/OpenCoven/example.git", + default_branch: "main", + }, + issue: { + number: 42, + title: "Wire the app", + body: "Make the first app route functional.", + labels: [{name: "coven:fix"}], + }, +}; +const body = Buffer.from(JSON.stringify(payload)); +const signature = `sha256=${createHmac("sha256", secret).update(body).digest("hex")}`; + +const config = createConfig( + { + COVEN_GITHUB_DEMO_MODE: "1", + COVEN_GITHUB_STATE_DIR: stateDir, + COVEN_GITHUB_POLICY_PATH: policyPath, + GITHUB_WEBHOOK_SECRET: secret, + }, + root, +); + +const server = createWebhookServer(config); +await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve)); + +try { + const response = await fetch(`http://127.0.0.1:${port}/webhook`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-GitHub-Event": "issues", + "X-GitHub-Delivery": deliveryId, + "X-Hub-Signature-256": signature, + }, + body, + }); + const result = await response.json(); + const delivery = JSON.parse(readFileSync(join(stateDir, "deliveries", `${deliveryId}.json`), "utf8")); + const task = JSON.parse(readFileSync(join(stateDir, "tasks", `${deliveryId}.json`), "utf8")); + const demoResult = JSON.parse(readFileSync(task.result_path, "utf8")); + + console.log(JSON.stringify({ + ok: response.ok, + status: response.status, + response: result, + state_dir: stateDir, + delivery: { + id: delivery.delivery_id, + event: delivery.event, + routing_result: delivery.routing_result, + state: delivery.state, + repository: delivery.repository, + }, + task: { + id: task.task_id, + state: task.state, + trigger: task.trigger, + familiar: task.familiar, + issue_number: task.task?.issue_number, + publication_state: task.publication_state, + session_brief_path: task.session_brief_path, + result_path: task.result_path, + }, + result: { + status: demoResult.status, + summary: demoResult.summary, + evidence_status: demoResult.review?.evidence_status, + tests_run: demoResult.review?.tests_run, + limitations: demoResult.review?.limitations, + }, + }, null, 2)); +} finally { + await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); +} diff --git a/src/adapter.ts b/src/adapter.ts index 1452cd1..47f30da 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -23,6 +23,7 @@ export interface AdapterConfig { maxReviewFixLoops: number; codexTokensPath: string; maxWebhookBodyBytes: number; + demoMode: boolean; } export interface AdapterRequest { @@ -78,6 +79,7 @@ export function createConfig(env: NodeJS.ProcessEnv = process.env, rootDir = pro maxReviewFixLoops: envInt(env.COVEN_REVIEW_FIX_LOOPS, 0, 0, 5), codexTokensPath: configuredCodexTokensPath(env), maxWebhookBodyBytes: MAX_WEBHOOK_BODY_BYTES, + demoMode: ["1", "true", "yes"].includes((env.COVEN_GITHUB_DEMO_MODE || "").trim().toLowerCase()), }; for (const directory of [config.deliveriesDir, config.tasksDir, config.workspacesDir, config.attemptsDir]) { @@ -787,6 +789,10 @@ async function runTask(config: AdapterConfig, taskId: string): Promise { skills: ["systematic-debugging", "test-driven-development"], }); }); + +test("demo mode handles a signed labeled issue without external GitHub calls", async () => { + const secret = "demo-route-secret"; + const stateDir = tempStateDir(); + const policyPath = join(stateDir, "policy.json"); + writeFileSync( + policyPath, + readFileSync(new URL("../config/example-policy.json", import.meta.url)), + ); + const config = createConfig( + { + COVEN_GITHUB_DEMO_MODE: "1", + COVEN_GITHUB_STATE_DIR: stateDir, + COVEN_GITHUB_POLICY_PATH: policyPath, + GITHUB_WEBHOOK_SECRET: secret, + }, + process.cwd(), + ); + const body = Buffer.from(JSON.stringify({ + action: "labeled", + installation: {id: 123456}, + repository: { + id: 987654321, + full_name: "OpenCoven/example", + clone_url: "https://github.com/OpenCoven/example.git", + default_branch: "main", + }, + issue: { + number: 42, + title: "Wire the app", + body: "Make the first app route functional.", + labels: [{name: "coven:fix"}], + }, + })); + + const response = await callWebhook( + body, + { + "X-GitHub-Event": "issues", + "X-GitHub-Delivery": "delivery-demo-mode", + "X-Hub-Signature-256": signature(secret, body), + }, + "auto", + config, + ); + + assert.equal(response.status, 200); + assert.equal(response.body.action, "accepted"); + assert.equal(response.body.state, "completed"); + + const taskPath = join(stateDir, "tasks", "delivery-demo-mode.json"); + assert.equal(existsSync(taskPath), true); + const task = JSON.parse(readFileSync(taskPath, "utf8")) as JsonObject; + assert.equal(task.state, "completed"); + assert.equal(task.demo_mode, true); + assert.equal(task.publication_state, "demo_mode_no_github_calls"); + assert.equal(existsSync(String(task.session_brief_path)), true); + assert.equal(existsSync(String(task.result_path)), true); +});