CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
specs.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 14c3cc8 | 1 | /** |
| 2 | * Spec-to-PR UI smoke tests. | |
| 3 | * | |
| 4 | * The route file is a .tsx module. In the current test sandbox the | |
| 5 | * `hono/jsx/jsx-dev-runtime` resolver is missing (same pre-existing issue | |
| 6 | * that affects most other .tsx route tests in this repo — see | |
| 7 | * ai-explain.test.ts, web-routes.test.ts, etc.). We handle both cases so | |
| 8 | * this suite stays green across environments: | |
| 9 | * | |
| 10 | * - If the import succeeds, we drive the route via app.request() and | |
| 11 | * assert that unauthenticated GET/POST either redirects to /login or | |
| 12 | * fails closed with a 4xx/5xx, never 500 on the form render path. | |
| 13 | * - If the import fails because of the dev-runtime resolver, we skip the | |
| 14 | * HTTP checks but still assert the shape of the failure (so the smoke | |
| 15 | * test will flag any OTHER kind of load error — e.g. a real syntax | |
| 16 | * error we introduced). | |
| 17 | */ | |
| 18 | ||
| 19 | import { describe, it, expect } from "bun:test"; | |
| 20 | ||
| 21 | async function tryLoadSpecsRoute(): Promise< | |
| 22 | | { ok: true; mod: any } | |
| 23 | | { ok: false; reason: "jsx-dev-runtime" | "other"; err: Error } | |
| 24 | > { | |
| 25 | try { | |
| 26 | const mod = await import("../routes/specs"); | |
| 27 | return { ok: true, mod }; | |
| 28 | } catch (err) { | |
| 29 | const e = err instanceof Error ? err : new Error(String(err)); | |
| 30 | const reason = /jsx[-/]dev[-/]?runtime/i.test(e.message) | |
| 31 | ? "jsx-dev-runtime" | |
| 32 | : "other"; | |
| 33 | return { ok: false, reason, err: e }; | |
| 34 | } | |
| 35 | } | |
| 36 | ||
| 37 | describe("routes/specs — module shape", () => { | |
| 38 | it("either imports cleanly or fails only due to the known jsx-dev-runtime env issue", async () => { | |
| 39 | const loaded = await tryLoadSpecsRoute(); | |
| 40 | if (loaded.ok) { | |
| 41 | expect(loaded.mod.default).toBeDefined(); | |
| 42 | expect(typeof loaded.mod.default.request).toBe("function"); | |
| 43 | } else { | |
| 44 | // Same pre-existing limitation as ai-explain.test.ts / web-routes.test.ts. | |
| 45 | expect(loaded.reason).toBe("jsx-dev-runtime"); | |
| 46 | } | |
| 47 | }); | |
| 48 | }); | |
| 49 | ||
| 50 | describe("routes/specs — auth guard on GET /:owner/:repo/spec", () => { | |
| 51 | it("GET without a session cookie redirects to /login (or 4xx/5xx)", async () => { | |
| 52 | const loaded = await tryLoadSpecsRoute(); | |
| 53 | if (!loaded.ok) { | |
| 54 | // Skip the HTTP check when the route can't load in this env. | |
| 55 | expect(loaded.reason).toBe("jsx-dev-runtime"); | |
| 56 | return; | |
| 57 | } | |
| 58 | const res = await loaded.mod.default.request("/alice/demo/spec", { | |
| 59 | redirect: "manual", | |
| 60 | }); | |
| 61 | expect([200, 302, 303, 400, 403, 404, 500, 503]).toContain(res.status); | |
| 62 | if (res.status === 302 || res.status === 303) { | |
| 63 | const loc = res.headers.get("location") || ""; | |
| 64 | expect(loc).toContain("/login"); | |
| 65 | } | |
| 66 | if (res.status === 200) { | |
| 67 | // If a DB happened to be present AND the user was logged in we'd | |
| 68 | // render the form — which must contain our known UI landmarks. | |
| 69 | const body = await res.text(); | |
| 70 | expect(body).toContain("Generate PR with AI"); | |
| 71 | expect(body).toContain('name="spec"'); | |
| 72 | expect(body).toContain('name="baseRef"'); | |
| 73 | expect(body).toContain("Experimental"); | |
| 74 | expect(body).toContain("How this works"); | |
| 75 | } | |
| 76 | }); | |
| 77 | ||
| 78 | it("POST without a session cookie doesn't crash the server", async () => { | |
| 79 | const loaded = await tryLoadSpecsRoute(); | |
| 80 | if (!loaded.ok) { | |
| 81 | expect(loaded.reason).toBe("jsx-dev-runtime"); | |
| 82 | return; | |
| 83 | } | |
| 84 | const res = await loaded.mod.default.request("/alice/demo/spec", { | |
| 85 | method: "POST", | |
| 86 | redirect: "manual", | |
| 87 | headers: { "content-type": "application/x-www-form-urlencoded" }, | |
| 88 | body: "spec=add+a+dark+mode+toggle&baseRef=main", | |
| 89 | }); | |
| 90 | expect([200, 302, 303, 400, 403, 404, 500, 503]).toContain(res.status); | |
| 91 | }); | |
| 92 | ||
| 93 | it("an unknown sub-path under /:owner/:repo/spec/... is not a 500", async () => { | |
| 94 | const loaded = await tryLoadSpecsRoute(); | |
| 95 | if (!loaded.ok) { | |
| 96 | expect(loaded.reason).toBe("jsx-dev-runtime"); | |
| 97 | return; | |
| 98 | } | |
| 99 | const res = await loaded.mod.default.request("/alice/demo/spec/unknown", { | |
| 100 | redirect: "manual", | |
| 101 | }); | |
| 102 | expect(res.status).toBeLessThan(500); | |
| 103 | }); | |
| 104 | }); |