|
| 1 | +// The approve page's context grid is a label→value map, so it must only build |
| 2 | +// cells from a real object. POST /api/approvals stores whatever the client |
| 3 | +// sent, and Object.entries() on a string indexes it per character. |
| 4 | +// |
| 5 | +// These boot the real router against a throwaway libsql file database. They |
| 6 | +// skip cleanly when the PWA dependencies are not installed (a fresh repo clone |
| 7 | +// only has the root CLI deps), so the root `npm test` stays green either way. |
| 8 | +// Run `npm install` in apps/pwa to enable them. |
| 9 | +import assert from "node:assert/strict"; |
| 10 | +import { mkdtempSync } from "node:fs"; |
| 11 | +import { tmpdir } from "node:os"; |
| 12 | +import path from "node:path"; |
| 13 | +import { createRequire } from "node:module"; |
| 14 | +import test from "node:test"; |
| 15 | + |
| 16 | +const require = createRequire(import.meta.url); |
| 17 | +let deps = null; |
| 18 | +try { |
| 19 | + deps = { express: require("express"), cookieParser: require("cookie-parser") }; |
| 20 | +} catch { |
| 21 | + deps = null; // pwa dependencies not installed — tests below skip |
| 22 | +} |
| 23 | + |
| 24 | +// Point the app at a throwaway database BEFORE importing its modules (config |
| 25 | +// reads the environment once, at import time). |
| 26 | +const workdir = mkdtempSync(path.join(tmpdir(), "moshcode-pwa-context-test-")); |
| 27 | +process.env.DATABASE_URL = `file:${path.join(workdir, "test.db")}`; |
| 28 | +process.env.SESSION_SECRET = "test-secret"; |
| 29 | + |
| 30 | +async function boot() { |
| 31 | + const { migrate } = await import("../src/migrate.mjs"); |
| 32 | + await migrate(); |
| 33 | + const { run } = await import("../src/db.mjs"); |
| 34 | + const { sessionMiddleware, csrfGuard } = await import("../src/lib/session.mjs"); |
| 35 | + const { approvalsRouter } = await import("../src/routes/approvals.mjs"); |
| 36 | + const { createApiKey } = await import("../src/lib/apikey.mjs"); |
| 37 | + const { id } = await import("../src/lib/crypto.mjs"); |
| 38 | + |
| 39 | + const app = deps.express(); |
| 40 | + app.use(deps.express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString("utf8"); } })); |
| 41 | + app.use(deps.express.urlencoded({ extended: false })); |
| 42 | + app.use(deps.cookieParser()); |
| 43 | + app.use(sessionMiddleware); |
| 44 | + app.use(csrfGuard); |
| 45 | + app.use(approvalsRouter); |
| 46 | + const server = await new Promise((resolve) => { |
| 47 | + const s = app.listen(0, "127.0.0.1", () => resolve(s)); |
| 48 | + }); |
| 49 | + const base = `http://127.0.0.1:${server.address().port}`; |
| 50 | + |
| 51 | + const userId = id(); |
| 52 | + await run(`INSERT INTO users (id, email, display_name, created_at) VALUES (?,?,?,?)`, |
| 53 | + [userId, `${userId}@b.c`, "demo", Date.now()]); |
| 54 | + const { plaintext } = await createApiKey(userId, "cli"); |
| 55 | + |
| 56 | + // POST an approval carrying `context`, then read the page the operator opens |
| 57 | + // and pull out the (label, value) pairs of the context grid. |
| 58 | + const cellsFor = async (context) => { |
| 59 | + const res = await fetch(`${base}/api/approvals`, { |
| 60 | + method: "POST", |
| 61 | + headers: { authorization: `Bearer ${plaintext}`, "content-type": "application/json" }, |
| 62 | + body: JSON.stringify({ message: "ship it?", kind: "ask", script: "ship.mosh", context }), |
| 63 | + }); |
| 64 | + assert.equal(res.status, 201); |
| 65 | + const { id: approvalId, url } = await res.json(); |
| 66 | + const cap = new URL(url).searchParams.get("t"); |
| 67 | + const page = await fetch(`${base}/approve/${approvalId}?t=${cap}`); |
| 68 | + assert.equal(page.status, 200); |
| 69 | + const html = await page.text(); |
| 70 | + return [...html.matchAll(/<div class="label" style="font-size:.6rem">([^<]*)<\/div><div class="mono" style="margin-top:3px">([^<]*)</g)] |
| 71 | + .map((m) => [m[1], m[2]]); |
| 72 | + }; |
| 73 | + |
| 74 | + return { cellsFor, close: () => server.close() }; |
| 75 | +} |
| 76 | + |
| 77 | +test("an object context still renders one cell per key", { skip: !deps && "pwa deps not installed" }, async () => { |
| 78 | + const app = await boot(); |
| 79 | + try { |
| 80 | + assert.deepEqual(await app.cellsFor({ env: "prod", sha: "a1b2c3" }), [["env", "prod"], ["sha", "a1b2c3"]]); |
| 81 | + } finally { app.close(); } |
| 82 | +}); |
| 83 | + |
| 84 | +test("a string context is one cell, not one cell per character", { skip: !deps && "pwa deps not installed" }, async () => { |
| 85 | + const app = await boot(); |
| 86 | + try { |
| 87 | + // Before the fix this was 21 cells labelled 0…20, one letter each. |
| 88 | + assert.deepEqual(await app.cellsFor("deploy failed on prod"), [["context", "deploy failed on prod"]]); |
| 89 | + } finally { app.close(); } |
| 90 | +}); |
| 91 | + |
| 92 | +test("a number or boolean context is shown, not silently dropped", { skip: !deps && "pwa deps not installed" }, async () => { |
| 93 | + const app = await boot(); |
| 94 | + try { |
| 95 | + // Object.entries(42) and Object.entries(true) are both [], so the value the |
| 96 | + // caller sent never reached the page at all. |
| 97 | + assert.deepEqual(await app.cellsFor(42), [["context", "42"]]); |
| 98 | + assert.deepEqual(await app.cellsFor(true), [["context", "true"]]); |
| 99 | + } finally { app.close(); } |
| 100 | +}); |
| 101 | + |
| 102 | +test("an array context keeps its index-labelled cells", { skip: !deps && "pwa deps not installed" }, async () => { |
| 103 | + const app = await boot(); |
| 104 | + try { |
| 105 | + // An array is an object, so it is left exactly as it rendered before. |
| 106 | + assert.deepEqual(await app.cellsFor(["first", "second"]), [["0", "first"], ["1", "second"]]); |
| 107 | + } finally { app.close(); } |
| 108 | +}); |
| 109 | + |
| 110 | +test("no context renders no grid at all", { skip: !deps && "pwa deps not installed" }, async () => { |
| 111 | + const app = await boot(); |
| 112 | + try { |
| 113 | + assert.deepEqual(await app.cellsFor(undefined), []); |
| 114 | + } finally { app.close(); } |
| 115 | +}); |
0 commit comments