Public: anyone can read this; the crew can change it.
A thirty-pixel thumbnail loads a full-size screenshot #173
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/159-a-thirty-pixel-thumbnail-loads-a-full-si → main 1 commit fork
9 files
changed +143
−58
docs/APPS.md +10 −7
| @@ -449,13 +449,16 @@ older ones are history, not status. | ||
| 449 | 449 | ### The look |
| 450 | 450 | |
| 451 | 451 | When a deploy comes up green the forge takes a picture of it — the page at |
| 452 | `/`, at a phone's width (390×844) and a laptop's (1280×800) — and keeps the | |
| 453 | pictures beside the commit they are of, so a person judges a change from | |
| 454 | what it is rather than from what it says. The pull page shows both under | |
| 455 | the checks, with the commit and a word when the head has moved since; the | |
| 456 | studio's ask row leads with the phone picture; an app's studio head shows | |
| 457 | how production looks now. Pictures are served at | |
| 458 | `/:owner/:name/looks/<env>/<commit>/{phone,desktop}.png` with the | |
| 452 | `/`, at a phone's width (390×844), a laptop's (1280×800), and a thumb's | |
| 453 | (200×125, desktop's own aspect, small) — and keeps the pictures beside the | |
| 454 | commit they are of, so a person judges a change from what it is rather than | |
| 455 | from what it says. The pull page shows the phone and desktop pair under the | |
| 456 | checks, with the commit and a word when the head has moved since; the | |
| 457 | studio's ask row and the board's landed row lead with the thumb; an app's | |
| 458 | studio head shows how production looks now, with the thumb too. A thumb | |
| 459 | missing on a commit pictured before it existed falls back to the phone shot | |
| 460 | that slot used to load, so nothing goes blank. Pictures are served at | |
| 461 | `/:owner/:name/looks/<env>/<commit>/{phone,desktop,thumb}.png` with the | |
| 459 | 462 | repository's own read access, cached by commit. |
| 460 | 463 | |
| 461 | 464 | The picture is what the repository's owner would see: headless Chromium is |
src/core/looks.ts +9 −4
| @@ -8,18 +8,23 @@ | ||
| 8 | 8 | import type { Database } from "bun:sqlite"; |
| 9 | 9 | import { id as newId } from "../kernel/id.ts"; |
| 10 | 10 | |
| 11 | /** The two widths a page is pictured at. */ | |
| 12 | export type LookKind = "phone" | "desktop"; | |
| 13 | export const LOOK_KINDS: readonly LookKind[] = ["phone", "desktop"]; | |
| 11 | /** The three widths a page is pictured at. */ | |
| 12 | export type LookKind = "phone" | "desktop" | "thumb"; | |
| 13 | export const LOOK_KINDS: readonly LookKind[] = ["phone", "desktop", "thumb"]; | |
| 14 | 14 | export const isLookKind = (value: string): value is LookKind => |
| 15 | 15 | (LOOK_KINDS as readonly string[]).includes(value); |
| 16 | 16 | |
| 17 | /** The two widths, as a person holds them: a phone, and a laptop's window. */ | |
| 17 | /** | |
| 18 | * The three widths, as a person holds them: a phone, a laptop's window, and | |
| 19 | * a thumbnail — taken at the size a slot actually shows, at desktop's own | |
| 20 | * aspect, rather than a full picture squeezed into a small box. | |
| 21 | */ | |
| 18 | 22 | export const LOOK_SIZES: Readonly< |
| 19 | 23 | Record<LookKind, { readonly width: number; readonly height: number }> |
| 20 | 24 | > = { |
| 21 | 25 | phone: { width: 390, height: 844 }, |
| 22 | 26 | desktop: { width: 1280, height: 800 }, |
| 27 | thumb: { width: 200, height: 125 }, | |
| 23 | 28 | }; |
| 24 | 29 | |
| 25 | 30 | export interface Look { |
src/core/schema.ts +21
| @@ -762,4 +762,25 @@ export const migrations: readonly string[] = [ | ||
| 762 | 762 | ` |
| 763 | 763 | ALTER TABLE run ADD COLUMN check_detail TEXT NOT NULL DEFAULT ''; |
| 764 | 764 | `, |
| 765 | ||
| 766 | // 26 — a third width: thumb, a picture taken small for a slot that only | |
| 767 | // ever showed one small, instead of a full phone or desktop picture | |
| 768 | // squeezed into it. SQLite's CHECK cannot be altered in place, so the | |
| 769 | // table is rebuilt; every row already there keeps its id and content. | |
| 770 | ` | |
| 771 | CREATE TABLE look_new ( | |
| 772 | id TEXT PRIMARY KEY, | |
| 773 | app_id TEXT NOT NULL REFERENCES app(id) ON DELETE CASCADE, | |
| 774 | env TEXT NOT NULL, | |
| 775 | commit_oid TEXT NOT NULL, | |
| 776 | kind TEXT NOT NULL CHECK (kind IN ('phone','desktop','thumb')), | |
| 777 | path TEXT NOT NULL, | |
| 778 | created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), | |
| 779 | error TEXT, | |
| 780 | UNIQUE (app_id, env, commit_oid, kind) | |
| 781 | ); | |
| 782 | INSERT INTO look_new SELECT * FROM look; | |
| 783 | DROP TABLE look; | |
| 784 | ALTER TABLE look_new RENAME TO look; | |
| 785 | `, | |
| 765 | 786 | ]; |
src/crew/runner.ts +6 −1
| @@ -135,7 +135,12 @@ async function reviewInputs( | ||
| 135 | 135 | record === undefined |
| 136 | 136 | ? [] |
| 137 | 137 | : looksForEnv(app.db, record.id, `pull-${pull.number}`) |
| 138 | .filter((l) => l.commitOid === pull.headOid && l.error === null) | |
| 138 | .filter( | |
| 139 | (l) => | |
| 140 | l.commitOid === pull.headOid && | |
| 141 | l.error === null && | |
| 142 | (l.kind === "phone" || l.kind === "desktop"), | |
| 143 | ) | |
| 139 | 144 | .sort( |
| 140 | 145 | (a, b) => |
| 141 | 146 | (a.kind === "phone" ? -1 : 1) - (b.kind === "phone" ? -1 : 1), |
src/web/pages/board.ts +5 −8
| @@ -30,7 +30,7 @@ import { boardJs, liveRegionsJs } from "../client.ts"; | ||
| 30 | 30 | import { html, when, type Html } from "../html.ts"; |
| 31 | 31 | import { page } from "../layout.ts"; |
| 32 | 32 | import { avatar, icon, time } from "../ui.ts"; |
| 33 | import { lookUrl } from "./looks.ts"; | |
| 33 | import { lookUrl, thumbLook } from "./looks.ts"; | |
| 34 | 34 | |
| 35 | 35 | // ---- words ---- |
| 36 | 36 | |
| @@ -273,15 +273,12 @@ function landed(ctx: Ctx, scope: Scope): Html { | ||
| 273 | 273 | const pictureOf = (repo: Repo): string | undefined => { |
| 274 | 274 | const record = scope.records.get(repo.id); |
| 275 | 275 | if (record === undefined) return undefined; |
| 276 | const look = looks.find( | |
| 277 | (l) => | |
| 278 | l.appId === record.id && | |
| 279 | l.env === "production" && | |
| 280 | l.kind === "phone" && | |
| 281 | l.error === null, | |
| 276 | const look = thumbLook( | |
| 277 | looks.filter((l) => l.appId === record.id && l.env === "production"), | |
| 282 | 278 | ); |
| 283 | 279 | return ( |
| 284 | look && lookUrl(repoBase(repo), "production", look.commitOid, "phone") | |
| 280 | look && | |
| 281 | lookUrl(repoBase(repo), "production", look.commitOid, look.kind) | |
| 285 | 282 | ); |
| 286 | 283 | }; |
| 287 | 284 | const rows = pulls.map((pull: Pull) => { |
src/web/pages/looks.ts +20 −9
| @@ -40,6 +40,13 @@ export const lookUrl = ( | ||
| 40 | 40 | const pictured = (looks: readonly Look[], kind: LookKind): Look | undefined => |
| 41 | 41 | looks.find((l) => l.kind === kind && l.error === null); |
| 42 | 42 | |
| 43 | /** | |
| 44 | * The picture for a small slot: the dedicated thumb, or — for a commit | |
| 45 | * taken before it existed — the phone shot a thumbnail loaded before. | |
| 46 | */ | |
| 47 | export const thumbLook = (looks: readonly Look[]): Look | undefined => | |
| 48 | pictured(looks, "thumb") ?? pictured(looks, "phone"); | |
| 49 | ||
| 43 | 50 | // GET /:owner/:name/looks/:env/:commit/:kind.png |
| 44 | 51 | export function lookImage(ctx: Ctx): Response { |
| 45 | 52 | const repo = loadRepo(ctx); |
| @@ -136,29 +143,33 @@ export function lookCard( | ||
| 136 | 143 | ); |
| 137 | 144 | } |
| 138 | 145 | |
| 139 | /** How production looks now, small, for the app's studio head. */ | |
| 146 | /** | |
| 147 | * How production looks now, small, for the app's studio head — the | |
| 148 | * dedicated thumb, or a commit from before it existed still shows what it | |
| 149 | * always showed. It is the first picture on the app's own page, so it is | |
| 150 | * not lazy: a person opening the app wants to see it at once. | |
| 151 | */ | |
| 140 | 152 | export function lookNow(ctx: Ctx, repo: Repo, record: AppRecord): Html { |
| 141 | const look = pictured( | |
| 142 | looksForEnv(ctx.app.db, record.id, "production"), | |
| 143 | "desktop", | |
| 144 | ); | |
| 153 | const looks = looksForEnv(ctx.app.db, record.id, "production"); | |
| 154 | const look = pictured(looks, "thumb") ?? pictured(looks, "desktop"); | |
| 145 | 155 | if (look === undefined) return html``; |
| 146 | 156 | const base = repoBase(repo); |
| 157 | const size = LOOK_SIZES[look.kind]; | |
| 147 | 158 | return html`<a |
| 148 | 159 | class="look-now" |
| 149 | 160 | href="${base}/app/production/open" |
| 150 | 161 | title="How it looks now, at ${look.commitOid.slice(0, 7)}" |
| 151 | 162 | > |
| 152 | 163 | <img |
| 153 | src="${lookUrl(base, "production", look.commitOid, "desktop")}" | |
| 154 | width="${LOOK_SIZES.desktop.width}" | |
| 155 | height="${LOOK_SIZES.desktop.height}" | |
| 164 | src="${lookUrl(base, "production", look.commitOid, look.kind)}" | |
| 165 | width="${size.width}" | |
| 166 | height="${size.height}" | |
| 156 | 167 | alt="${repo.name} as it looks now" |
| 157 | 168 | /> |
| 158 | 169 | </a>`; |
| 159 | 170 | } |
| 160 | 171 | |
| 161 | /** A phone picture as a thumbnail that opens the pull. */ | |
| 172 | /** A small picture as a thumbnail that opens the pull. */ | |
| 162 | 173 | export function lookThumb(href: string, src: string): Html { |
| 163 | 174 | return html`<a class="look-thumb" href="${href}" |
| 164 | 175 | ><img src="${src}" width="30" height="40" alt="" loading="lazy" |
src/web/studio/asks.ts +4 −8
| @@ -32,7 +32,7 @@ import type { Ctx } from "../../http/context.ts"; | ||
| 32 | 32 | import { repoBase } from "../chrome.ts"; |
| 33 | 33 | import { html, when, type Html } from "../html.ts"; |
| 34 | 34 | import { btn, card, icon, row } from "../ui.ts"; |
| 35 | import { lookThumb, lookUrl } from "../pages/looks.ts"; | |
| 35 | import { lookThumb, lookUrl, thumbLook } from "../pages/looks.ts"; | |
| 36 | 36 | import { leanNote, leanVariant } from "./leans.ts"; |
| 37 | 37 | |
| 38 | 38 | /** The apps a person can see, as repositories, with their records — the |
| @@ -203,14 +203,10 @@ export function decisionsFor(ctx: Ctx, allRepos: readonly Repo[]): Decision[] { | ||
| 203 | 203 | const record = records.get(repo.id); |
| 204 | 204 | if (record === undefined) return undefined; |
| 205 | 205 | const env = previewEnv(pull.number); |
| 206 | const look = looks.find( | |
| 207 | (l) => | |
| 208 | l.appId === record.id && | |
| 209 | l.env === env && | |
| 210 | l.kind === "phone" && | |
| 211 | l.error === null, | |
| 206 | const look = thumbLook( | |
| 207 | looks.filter((l) => l.appId === record.id && l.env === env), | |
| 212 | 208 | ); |
| 213 | return look && lookUrl(repoBase(repo), env, look.commitOid, "phone"); | |
| 209 | return look && lookUrl(repoBase(repo), env, look.commitOid, look.kind); | |
| 214 | 210 | }; |
| 215 | 211 | return candidates.map((c): Decision => { |
| 216 | 212 | if (c.kind === "stopped" && c.pull) { |
test/board.test.ts +15
| @@ -13,7 +13,9 @@ import { addToCrew, createAgent } from "../src/actions/crew.ts"; | ||
| 13 | 13 | import { openIssue } from "../src/actions/issues.ts"; |
| 14 | 14 | import { mergePull, openPull } from "../src/actions/pulls.ts"; |
| 15 | 15 | import { commitToBranch, createRepo } from "../src/actions/repos.ts"; |
| 16 | import { getAppByRepo } from "../src/core/apps.ts"; | |
| 16 | 17 | import { recordDecision } from "../src/core/decisions.ts"; |
| 18 | import { recordLook } from "../src/core/looks.ts"; | |
| 17 | 19 | import { getPull } from "../src/core/pulls.ts"; |
| 18 | 20 | import { updateRepoSettings, type Repo } from "../src/core/repos.ts"; |
| 19 | 21 | import { claimRun, createRun } from "../src/core/runs.ts"; |
| @@ -186,6 +188,16 @@ describe("the board", () => { | ||
| 186 | 188 | const merged = await mergePull(app, alice, notes, opened.value, "squash"); |
| 187 | 189 | if (!merged.ok) throw new Error(JSON.stringify(merged.error)); |
| 188 | 190 | const pull = getPull(app.db, notes.id, opened.value.number)!; |
| 191 | // A production thumb of the app that just landed: the board's landed | |
| 192 | // row leads with it, not with the full phone shot. | |
| 193 | const notesApp = getAppByRepo(app.db, notes.id)!; | |
| 194 | recordLook(app.db, { | |
| 195 | appId: notesApp.id, | |
| 196 | env: "production", | |
| 197 | commitOid: pull.headOid, | |
| 198 | kind: "thumb", | |
| 199 | path: "looks/does-not-matter.png", | |
| 200 | }); | |
| 189 | 201 | recordDecision(app.db, { |
| 190 | 202 | kind: "land", |
| 191 | 203 | repoId: notes.id, |
| @@ -230,6 +242,9 @@ describe("the board", () => { | ||
| 230 | 242 | expect(landed).toContain("Faster notes"); |
| 231 | 243 | expect(landed).toContain("notes · scout"); |
| 232 | 244 | expect(landed).toContain(`/alice/notes/pulls/${pull.number}`); |
| 245 | expect(landed).toContain( | |
| 246 | `src="/alice/notes/looks/production/${pull.headOid}/thumb.png"`, | |
| 247 | ); | |
| 233 | 248 | // Decided: kind in words, the answer, how sure, the pick's issue. |
| 234 | 249 | const decided = html.slice( |
| 235 | 250 | html.indexOf('id="decided"'), |
test/looks.test.ts +53 −21
| @@ -1,8 +1,8 @@ | ||
| 1 | 1 | /** |
| 2 | * The look (docs/APPS.md): a green deploy is pictured at phone and desktop | |
| 3 | * width, once per head, as the owner would see it; the pictures are served | |
| 4 | * with the repository's own read access and shown on the pull, on the | |
| 5 | * studio's ask row and in the app's head; a picture that could not be | |
| 2 | * The look (docs/APPS.md): a green deploy is pictured at phone, desktop | |
| 3 | * and thumb width, once per head, as the owner would see it; the pictures | |
| 4 | * are served with the repository's own read access and shown on the pull, | |
| 5 | * on the studio's ask row and in the app's head; a picture that could not be | |
| 6 | 6 | * taken is recorded as such and the page says so; a closed pull's pictures |
| 7 | 7 | * go with its preview. A stand-in capturer drives all of that; a real |
| 8 | 8 | * Chromium, when this machine has one, proves the browser end. |
| @@ -121,13 +121,13 @@ describe("the look", () => { | ||
| 121 | 121 | : undefined; |
| 122 | 122 | }); |
| 123 | 123 | |
| 124 | /** Both looks of a commit, once they are recorded — pictured or not. */ | |
| 124 | /** All three looks of a commit, once they are recorded — pictured or not. */ | |
| 125 | 125 | const pictured = (env: string, commitOid: string): Promise<Look[]> => |
| 126 | 126 | until(() => { |
| 127 | 127 | const rows = looksForEnv(app.db, record.id, env).filter( |
| 128 | 128 | (l) => l.commitOid === commitOid, |
| 129 | 129 | ); |
| 130 | return rows.length === 2 ? rows : undefined; | |
| 130 | return rows.length === 3 ? rows : undefined; | |
| 131 | 131 | }, 30_000); |
| 132 | 132 | |
| 133 | 133 | beforeAll(async () => { |
| @@ -183,11 +183,15 @@ describe("the look", () => { | ||
| 183 | 183 | let first = ""; |
| 184 | 184 | let second = ""; |
| 185 | 185 | |
| 186 | test("a green deploy is pictured at both widths, as the owner, once per head; a newer head replaces", async () => { | |
| 186 | test("a green deploy is pictured at all three widths, as the owner, once per head; a newer head replaces", async () => { | |
| 187 | 187 | const production = await settled((r) => r.env === "production"); |
| 188 | 188 | expect(production.state).toBe("ok"); |
| 189 | 189 | const rows = await pictured("production", production.commitOid); |
| 190 | expect(rows.map((l) => l.kind).sort()).toEqual(["desktop", "phone"]); | |
| 190 | expect(rows.map((l) => l.kind).sort()).toEqual([ | |
| 191 | "desktop", | |
| 192 | "phone", | |
| 193 | "thumb", | |
| 194 | ]); | |
| 191 | 195 | for (const look of rows) { |
| 192 | 196 | expect(look.error).toBeNull(); |
| 193 | 197 | expect(look.path).toBe( |
| @@ -197,7 +201,7 @@ describe("the look", () => { | ||
| 197 | 201 | } |
| 198 | 202 | // The browser was pointed at the grant door on the forge's own port, |
| 199 | 203 | // and the grant names the owner as an admin. |
| 200 | expect(calls).toHaveLength(2); | |
| 204 | expect(calls).toHaveLength(3); | |
| 201 | 205 | // Taken to one side and moved in whole: nothing half-written lives |
| 202 | 206 | // under the environment, and nothing is left behind on the side. |
| 203 | 207 | for (const call of calls) |
| @@ -206,6 +210,7 @@ describe("the look", () => { | ||
| 206 | 210 | const phone = calls.find((c) => c.width === 390)!; |
| 207 | 211 | expect(phone.height).toBe(844); |
| 208 | 212 | expect(calls.find((c) => c.width === 1280)?.height).toBe(800); |
| 213 | expect(calls.find((c) => c.width === 200)?.height).toBe(125); | |
| 209 | 214 | const url = new URL(phone.url); |
| 210 | 215 | expect(url.host).toBe(`hello.${DOMAIN}:${servers.port}`); |
| 211 | 216 | expect(url.pathname).toBe("/__forge/grant"); |
| @@ -234,9 +239,9 @@ describe("the look", () => { | ||
| 234 | 239 | pull = opened.value; |
| 235 | 240 | const env = previewEnv(pull.number); |
| 236 | 241 | expect((await settled((r) => r.env === env)).state).toBe("ok"); |
| 237 | expect(await pictured(env, first)).toHaveLength(2); | |
| 238 | expect(calls).toHaveLength(4); | |
| 239 | expect(new URL(calls[2]!.url).hostname).toBe( | |
| 242 | expect(await pictured(env, first)).toHaveLength(3); | |
| 243 | expect(calls).toHaveLength(6); | |
| 244 | expect(new URL(calls[3]!.url).hostname).toBe( | |
| 240 | 245 | `hello--${pull.number}.${DOMAIN}`, |
| 241 | 246 | ); |
| 242 | 247 | |
| @@ -246,8 +251,8 @@ describe("the look", () => { | ||
| 246 | 251 | (await settled((r) => r.env === env && r.reason === "redeploy")).state, |
| 247 | 252 | ).toBe("ok"); |
| 248 | 253 | await looks.idle(); |
| 249 | expect(calls).toHaveLength(4); | |
| 250 | expect(looksForEnv(app.db, record.id, env)).toHaveLength(2); | |
| 254 | expect(calls).toHaveLength(6); | |
| 255 | expect(looksForEnv(app.db, record.id, env)).toHaveLength(3); | |
| 251 | 256 | |
| 252 | 257 | // A newer head is pictured, and the older head's pictures go. |
| 253 | 258 | const again = await commitToBranch(app, alice, repo, { |
| @@ -260,8 +265,8 @@ describe("the look", () => { | ||
| 260 | 265 | expect( |
| 261 | 266 | (await settled((r) => r.env === env && r.commitOid === second)).state, |
| 262 | 267 | ).toBe("ok"); |
| 263 | expect(await pictured(env, second)).toHaveLength(2); | |
| 264 | expect(calls).toHaveLength(6); | |
| 268 | expect(await pictured(env, second)).toHaveLength(3); | |
| 269 | expect(calls).toHaveLength(9); | |
| 265 | 270 | await until(() => |
| 266 | 271 | looksForEnv(app.db, record.id, env).every((l) => l.commitOid === second) |
| 267 | 272 | ? true |
| @@ -452,7 +457,7 @@ describe("the look", () => { | ||
| 452 | 457 | looksForEnv(app.db, record.id, env).filter( |
| 453 | 458 | (l) => l.commitOid === commitOid && l.error === null, |
| 454 | 459 | ), |
| 455 | ).toHaveLength(2); | |
| 460 | ).toHaveLength(3); | |
| 456 | 461 | |
| 457 | 462 | expect( |
| 458 | 463 | (await setIssueStateThrough(app, alice, repo, opened.value, "closed")) |
| @@ -499,7 +504,7 @@ describe("the look", () => { | ||
| 499 | 504 | ).toBe(false); |
| 500 | 505 | }); |
| 501 | 506 | |
| 502 | test("the studio's look ask leads with the phone picture, and the app's head shows production", async () => { | |
| 507 | test("the studio's look ask leads with the thumb picture, and the app's head shows production", async () => { | |
| 503 | 508 | const scout = await createAgent(app, alice, { username: "scout" }); |
| 504 | 509 | if (!scout.ok) throw new Error(String(scout.error)); |
| 505 | 510 | expect(addToCrew(app, alice, repo, "scout").ok).toBe(true); |
| @@ -529,17 +534,44 @@ describe("the look", () => { | ||
| 529 | 534 | `<a class="look-thumb" href="/alice/hello/pulls/${ask.number}"`, |
| 530 | 535 | ); |
| 531 | 536 | expect(page).toContain( |
| 532 | `src="/alice/hello/looks/${env}/${branch.value.commitOid}/phone.png"`, | |
| 537 | `src="/alice/hello/looks/${env}/${branch.value.commitOid}/thumb.png"`, | |
| 533 | 538 | ); |
| 534 | 539 | const production = looksForEnv(app.db, record.id, "production").find( |
| 535 | (l) => l.kind === "desktop", | |
| 540 | (l) => l.kind === "thumb", | |
| 536 | 541 | )!; |
| 537 | 542 | expect(page).toContain(`class="look-now"`); |
| 538 | 543 | expect(page).toContain( |
| 539 | `src="/alice/hello/looks/production/${production.commitOid}/desktop.png"`, | |
| 544 | `src="/alice/hello/looks/production/${production.commitOid}/thumb.png"`, | |
| 540 | 545 | ); |
| 541 | 546 | const home = await (await web("/", studio)).text(); |
| 542 | 547 | expect(home).toContain(`class="look-thumb"`); |
| 548 | ||
| 549 | // A commit pictured before the thumb kind existed has only its phone | |
| 550 | // and desktop rows — this is exactly that, with the thumb dropped — | |
| 551 | // and every slot that would show a thumb falls back to what it showed | |
| 552 | // before, rather than going blank. | |
| 553 | app.db | |
| 554 | .query( | |
| 555 | `DELETE FROM look WHERE app_id = ?1 AND env = ?2 AND kind = 'thumb'`, | |
| 556 | ) | |
| 557 | .run(record.id, env); | |
| 558 | app.db | |
| 559 | .query( | |
| 560 | `DELETE FROM look WHERE app_id = ?1 AND env = 'production' AND kind = 'thumb'`, | |
| 561 | ) | |
| 562 | .run(record.id); | |
| 563 | const fallback = await (await web("/alice/hello", studio)).text(); | |
| 564 | expect(fallback).toContain( | |
| 565 | `src="/alice/hello/looks/${env}/${branch.value.commitOid}/phone.png"`, | |
| 566 | ); | |
| 567 | const productionDesktop = looksForEnv( | |
| 568 | app.db, | |
| 569 | record.id, | |
| 570 | "production", | |
| 571 | ).find((l) => l.kind === "desktop")!; | |
| 572 | expect(fallback).toContain( | |
| 573 | `src="/alice/hello/looks/production/${productionDesktop.commitOid}/desktop.png"`, | |
| 574 | ); | |
| 543 | 575 | }, 120_000); |
| 544 | 576 | |
| 545 | 577 | test("memory pressure is what the container truly holds, never its page cache", async () => { |
new above
offline