Public: anyone can read this; the crew can change it.

Dev's App page goes still the moment nothing is moving: a push lands and the page never says so #301

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/265-dev-s-app-page-goes-still-the-moment-not → main 1 commit fork

  • Merged. scout/265-dev-s-app-page-goes-still-the-moment-not was merged into main as 153477f.

Review

Sign in to review this change.
2 files changed +65 −9
src/web/pages/apps.ts +1 −8
@@ -36,7 +36,6 @@ import {
3636 listReposVisibleTo,
3737 type Repo,
3838 } from "../../core/repos.ts";
39 import { isTerminal } from "../../core/runs.ts";
4039 import type { User } from "../../core/users.ts";
4140 import { safeBack, type Ctx } from "../../http/context.ts";
4241 import { resolveAppHostname } from "../../apps/proxy.ts";
@@ -659,12 +658,6 @@ export function appPage(ctx: Ctx): Response {
659658 runs.map((run) => runRow(base, run, actors, issues)),
660659 { head: runsHead },
661660 );
662 // Something still moving is news the live pill announces; the page is
663 // never pulled out from under a reader.
664 const moving =
665 envs.some((env) => env.state === "deploying") ||
666 runs.some((run) => !isTerminal(run.state));
667
668661 const environmentsBlock = !serving
669662 ? card(
670663 html`<p class="muted small">
@@ -760,7 +753,7 @@ export function appPage(ctx: Ctx): Response {
760753 </div>
761754 <aside>${about} ${crewCard}</aside>
762755 </div>
763 ${when(moving, () => liveMount({ repo: base }))}`;
756 ${liveMount({ repo: base })}`;
764757
765758 return page({
766759 ctx,
test/web-apps.test.ts +64 −1
@@ -26,7 +26,7 @@ import { listAppEnvNames } from "../src/core/app_env.ts";
2626 import { getAppByRepo } from "../src/core/apps.ts";
2727 import { getIssue } from "../src/core/issues.ts";
2828 import { getRepo, type Repo } from "../src/core/repos.ts";
29 import { listRuns, type Run } from "../src/core/runs.ts";
29 import { isTerminal, listRuns, type Run } from "../src/core/runs.ts";
3030 import { getUserByName, type User } from "../src/core/users.ts";
3131 import { serve, type Servers } from "../src/http/server.ts";
3232 import { setLogLevel } from "../src/kernel/log.ts";
@@ -80,6 +80,35 @@ describe("apps, runs and the crew on the web", () => {
8080 return words(await response.text());
8181 };
8282
83 /** Read from an SSE body until `predicate(buffer)` holds or we time out. */
84 const readUntil = async (
85 body: ReadableStream<Uint8Array>,
86 predicate: (buf: string) => boolean,
87 timeoutMs = 10_000,
88 ): Promise<string> => {
89 const reader = body.getReader();
90 const decoder = new TextDecoder();
91 let buf = "";
92 const deadline = Date.now() + timeoutMs;
93 try {
94 while (Date.now() < deadline) {
95 const timeout = new Promise<{ done: true; value: undefined }>((r) =>
96 setTimeout(
97 () => r({ done: true, value: undefined }),
98 Math.max(0, deadline - Date.now()),
99 ),
100 );
101 const { done, value } = await Promise.race([reader.read(), timeout]);
102 if (done) break;
103 if (value) buf += decoder.decode(value, { stream: true });
104 if (predicate(buf)) break;
105 }
106 } finally {
107 reader.cancel().catch(() => {});
108 }
109 return buf;
110 };
111
83112 const signUp = async (username: string): Promise<string> => {
84113 const response = await fetch(`${base}/signup`, {
85114 method: "POST",
@@ -230,6 +259,40 @@ describe("apps, runs and the crew on the web", () => {
230259 expect(readers).not.toContain('href="/alice/hello/settings/app"');
231260 });
232261
262 test("the App page mounts the live stream on a quiet app, so a push after it was drawn still shows up", async () => {
263 // Quiet: every run on hello has already finished, nothing is deploying.
264 await runsOf(hello, () => true);
265 const deadline = Date.now() + 10_000;
266 while (Date.now() < deadline) {
267 const runs = listRuns(app.db, hello.id, { limit: 50 });
268 if (runs.every((run) => isTerminal(run.state))) break;
269 await Bun.sleep(50);
270 }
271 const html = await bodyOf(alice, "/alice/hello/app");
272 expect(html).toContain('id="live"');
273 expect(html).toContain('data-repo="/alice/hello"');
274
275 // A quiet page still opens the stream: the redeploy fired after the
276 // page was drawn reaches it, exactly what a still-page never saw.
277 const res = await fetch(`${base}/alice/hello/stream`, {
278 headers: { cookie: alice },
279 });
280 expect(res.status).toBe(200);
281 const redeployed = await web(
282 alice,
283 "/alice/hello/app/production/redeploy",
284 { form: {} },
285 );
286 expect(redeployed.status).toBe(303);
287 const buf = await readUntil(
288 res.body!,
289 (b) => b.includes("event: run.started") || b.includes("event: app.serving"),
290 );
291 expect(
292 buf.includes("event: run.started") || buf.includes("event: app.serving"),
293 ).toBe(true);
294 }, 20_000);
295
233296 test("the runs list, one run's log page, and the short door to it", async () => {
234297 const [run] = await runsOf(hello);
235298 expect(run).toBeDefined();

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs