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

Red after landing: 2bd5aeb broke test/pages-budget.test.ts #12039

Merged opened this in answer to

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

Merged

scout/forge:scout/12037-red-after-landing-2bd5aeb-broke-test-pag → main 7 commits fork 3 threads · 3 unresolved

  • Merged. scout/12037-red-after-landing-2bd5aeb-broke-test-pag was merged into main as e3e12d0.

Review

Sign in to review this change.
7 files changed +57 −17
src/actions/first_ask.ts +23 −5
@@ -9,7 +9,8 @@ import { readFileSync } from "node:fs";
99 import { join } from "node:path";
1010 import type { App } from "../app.ts";
1111 import { appsForRepos } from "../core/apps.ts";
12 import { isForgeSource } from "../core/forge_repo.ts";
12 import { isNamed } from "../core/forge_repo.ts";
13 import { forgeSource } from "../settings/forge_source.ts";
1314 import { nextStepsFor } from "../core/next-steps.ts";
1415 import { listReposWritableTo } from "../core/repos.ts";
1516 import { FORGE_CODE_NAME } from "../spawn/forge_code.ts";
@@ -102,10 +103,27 @@ function spawnedName(app: App): string | undefined {
102103 * that names no repository never lands on it. An ask that names it still does.
103104 */
104105 export function isForgeCopy(app: App, repo: Pick<Repo, "ownerName" | "name">): boolean {
105 return (
106 isForgeSource(app, repo) ||
107 (repo.name === FORGE_CODE_NAME && repo.ownerName.toLowerCase() === spawnedName(app))
108 );
106 return forgeCopyTest(app)(repo);
107 }
108
109 /** `isForgeCopy` for many repositories: the source setting is read once, not per repository. */
110 export function forgeCopyTest(app: App): (repo: Pick<Repo, "ownerName" | "name">) => boolean {
111 const own = forgeSource(app.db, app.config.forgeRepo);
112 const spawned = spawnedName(app);
113 return (repo) => isNamed(own, repo) || (repo.name === FORGE_CODE_NAME && repo.ownerName.toLowerCase() === spawned);
outdated

This re-implements isForgeSource from core/forge_repo.ts inline, so the two matches can drift. Share one definition of the match. No test covers forgeCopyTest directly, and the existing test only reaches it through isForgeCopy; add one that checks it agrees with isForgeCopy across several repositories.

114 }
115
116 /**
117 * Repositories apps first, the forge's own copy last, and whether nothing but the copy is there;
118 * the setting is read once.
119 */
120 export function appsFirst<R extends Pick<Repo, "ownerName" | "name">>(
121 app: App,
122 repos: readonly R[],
123 ): { ordered: R[]; onlyForgeCopy: boolean } {
124 const isCopy = forgeCopyTest(app);
125 const ordered = [...repos.filter((r) => !isCopy(r)), ...repos.filter(isCopy)];
126 return { ordered, onlyForgeCopy: repos.length > 0 && ordered.every(isCopy) };
109127 }
110128
111129 /**
src/core/forge_repo.ts +5 −1
@@ -19,7 +19,11 @@ export function publicForgeRepo(app: App): Repo | undefined {
1919 * forge's own lookups do.
2020 */
2121 export function isForgeSource(app: App, repo: Pick<Repo, "ownerName" | "name">): boolean {
22 const own = forgeSource(app.db, app.config.forgeRepo);
22 return isNamed(forgeSource(app.db, app.config.forgeRepo), repo);
23 }
24
25 /** The one match of a repository against a configured `owner/name`, without case. */
26 export function isNamed(own: string | undefined, repo: Pick<Repo, "ownerName" | "name">): boolean {
2327 return own !== undefined && own.toLowerCase() === `${repo.ownerName}/${repo.name}`.toLowerCase();
2428 }
2529
src/web/studio/feed.ts +2 −3
@@ -35,7 +35,7 @@ import { avatar, btn, card, disclosure, empty, hintAttr, nameMark, row, stateBad
3535 import { heldPostsJs } from "./held_posts.ts";
3636 import { stepLink } from "./step_link.ts";
3737 import { writableAppsOf } from "./asks.ts";
38 import { isForgeCopy } from "../../actions/first_ask.ts";
38 import { appsFirst } from "../../actions/first_ask.ts";
3939
4040 /** Events that can change what the feed says. */
4141 export const FEED_LIVE =
@@ -249,8 +249,7 @@ export function composer(ctx: Ctx, user: User): Html {
249249 })}`;
250250 // The forge's own copy is never the default target: a person with no other app asks for a new one,
251251 // and a person with apps sees theirs first.
252 const onlyForgeCopy = repos.length > 0 && repos.every((r) => isForgeCopy(ctx.app, r));
253 const ordered = [...repos.filter((r) => !isForgeCopy(ctx.app, r)), ...repos.filter((r) => isForgeCopy(ctx.app, r))];
252 const { ordered, onlyForgeCopy } = appsFirst(ctx.app, repos);
254253 const shared = sharedDraft(ctx.url.searchParams);
255254 const form = html`<form id="composer" class="stack" method="post" action="/tell">
outdated

This repeats the apps-first, forge-copy-last ordering that living.ts now has as orderedRepos. Export one ordering helper from first_ask.ts and use it in both places.

256255 <input type="hidden" name="${COMPOSER_FIELDS.back}" value="/feed" />
src/web/studio/living.ts +4 −3
@@ -21,7 +21,7 @@ import { nowHref } from "../pages/now.ts";
2121 import type { Repo } from "../../core/repos.ts";
2222 import { writableAppsOf } from "./asks.ts";
2323 import { FORGE_TARGET, NEW_APP_TARGET } from "./feed.ts";
24 import { isForgeCopy } from "../../actions/first_ask.ts";
24 import { appsFirst } from "../../actions/first_ask.ts";
2525 import { COMPOSER_FIELDS, TELL_MAX, sharedDraft, shareToken } from "../share_target.ts";
2626 import { repoBase } from "../chrome.ts";
2727 import { crewFace, type FaceState } from "../pixel.ts";
@@ -111,15 +111,16 @@ export function tellBox(ctx: Ctx, user: User): Html {
111111 const { repos } = writableAppsOf(ctx, user);
112112 const slug = (r: { ownerName: string; name: string }) => `${r.ownerName}/${r.name}`;
113113 const shared = sharedDraft(ctx.url.searchParams);
114 const { ordered, onlyForgeCopy } = appsFirst(ctx.app, repos);
114115 return html`<form id="tell-box" class="bar tell-box" method="post" action="/tell">
115116 ${shared === "" ? "" : html`<input type="hidden" name="${COMPOSER_FIELDS.once}" value="${shareToken(user.id, shared)}" />`}
116117 <input class="input" name="${COMPOSER_FIELDS.text}" maxlength="${String(TELL_MAX)}" required placeholder="Tell the crew…" aria-label="Tell the crew" data-draft="${user.id}" value="${shared.replace(/\s+/g, " ")}" />
117118 ${repos.length === 0
118119 ? html`<input type="hidden" name="${COMPOSER_FIELDS.app}" value="${FORGE_TARGET}" />`
119 : repos.every((r) => isForgeCopy(ctx.app, r))
120 : onlyForgeCopy
120121 ? html`<input type="hidden" name="${COMPOSER_FIELDS.app}" value="${NEW_APP_TARGET}" />`
121122 : html`<select name="${COMPOSER_FIELDS.app}" class="input" aria-label="Send to">
122 ${[...repos.filter((r) => !isForgeCopy(ctx.app, r)), ...repos.filter((r) => isForgeCopy(ctx.app, r))].map((r) => html`<option value="${slug(r)}">${slug(r)}</option>`)}
123 ${ordered.map((r) => html`<option value="${slug(r)}">${slug(r)}</option>`)}
123124 <option value="${FORGE_TARGET}">the forge</option>
124125 </select>`}
125126 ${btn("Tell", { variant: "primary" })}
test/first-ask-forge-copy.test.ts +18 −1
@@ -4,7 +4,7 @@
44 * without a repo, and one that names the copy still files there.
55 */
66 import { afterAll, beforeAll, expect, test } from "bun:test";
7 import { asksMakeANewApp, isForgeCopy } from "../src/actions/first_ask.ts";
7 import { appsFirst, asksMakeANewApp, forgeCopyTest, isForgeCopy } from "../src/actions/first_ask.ts";
88 import { createRepo } from "../src/actions/repos.ts";
99 import { listIssues } from "../src/core/issues.ts";
1010 import { deleteRepo, getRepo, listReposOwnedBy } from "../src/core/repos.ts";
@@ -96,3 +96,20 @@ test("naming the forge copy still files the ask on it", async () => {
9696 expect(res.status).toBe(303);
9797 expect(listIssues(forge.child.db, copy.id, {}).length).toBe(1);
9898 });
99
100 test("forgeCopyTest agrees with isForgeCopy on several repositories, and appsFirst orders by it", () => {
101 const repos = [
102 { ownerName: name, name: "forge" },
103 { ownerName: name.toUpperCase(), name: "FORGE" },
104 { ownerName: name, name: "bakery" },
105 { ownerName: "someone", name: "forge" },
106 ];
107 const test = forgeCopyTest(forge.child);
108 for (const r of repos) expect(test(r)).toBe(isForgeCopy(forge.child, r));
109 expect(repos.map(test)).toEqual([true, true, false, false]);
110 const { ordered, onlyForgeCopy } = appsFirst(forge.child, repos);
111 expect(ordered.map((r) => r.name)).toEqual(["bakery", "forge", "forge", "FORGE"]);
112 expect(onlyForgeCopy).toBe(false);
113 expect(appsFirst(forge.child, repos.slice(0, 2)).onlyForgeCopy).toBe(true);
114 expect(appsFirst(forge.child, []).onlyForgeCopy).toBe(false);
115 });
test/home-living.test.ts +3 −2
@@ -190,9 +190,10 @@ describe("home", () => {
190190
191191 test("a warm home costs a bounded number of statements, the two scans read once per write", async () => {
192192 await home(aliceCookie);
193 // Calm pass (#10298): home now also reads what waits on the owner, the last visit (a write) and the digest since: 3 more statements, bound raised 40 to 46.
193 // Calm pass (#10298): home also reads what waits on the owner, the last visit (a write) and the
194 // digest since: bound 40 to 46. The tell box reads the forge-copy setting once (#12037): 47.
194195 const warm = await requestStatements(app.db, `${servers.url}/`, { headers: { cookie: aliceCookie } });
195 expectWork(warm, { max: 46 });
196 expectWork(warm, { max: 47 });
196197 const scans = await statements(app.db, async () => void (await get("/live/moving", aliceCookie).then((r) => r.text())));
197198 const again = await statements(app.db, async () => void (await get("/live/moving", aliceCookie).then((r) => r.text())));
198199 expect(again).toBeLessThanOrEqual(scans);
test/pages-budget.test.ts +2 −2
@@ -38,8 +38,8 @@ const DEFAULT = { statements: 60, git: 8 };
3838 const HEAVIER: Record<string, { statements?: number; git?: number }> = {
3939 "/:owner/:name/issues/:number": { statements: 70 },
4040 "/:owner/:name/next": { statements: 64 },
41 "/:owner/:name/pulls/:number": { statements: 112 }, // +1: the paused sentence, one read of a queued paused run on the pull (#12010); +1: the header Talk count (#10327); +1: the landing line (#10860); +1: whether a reviewer is seated, read only when Require review is off and the pull would otherwise wait on one; the page loads no crew besides (#11666)
42 "/board": { statements: 71 }, // +1: the vitals read the database's growth (#12025); +6: the money line (#510) — one settings read, then the month's and the 30 days' meter, storage and ledger reads; constant in the number of apps
41 "/:owner/:name/pulls/:number": { statements: 113 }, // +1: whether a repository is the forge's own copy, one read of the source setting for the tell box (#12037); +1: the paused sentence, one read of a queued paused run on the pull (#12010); +1: the header Talk count (#10327); +1: the landing line (#10860); +1: whether a reviewer is seated, read only when Require review is off and the pull would otherwise wait on one; the page loads no crew besides (#11666)
42 "/board": { statements: 72 }, // +1: the forge-copy read for the tell box (#12037); +1: the vitals read the database's growth (#12025); +6: the money line (#510) — one settings read, then the month's and the 30 days' meter, storage and ledger reads; constant in the number of apps
4343 "/settings/capacity": { statements: 71 },
4444 };
4545

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs