Public: anyone can read this; the crew can change it.
The studio's three lists disagree: one header, and two dead filters #10030
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/175-the-studio-s-three-lists-disagree-one-he → main 5 commits fork
2 files
changed +34
−4
src/web/studio/lists.ts +16 −3
| @@ -160,6 +160,15 @@ function wordFilterBar( | ||
| 160 | 160 | * look — so those two are not offered as filters there. */ |
| 161 | 161 | const PLAIN_FILTERS: readonly WordFilter[] = ["working", "shipped", "stopped"]; |
| 162 | 162 | |
| 163 | /** The words a pull or a run can carry, as a list links to them: every word | |
| 164 | * but *told*. `pullWord` never hands that word back, and `runWord` reaches a | |
| 165 | * pull's word only through `pullWord` — so on the changes and runs lists a | |
| 166 | * *Not started* filter could only ever land on an empty page (#175). The | |
| 167 | * tells list, built on `tellWord`, is the one that offers all five. */ | |
| 168 | const PULL_FILTERS: readonly WordFilter[] = WORD_FILTERS.filter( | |
| 169 | (w) => w !== "told", | |
| 170 | ); | |
| 171 | ||
| 163 | 172 | const LIST_PAGE = 25; |
| 164 | 173 | |
| 165 | 174 | /** |
| @@ -661,7 +670,7 @@ export async function changesList(ctx: Ctx): Promise<Response> { | ||
| 661 | 670 | f.word, |
| 662 | 671 | f.word === undefined && f.state === "open", |
| 663 | 672 | undefined, |
| 664 | record === undefined ? PLAIN_FILTERS : WORD_FILTERS, | |
| 673 | record === undefined ? PLAIN_FILTERS : PULL_FILTERS, | |
| 665 | 674 | )} |
| 666 | 675 | ${facetChips(listBase, f, repoLabels)} ${list} |
| 667 | 676 | ${pager( |
| @@ -790,7 +799,9 @@ const RUNS_KIND_WORDS: Record<RunsKind, string> = { | ||
| 790 | 799 | * one row per run in its studio word — who ran it, when it last moved, and |
| 791 | 800 | * the transcript as the row's own link. Agent runs, checks and deploys |
| 792 | 801 | * together, newest first, by default; `?kind=agent|deploy|check` narrows to |
| 793 | * one, from the three links under the list. `?word=` narrows any of them. | |
| 802 | * one, from the three links under the list. `?word=` narrows any of them, | |
| 803 | * from the changes list's words (`PULL_FILTERS`). The header is the full one | |
| 804 | * the tells and changes lists carry, description included. | |
| 794 | 805 | */ |
| 795 | 806 | export async function runsListPage(ctx: Ctx, repo: Repo): Promise<Response> { |
| 796 | 807 | const db = ctx.app.db; |
| @@ -875,7 +886,7 @@ export async function runsListPage(ctx: Ctx, repo: Repo): Promise<Response> { | ||
| 875 | 886 | ); |
| 876 | 887 | const last = fetched[fetched.length - 1]; |
| 877 | 888 | |
| 878 | const body = html`${repoHeader(ctx, repo, "runs", { compact: true })} | |
| 889 | const body = html`${repoHeader(ctx, repo, "runs")} | |
| 879 | 890 | <div class="mt"> |
| 880 | 891 | <nav aria-label="Run state"> |
| 881 | 892 | ${seg( |
| @@ -890,6 +901,8 @@ export async function runsListPage(ctx: Ctx, repo: Repo): Promise<Response> { | ||
| 890 | 901 | (word) => runsHref(base, kind, word, undefined, state), |
| 891 | 902 | filter, |
| 892 | 903 | filter === undefined, |
| 904 | undefined, | |
| 905 | PULL_FILTERS, | |
| 893 | 906 | )} |
| 894 | 907 | ${list} |
| 895 | 908 | ${pager( |
test/studio-changes.test.ts +18 −1
| @@ -170,6 +170,23 @@ describe("the studio, continued", () => { | ||
| 170 | 170 | expect(studioCookie).toContain("<h2>Depends on</h2>"); |
| 171 | 171 | }); |
| 172 | 172 | |
| 173 | test("the three studio lists carry one header, and only the issue list offers Not started (#175)", async () => { | |
| 174 | const issues = await (await get("/alice/notes/issues")).text(); | |
| 175 | const pulls = await (await get("/alice/notes/pulls")).text(); | |
| 176 | const runs = await (await get("/alice/notes/runs")).text(); | |
| 177 | // The same header on all three: the description, not the compact one. | |
| 178 | for (const page of [issues, pulls, runs]) | |
| 179 | expect(page).toContain('<p class="muted small">a place for notes</p>'); | |
| 180 | // No pull and no run is ever told, so only the issue list links to it. | |
| 181 | expect(issues).toContain('href="/alice/notes/issues?word=told"'); | |
| 182 | expect(pulls).not.toContain("word=told"); | |
| 183 | expect(runs).not.toContain("word=told"); | |
| 184 | for (const page of [pulls, runs]) expect(page).not.toContain("Not started"); | |
| 185 | // A URL somebody holds still answers, never an error. | |
| 186 | expect((await get("/alice/notes/pulls?word=told")).status).toBe(200); | |
| 187 | expect((await get("/alice/notes/runs?word=told")).status).toBe(200); | |
| 188 | }); | |
| 189 | ||
| 173 | 190 | test("everything told is the one issue list, in the studio's own words; the word filter narrows it, and ?mode=dev changes nothing", async () => { |
| 174 | 191 | const told = openIssue(app, alice, repo, { |
| 175 | 192 | title: "List test: not picked up yet", |
| @@ -299,7 +316,7 @@ describe("the studio, continued", () => { | ||
| 299 | 316 | expect(page).toContain('href="/alice/notes/runs?kind=check"'); |
| 300 | 317 | const deploys = await (await get("/alice/notes/runs?kind=deploy")).text(); |
| 301 | 318 | expect(deploys).not.toContain(`href="/alice/notes/runs/${run.id}"`); |
| 302 | expect(deploys).toContain('href="/alice/notes/runs?kind=deploy&word=told"'); | |
| 319 | expect(deploys).toContain('href="/alice/notes/runs?kind=deploy&word=stopped"'); | |
| 303 | 320 | }); |
| 304 | 321 | |
| 305 | 322 | test("the runs list shows agent runs, checks and deploys together, and each kind link narrows it", async () => { |