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

"Now" says nothing while the app is plainly coming alive #168

Merged opened this

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

Merged

scout/forge:scout/163-now-says-nothing-while-the-app-is-plainl → main 1 commit fork

  • Merged. scout/163-now-says-nothing-while-the-app-is-plainl was merged into main as af34181.

Review

Sign in to review this change.
3 files changed +156 −3
src/web/studio/app.ts +3 −1
@@ -335,7 +335,9 @@ export async function studioApp(
335335 nowRows.length === 0
336336 ? card(
337337 html`<p class="muted small">
338 Nothing yet. Tell the crew something.
338 ${deployTailing
339 ? "Production is coming up — the log above has it."
340 : "Nothing yet. Tell the crew something."}
339341 </p>`,
340342 {
341343 head: html`${icon("activity")}
src/web/studio/previews.ts +3 −2
@@ -125,8 +125,9 @@ function previewsPanel(
125125 * its row — the app's own first thirty seconds are not a blank page. The
126126 * agent's run wins the one stream this page opens when both are going: it
127127 * is the crew's own answer to something asked, where a deploy is often
128 * background (a redeploy after an unrelated push), and it is already what
129 * "Now" shows.
128 * background (a redeploy after an unrelated push). When "Now" would
129 * otherwise sit empty, its own empty state points back up at this tail
130 * rather than opening a second stream of the same lines.
130131 */
131132 function deployTail(
132133 ctx: Ctx,
test/studio.test.ts +150
@@ -3286,6 +3286,12 @@ describe("the app's page tails the deploy that is bringing it alive", () => {
32863286 // No script, no problem: the page keeps refreshing while the deploy
32873287 // goes — the actual meta tag, not just the script's own selector text.
32883288 expect(page).toMatch(/<meta\s+http-equiv="refresh"/);
3289 // Issue #163: the deploy is plainly going, in the head card above — "Now"
3290 // does not also claim nothing is happening, and it does not open a
3291 // second stream of the same lines to say so.
3292 expect(page).not.toContain("Nothing yet");
3293 expect(page).toContain("Production is coming up");
3294 expect(page.match(/id="app-run-tail"/g)?.length).toBe(1);
32893295
32903296 finishRun(app.db, deployRun.id, { state: "ok", summary: "deploy" });
32913297 });
@@ -3349,6 +3355,150 @@ describe("the app's page tails the deploy that is bringing it alive", () => {
33493355 });
33503356 });
33513357
3358 /**
3359 * Issue #163: "Now" is built from agent runs alone, so a fresh app with a
3360 * deploy going and nothing else read as if nothing were happening — right
3361 * beside the head card tailing that very deploy. "Now"'s empty state now
3362 * says otherwise when the head card is tailing a deploy, and keeps its
3363 * ordinary wording when nothing at all is going.
3364 */
3365 describe(`"Now" while production is coming up and nothing else is`, () => {
3366 let dataDir: string;
3367 let app: App;
3368 let servers: Servers;
3369 let base: string;
3370 let runtime: AppsRuntime;
3371 let repo: Repo;
3372 let record: AppRecord;
3373 let owner: string;
3374
3375 interface PreviewSlot {
3376 readonly app: AppRecord;
3377 readonly env: EnvName;
3378 readonly chain: Promise<void>;
3379 readonly restarts: number[];
3380 deploying?: unknown;
3381 live?: unknown;
3382 lastError?: string;
3383 lastRunId?: string;
3384 }
3385 const putEnv = (
3386 env: EnvName,
3387 slot: Omit<PreviewSlot, "app" | "env" | "chain" | "restarts">,
3388 ): void => {
3389 (runtime as unknown as { envs: Map<string, PreviewSlot> }).envs.set(
3390 `${record.id}/${env}`,
3391 { app: record, env, chain: Promise.resolve(), restarts: [], ...slot },
3392 );
3393 };
3394
3395 const signIn = async (username: string): Promise<string> =>
3396 (
3397 (
3398 await fetch(`${base}/signin`, {
3399 method: "POST",
3400 body: new URLSearchParams({
3401 username,
3402 password: "a-good-passphrase",
3403 }),
3404 redirect: "manual",
3405 })
3406 ).headers.get("set-cookie") ?? ""
3407 ).split(";")[0]!;
3408
3409 const get = (path: string, cookie: string) =>
3410 fetch(`${base}${path}`, {
3411 headers: { cookie: `${cookie}; forge_mode=studio` },
3412 redirect: "manual",
3413 });
3414
3415 beforeAll(async () => {
3416 dataDir = await mkdtemp(join(tmpdir(), "forge-studio-now-empty-"));
3417 app = createApp({ dataDir, port: 0 });
3418 servers = await serve(app);
3419 base = servers.url;
3420 runtime = new AppsRuntime(app, {
3421 domain: "nowempty.test",
3422 scheme: "http",
3423 publicPort: servers.port,
3424 forgeUrl: servers.url,
3425 concurrency: 1,
3426 });
3427 const a = await createUser(app.db, {
3428 username: "alice",
3429 email: "[email protected]",
3430 password: "a-good-passphrase",
3431 adminIfFirst: true,
3432 });
3433 if (!a.ok) throw new Error(a.error);
3434 owner = await signIn("alice");
3435 const made = await createRepo(app, a.value, {
3436 name: "ember",
3437 visibility: "private",
3438 description: "a new app",
3439 });
3440 if (!made.ok) throw new Error(String(made.error));
3441 repo = made.value;
3442 const seeded = await commitToBranch(app, a.value, repo, {
3443 message: "Start",
3444 files: [{ path: "README.md", content: "ember\n" }],
3445 });
3446 if (!seeded.ok) throw new Error(JSON.stringify(seeded.error));
3447 const enabled = await enableApp(app, a.value, repo, { slug: "ember" });
3448 if (!enabled.ok) throw new Error(String(enabled.error));
3449 record = getAppByRepo(app.db, repo.id)!;
3450 });
3451
3452 afterAll(async () => {
3453 servers.stop();
3454 app.close();
3455 await rm(dataDir, { recursive: true, force: true });
3456 });
3457
3458 test("a running deploy and no agent run: \"Now\" does not say nothing is happening, and opens no second tail", async () => {
3459 const deployRun = createRun(app.db, {
3460 repoId: repo.id,
3461 kind: "deploy",
3462 env: "production",
3463 commitOid: "eae580a",
3464 reason: "boot",
3465 });
3466 claimRun(app.db, deployRun.id);
3467 appendRunLog(app.config.dataDir, deployRun.id, "deploy in progress");
3468 putEnv("production", {
3469 deploying: {
3470 run: { id: deployRun.id, commitOid: "eae580a" },
3471 abort: new AbortController(),
3472 },
3473 });
3474
3475 const page = await (await get("/alice/ember", owner)).text();
3476 expect(page).not.toContain("Nothing yet");
3477 expect(page).toContain("Production is coming up");
3478 // Exactly one tail on the page — the head card's, not a second one in
3479 // "Now".
3480 expect(page.match(/id="app-run-tail"/g)?.length).toBe(1);
3481
3482 finishRun(app.db, deployRun.id, { state: "ok", summary: "deploy" });
3483 });
3484
3485 test("with nothing going at all, \"Now\" reads exactly as it does today", async () => {
3486 putEnv("production", {
3487 live: {
3488 commitOid: "eae580a",
3489 handle: { port: 4502 },
3490 runId: "irrelevant",
3491 since: new Date().toISOString(),
3492 manifest: { tty: null },
3493 },
3494 });
3495
3496 const page = await (await get("/alice/ember", owner)).text();
3497 expect(page).toContain("Nothing yet. Tell the crew something.");
3498 expect(page).not.toContain('id="app-run-tail"');
3499 });
3500 });
3501
33523502 /**
33533503 * Issue #65: a public app's studio page is the app, not the owner's books.
33543504 * `canSeeOwnerFacts` (`src/core/repos.ts`) is the one predicate — the same

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs