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

The studio's lists answer at the studio's nouns: /tells and /changes #139

Merged opened this

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

Merged

scout/forge:scout/119-the-studio-s-lists-answer-at-the-studio → main 1 commit fork

  • Merged. scout/119-the-studio-s-lists-answer-at-the-studio was merged into main as 1cc73a7.

Review

Sign in to review this change.
18 files changed +255 −56
docs/STUDIO.md +14 −5
@@ -156,11 +156,20 @@ issue numbers, labels, milestones and branch names replaced by the same
156156 five words the rest of the studio speaks. Each is a page of rows, newest
157157 first: who has it (the assignee, the author, or who ran it), when it last
158158 moved, its state as `wordBadge` draws it (`src/web/studio/words.ts`), and
159 one link — the row itself — to the tell, the change or the transcript. The
160 app page's own footer (`the map · everything told · every change`, and
161 *Every run* above **Now**) and the repository header (`Told`, `Look`, in
162 place of dev's `Issues`, `Pulls`) both land here; the header's own tabs
163 keep dev's names, and dev's `Commits`/`Branches` stay dev's alone.
159 one link — the row itself — to the tell, the change or the transcript.
160 `/:owner/:name/tells` and `/:owner/:name/changes` are the studio's own
161 nouns for the first two lists — the same handler, the same authorization,
162 the same `?word=` filter and paging, not a second copy of the page; a
163 viewer the studio list does not apply to (dev mode, `?mode=dev`, or a
164 repository that is not an app) is sent on to `/issues` or `/pulls`, dev's
165 own address, rather than the two ever growing a second address of their
166 own. The app page's own footer (`the map · everything told · every
167 change`, and *Every run* above **Now**) and the repository header's tabs
168 (`Told`, `Look`, in place of dev's `Issues`, `Pulls`) link to `/tells` and
169 `/changes` in studio mode; a tell or a change keeps its own
170 `/issues/:number` / `/pulls/:number` address either way, and dev's
171 `Commits`/`Branches` stay dev's alone. `/runs` has no second noun — the
172 studio and dev share the one address there.
164173
165174 A row's word comes from `tellWord`, `pullWord` or `runWord` — the one place
166175 each is decided, never re-derived here. `?word=told|working|look|shipped|
src/http/server.ts +8
@@ -84,6 +84,7 @@ import {
8484 tellSubmit,
8585 } from "../web/studio/actions.ts";
8686 import { appStateFragment } from "../web/studio/previews.ts";
87 import { changesList, tellsList } from "../web/studio/lists.ts";
8788 import { DEFAULT_MODE, isMode, modeCookie, type Mode } from "../web/mode.ts";
8889 import { dependencySubmit } from "../web/pages/thread.ts";
8990 import {
@@ -529,6 +530,11 @@ export function buildRouter(): Router<Handler> {
529530 // Issues (web). The literal /new is registered before /:number so it can
530531 // never be captured as a number.
531532 router.add("GET", "/:owner/:name/issues", issuesList);
533 // The studio's own noun for the same list — everything told. A tell or a
534 // change keeps its `/issues/:number` / `/pulls/:number` address; only the
535 // list itself gains a second door (`tellsList`/`changesList` redirect to
536 // dev's own address when the studio's list does not apply).
537 router.add("GET", "/:owner/:name/tells", tellsList);
532538 router.add("GET", "/:owner/:name/issues/new", newIssuePage);
533539 router.add("POST", "/:owner/:name/issues/new", newIssueSubmit);
534540 router.add("GET", "/:owner/:name/issues/:number", issuePage);
@@ -562,6 +568,8 @@ export function buildRouter(): Router<Handler> {
562568
563569 // Pull requests. Same /new-before-/:number ordering discipline.
564570 router.add("GET", "/:owner/:name/pulls", pullsList);
571 // The studio's own noun for the same list — every change.
572 router.add("GET", "/:owner/:name/changes", changesList);
565573 router.add("GET", "/:owner/:name/pulls/new", newPullPage);
566574 router.add("POST", "/:owner/:name/pulls/new", newPullSubmit);
567575 router.add("GET", "/:owner/:name/pulls/:number", pullPage);
src/web/chrome.ts +18 −6
@@ -165,7 +165,7 @@ export function scopeHeader(
165165 // Studio names its own lists — told, not issues; dev's tabs are
166166 // unchanged in dev mode and on a repository that is not an app.
167167 label: studio ? "Told" : "Issues",
168 href: `${base}/issues`,
168 href: studio ? `${base}/tells` : `${base}/issues`,
169169 icon: studio ? "clock" : "issue",
170170 ...(facts.openIssues === null ? {} : { count: facts.openIssues }),
171171 current: active === "issues",
@@ -173,7 +173,7 @@ export function scopeHeader(
173173 },
174174 {
175175 label: studio ? "Look" : "Pulls",
176 href: `${base}/pulls`,
176 href: studio ? `${base}/changes` : `${base}/pulls`,
177177 icon: studio ? "eye" : "pull",
178178 ...(facts.openPulls === null ? {} : { count: facts.openPulls }),
179179 current: active === "pulls",
@@ -283,14 +283,26 @@ export function scopeHeader(
283283
284284 export { hostOf };
285285
286 /** This repo's own rows for the command palette. */
287 export function repoPalette(repo: Repo): PaletteItem[] {
286 /** This repo's own rows for the command palette — the studio's nouns for a
287 * studio-mode viewer of an app, so the palette never sends someone to a
288 * list under a name the address bar would not use. */
289 export function repoPalette(ctx: Ctx, repo: Repo): PaletteItem[] {
288290 const base = repoBase(repo);
289291 const name = `${repo.ownerName}/${repo.name}`;
292 const studio =
293 ctx.mode === "studio" && getAppByRepo(ctx.app.db, repo.id) !== undefined;
290294 return [
291295 { t: `${name} — Code`, h: base, c: "This repo" },
292 { t: `${name} — Issues`, h: `${base}/issues`, c: "This repo" },
293 { t: `${name} — Pull requests`, h: `${base}/pulls`, c: "This repo" },
296 {
297 t: `${name} — ${studio ? "Told" : "Issues"}`,
298 h: studio ? `${base}/tells` : `${base}/issues`,
299 c: "This repo",
300 },
301 {
302 t: `${name} — ${studio ? "Changes" : "Pull requests"}`,
303 h: studio ? `${base}/changes` : `${base}/pulls`,
304 c: "This repo",
305 },
294306 { t: `${name} — Commits`, h: `${base}/commits`, c: "This repo" },
295307 { t: `${name} — New issue`, h: `${base}/issues/new`, c: "This repo" },
296308 { t: `${name} — Runs`, h: `${base}/runs`, c: "This repo" },
src/web/pages/apps.ts +1 −1
@@ -761,7 +761,7 @@ export function appPage(ctx: Ctx): Response {
761761 ctx,
762762 title: `App · ${repo.name}`,
763763 crumbs: crumbsFor(repo),
764 palette: repoPalette(repo),
764 palette: repoPalette(ctx, repo),
765765 body,
766766 scriptJs: liveClientJs,
767767 });
src/web/pages/issues.ts +5 −9
@@ -63,7 +63,7 @@ import {
6363 threadFragment,
6464 } from "./thread.ts";
6565 import { crewActivityCard } from "./runs.ts";
66 import { studioTells } from "../studio/lists.ts";
66 import { studioListRecord, studioTells } from "../studio/lists.ts";
6767 import { studioTell } from "../studio/tell.ts";
6868
6969 const PER_PAGE = 25;
@@ -295,12 +295,8 @@ export async function issuesList(ctx: Ctx): Promise<Response> {
295295 // In the studio "everything told" is the issue list in the studio's own
296296 // words — the same rule `issuePage` follows for one tell. `?mode=dev` is
297297 // the one-off way to dev's list without switching the browser.
298 const studio =
299 ctx.mode === "studio" && ctx.url.searchParams.get("mode") !== "dev";
300 if (studio) {
301 const record = getAppByRepo(ctx.app.db, repo.id);
302 if (record !== undefined) return await studioTells(ctx, repo, record);
303 }
298 const record = studioListRecord(ctx, repo);
299 if (record !== undefined) return await studioTells(ctx, repo, record);
304300 const scope = localScope(ctx, repo);
305301 const base = scope.base;
306302 const query = readQuery(ctx);
@@ -420,7 +416,7 @@ export function newIssuePage(ctx: Ctx): Response {
420416 ctx,
421417 title: `New issue · ${repo.name}`,
422418 crumbs: crumbsFor(repo),
423 palette: repoPalette(repo),
419 palette: repoPalette(ctx, repo),
424420 body: html`${repoHeader(ctx, repo, "issues", {
425421 compact: true,
426422 ownTitle: true,
@@ -545,7 +541,7 @@ export async function issuePage(ctx: Ctx): Promise<Response> {
545541 ctx,
546542 title: `${issue.title} · ${repo.name}`,
547543 crumbs: crumbsFor(repo),
548 palette: repoPalette(repo),
544 palette: repoPalette(ctx, repo),
549545 body,
550546 styleCss: labelCss([...all, ...applied]),
551547 scriptJs: liveClientJs,
src/web/pages/map.ts +1 −1
@@ -1239,7 +1239,7 @@ export function mapPage(ctx: Ctx): Response {
12391239 ctx,
12401240 title: `Map · ${repo.name}`,
12411241 crumbs: crumbsFor(repo),
1242 palette: repoPalette(repo),
1242 palette: repoPalette(ctx, repo),
12431243 body,
12441244 styleCss: mapCss,
12451245 scriptJs: `${liveRegionsJs}\n${mapViewJs}`,
src/web/pages/pull.ts +1 −1
@@ -834,7 +834,7 @@ export async function pullPage(ctx: Ctx): Promise<Response> {
834834 ctx,
835835 title: `${pull.title} · ${repo.name}`,
836836 crumbs: crumbsFor(repo),
837 palette: repoPalette(repo),
837 palette: repoPalette(ctx, repo),
838838 body,
839839 styleCss: labelCss([...allLabels, ...applied]),
840840 scriptJs: liveClientJs,
src/web/pages/pulls.ts +5 −10
@@ -6,7 +6,6 @@
66 */
77
88 import { openPull } from "../../actions/pulls.ts";
9 import { getAppByRepo } from "../../core/apps.ts";
109 import {
1110 commentCounts,
1211 issueCounts,
@@ -53,7 +52,7 @@ import {
5352 } from "../ui.ts";
5453 import { refSet } from "./refs.ts";
5554 import { threadRow } from "./issues.ts";
56 import { studioChanges } from "../studio/lists.ts";
55 import { studioChanges, studioListRecord } from "../studio/lists.ts";
5756 import { originGlyph } from "./thread.ts";
5857
5958 const PER_PAGE = 25;
@@ -128,12 +127,8 @@ export async function pullsList(ctx: Ctx): Promise<Response> {
128127 // In the studio "every change" is the pull list in the studio's own
129128 // words — the same rule `pullPage` follows for one change. `?mode=dev` is
130129 // the one-off way to dev's list without switching the browser.
131 const studio =
132 ctx.mode === "studio" && ctx.url.searchParams.get("mode") !== "dev";
133 if (studio) {
134 const record = getAppByRepo(ctx.app.db, repo.id);
135 if (record !== undefined) return await studioChanges(ctx, repo, record);
136 }
130 const record = studioListRecord(ctx, repo);
131 if (record !== undefined) return await studioChanges(ctx, repo, record);
137132 const base = repoBase(repo);
138133 const db = ctx.app.db;
139134 const params = ctx.url.searchParams;
@@ -282,7 +277,7 @@ export async function pullsList(ctx: Ctx): Promise<Response> {
282277 ctx,
283278 title: `Pull requests · ${repo.name}`,
284279 crumbs: crumbsFor(repo),
285 palette: repoPalette(repo),
280 palette: repoPalette(ctx, repo),
286281 body,
287282 styleCss: labelCss([
288283 ...listLabels(db, repo.id),
@@ -449,7 +444,7 @@ export async function newPullPage(ctx: Ctx): Promise<Response> {
449444 ctx,
450445 title: `New pull request · ${repo.name}`,
451446 crumbs: crumbsFor(repo),
452 palette: repoPalette(repo),
447 palette: repoPalette(ctx, repo),
453448 body,
454449 });
455450 }
src/web/pages/runs.ts +2 −2
@@ -334,7 +334,7 @@ export async function runsList(ctx: Ctx): Promise<Response> {
334334 ctx,
335335 title: `Runs · ${repo.name}`,
336336 crumbs: crumbsFor(repo),
337 palette: repoPalette(repo),
337 palette: repoPalette(ctx, repo),
338338 body,
339339 scriptJs: liveClientJs,
340340 });
@@ -536,7 +536,7 @@ export async function runPage(ctx: Ctx): Promise<Response> {
536536 ctx,
537537 title: `${title} · ${repo.name}`,
538538 crumbs: crumbsFor(repo),
539 palette: repoPalette(repo),
539 palette: repoPalette(ctx, repo),
540540 body,
541541 scriptJs: runLogJs,
542542 // The no-script half of a live log: while the run is going, the page
src/web/pages/settings.ts +1 −1
@@ -170,7 +170,7 @@ function settingsPageResponse(
170170 ctx,
171171 title: `${title} · ${repo.name}`,
172172 crumbs: crumbsFor(repo),
173 palette: repoPalette(repo),
173 palette: repoPalette(ctx, repo),
174174 ...(styleCss !== undefined ? { styleCss } : {}),
175175 body: html`${repoHeader(ctx, repo, "settings", { compact: true })}
176176 ${settingsLayout(sections(repoBase(repo), active), body)}`,
src/web/studio/app.ts +3 −3
@@ -404,8 +404,8 @@ export async function studioApp(
404404 );
405405 const more = html`<p class="muted small">
406406 <a href="${base}/map">The map</a> ·
407 <a href="${base}/issues">Everything told</a> ·
408 <a href="${base}/pulls">Every change</a>
407 <a href="${base}/tells">Everything told</a> ·
408 <a href="${base}/changes">Every change</a>
409409 </p>`;
410410
411411 const body = html`${repoHeader(ctx, repo, "code", {
@@ -441,7 +441,7 @@ export async function studioApp(
441441 ctx,
442442 title: `${repo.name}`,
443443 crumbs: crumbsFor(repo),
444 palette: repoPalette(repo),
444 palette: repoPalette(ctx, repo),
445445 body,
446446 styleCss: studioCss,
447447 scriptJs: `${liveClientJs}\n${liveRegionsJs}\n${asksJs}`,
src/web/studio/change.ts +1 −1
@@ -197,7 +197,7 @@ export async function studioPull(
197197 ctx,
198198 title: `${pull.title} · ${repo.name}`,
199199 crumbs: crumbsFor(repo),
200 palette: repoPalette(repo),
200 palette: repoPalette(ctx, repo),
201201 body,
202202 styleCss: studioCss,
203203 scriptJs: liveClientJs,
src/web/studio/crew.ts +1 −1
@@ -253,7 +253,7 @@ export function studioCrewSettings(
253253 ctx,
254254 title: `Settings · ${repo.name}`,
255255 crumbs: crumbsFor(repo),
256 palette: repoPalette(repo),
256 palette: repoPalette(ctx, repo),
257257 body,
258258 });
259259 }
src/web/studio/lists.ts +66 −10
@@ -8,16 +8,23 @@
88 */
99
1010 import { appsRuntime } from "../../apps/runtime.ts";
11 import type { AppRecord } from "../../core/apps.ts";
11 import { getAppByRepo, type AppRecord } from "../../core/apps.ts";
1212 import { listIssues, type Issue } from "../../core/issues.ts";
1313 import { listPulls, openPullsClosing } from "../../core/pulls.ts";
1414 import type { Repo } from "../../core/repos.ts";
1515 import { listRuns } from "../../core/runs.ts";
1616 import { repoDir } from "../../git/repos.ts";
1717 import type { Ctx } from "../../http/context.ts";
18 import { crumbsFor, repoBase, repoHeader, repoPalette } from "../chrome.ts";
18 import {
19 crumbsFor,
20 loadRepo,
21 notFoundPage,
22 repoBase,
23 repoHeader,
24 repoPalette,
25 } from "../chrome.ts";
1926 import { html, type Html } from "../html.ts";
20 import { page } from "../layout.ts";
27 import { page, redirect } from "../layout.ts";
2128 import { liveClientJs, liveMount } from "../live.ts";
2229 import {
2330 avatar,
@@ -98,6 +105,18 @@ function pagedHref(base: string, filter: WordFilter | undefined, cursor: string)
98105 return `${base}?${filter ? `word=${filter}&` : ""}cursor=${cursor}`;
99106 }
100107
108 /**
109 * The one rule for whether a viewer sees the studio's own list on this repo:
110 * studio mode, no `?mode=dev` escape hatch, and an app to have a studio word
111 * for — the same check `issuesList`/`pullsList` make before falling back to
112 * dev's own list.
113 */
114 export function studioListRecord(ctx: Ctx, repo: Repo): AppRecord | undefined {
115 const studio =
116 ctx.mode === "studio" && ctx.url.searchParams.get("mode") !== "dev";
117 return studio ? getAppByRepo(ctx.app.db, repo.id) : undefined;
118 }
119
101120 /**
102121 * GET /:owner/:name/issues in the studio: everything told, one row per
103122 * issue in its studio word — who has it, when it last moved, and the tell
@@ -166,13 +185,17 @@ export async function studioTells(
166185 ),
167186 );
168187 const lastNumber = fetched[fetched.length - 1]?.number;
188 // Self-links follow whichever noun reached this page — `/tells` from the
189 // studio's own door, `/issues` when a dev URL is asked for in studio mode
190 // — so paging never jumps a reader to the other address mid-list.
191 const listBase = ctx.url.pathname;
169192
170193 const body = html`${repoHeader(ctx, repo, "issues")}
171194 <div class="mt">
172 ${wordFilterBar(`${base}/issues`, filter)} ${list}
195 ${wordFilterBar(listBase, filter)} ${list}
173196 ${pager(
174197 fetched.length === LIST_PAGE && lastNumber !== undefined
175 ? { next: pagedHref(`${base}/issues`, filter, String(lastNumber)) }
198 ? { next: pagedHref(listBase, filter, String(lastNumber)) }
176199 : {},
177200 )}
178201 </div>
@@ -186,7 +209,7 @@ export async function studioTells(
186209 ctx,
187210 title: `Everything told · ${repo.name}`,
188211 crumbs: crumbsFor(repo),
189 palette: repoPalette(repo),
212 palette: repoPalette(ctx, repo),
190213 body,
191214 styleCss: studioCss,
192215 scriptJs: liveClientJs,
@@ -251,13 +274,17 @@ export async function studioChanges(
251274 ),
252275 );
253276 const lastNumber = fetched[fetched.length - 1]?.number;
277 // Self-links follow whichever noun reached this page — `/changes` from the
278 // studio's own door, `/pulls` when a dev URL is asked for in studio mode
279 // — so paging never jumps a reader to the other address mid-list.
280 const listBase = ctx.url.pathname;
254281
255282 const body = html`${repoHeader(ctx, repo, "pulls")}
256283 <div class="mt">
257 ${wordFilterBar(`${base}/pulls`, filter)} ${list}
284 ${wordFilterBar(listBase, filter)} ${list}
258285 ${pager(
259286 fetched.length === LIST_PAGE && lastNumber !== undefined
260 ? { next: pagedHref(`${base}/pulls`, filter, String(lastNumber)) }
287 ? { next: pagedHref(listBase, filter, String(lastNumber)) }
261288 : {},
262289 )}
263290 </div>
@@ -271,13 +298,42 @@ export async function studioChanges(
271298 ctx,
272299 title: `Every change · ${repo.name}`,
273300 crumbs: crumbsFor(repo),
274 palette: repoPalette(repo),
301 palette: repoPalette(ctx, repo),
275302 body,
276303 styleCss: studioCss,
277304 scriptJs: liveClientJs,
278305 });
279306 }
280307
308 /**
309 * GET /:owner/:name/tells — the studio's own door onto the same list
310 * `/issues` reaches: `issuesList` and this route both resolve through
311 * `studioListRecord`/`studioTells`, so there is exactly one page, never a
312 * second copy of it. A viewer for whom the studio's own list does not apply
313 * (dev mode, `?mode=dev`, or a repository that is not an app) is sent to
314 * `/issues` — dev's own address is the one canonical URL for dev's own list,
315 * so this noun never grows a second, dev-flavoured address of its own.
316 */
317 export async function tellsList(ctx: Ctx): Promise<Response> {
318 const repo = loadRepo(ctx);
319 if (!repo) return notFoundPage(ctx);
320 const record = studioListRecord(ctx, repo);
321 if (record === undefined)
322 return redirect(`${repoBase(repo)}/issues${ctx.url.search}`);
323 return await studioTells(ctx, repo, record);
324 }
325
326 /** GET /:owner/:name/changes — the studio's own door onto the same list
327 * `/pulls` reaches. See `tellsList` for the rule. */
328 export async function changesList(ctx: Ctx): Promise<Response> {
329 const repo = loadRepo(ctx);
330 if (!repo) return notFoundPage(ctx);
331 const record = studioListRecord(ctx, repo);
332 if (record === undefined)
333 return redirect(`${repoBase(repo)}/pulls${ctx.url.search}`);
334 return await studioChanges(ctx, repo, record);
335 }
336
281337 /**
282338 * GET /:owner/:name/runs in the studio: every run, one row per run in its
283339 * studio word — who ran it, when it last moved, and the transcript as the
@@ -357,7 +413,7 @@ export async function studioRuns(
357413 ctx,
358414 title: `Every run · ${repo.name}`,
359415 crumbs: crumbsFor(repo),
360 palette: repoPalette(repo),
416 palette: repoPalette(ctx, repo),
361417 body,
362418 styleCss: studioCss,
363419 scriptJs: liveClientJs,
src/web/studio/run.ts +1 −1
@@ -283,7 +283,7 @@ export async function studioRun(
283283 ctx,
284284 title: `${title} · ${repo.name}`,
285285 crumbs: crumbsFor(repo),
286 palette: repoPalette(repo),
286 palette: repoPalette(ctx, repo),
287287 body,
288288 styleCss: studioCss,
289289 scriptJs: studioRunJs,
src/web/studio/tell.ts +1 −1
@@ -299,7 +299,7 @@ export async function studioTell(
299299 ctx,
300300 title: `${issue.title} · ${repo.name}`,
301301 crumbs: crumbsFor(repo),
302 palette: repoPalette(repo),
302 palette: repoPalette(ctx, repo),
303303 body,
304304 styleCss: studioCss,
305305 scriptJs: liveClientJs,
test/studio.test.ts +123
@@ -714,6 +714,129 @@ describe("the studio", () => {
714714 expect(runs).not.toContain("See dev's list");
715715 });
716716
717 test("/tells and /changes are the studio's own doors onto the same lists /issues and /pulls reach — same words, same filter, same paging", async () => {
718 const told = openIssue(app, alice, repo, {
719 title: "Door test: a tell",
720 body: "reached through /tells too",
721 });
722 if (!told.ok) throw new Error(String(told.error));
723
724 const viaIssues = await (await get("/alice/notes/issues")).text();
725 const viaTells = await (await get("/alice/notes/tells")).text();
726 expect(viaTells).toContain("Door test: a tell");
727 expect(viaTells).toContain('data-word="told"');
728 // Self-links (the word filter, paging) follow whichever door was used,
729 // so a dev URL asked for in studio mode still answers exactly as it
730 // does today.
731 expect(viaTells).toContain(`href="/alice/notes/tells?word=told"`);
732 expect(viaIssues).toContain(`href="/alice/notes/issues?word=told"`);
733
734 const filtered = await (
735 await get("/alice/notes/tells?word=told")
736 ).text();
737 expect(filtered).toContain("Door test: a tell");
738 const other = await (
739 await get("/alice/notes/tells?word=shipped")
740 ).text();
741 expect(other).not.toContain("Door test: a tell");
742
743 const branch = await commitToBranch(app, alice, repo, {
744 branch: "feature/door-test",
745 base: "main",
746 message: "Door test change",
747 files: [{ path: "DOOR_TEST.md", content: "a change\n" }],
748 });
749 if (!branch.ok) throw new Error(JSON.stringify(branch.error));
750 const opened = await openPull(app, alice, repo, {
751 title: "Door test: a change",
752 body: "",
753 baseBranch: "main",
754 headBranch: "feature/door-test",
755 });
756 if (!opened.ok) throw new Error(String(opened.error));
757 const doorPull = opened.value;
758 const doorCheck = createRun(app.db, {
759 repoId: repo.id,
760 kind: "check",
761 issueId: doorPull.id,
762 env: `pull-${doorPull.number}`,
763 commitOid: doorPull.headOid,
764 reason: "pull",
765 });
766 claimRun(app.db, doorCheck.id);
767 finishRun(app.db, doorCheck.id, { state: "ok", summary: "check passed" });
768
769 const viaPulls = await (await get("/alice/notes/pulls")).text();
770 const viaChanges = await (await get("/alice/notes/changes")).text();
771 expect(viaChanges).toContain("Door test: a change");
772 expect(viaChanges).toContain('data-word="look"');
773 expect(viaChanges).toContain(`href="/alice/notes/changes?word=look"`);
774 expect(viaPulls).toContain(`href="/alice/notes/pulls?word=look"`);
775
776 const changeFiltered = await (
777 await get("/alice/notes/changes?word=look")
778 ).text();
779 expect(changeFiltered).toContain("Door test: a change");
780 const changeOther = await (
781 await get("/alice/notes/changes?word=shipped")
782 ).text();
783 expect(changeOther).not.toContain("Door test: a change");
784 });
785
786 test("/tells and /changes send a dev-mode viewer, or ?mode=dev, on to /issues and /pulls — the one canonical dev address", async () => {
787 const devTells = await get("/alice/notes/tells", {
788 cookie: `${cookie}; forge_mode=dev`,
789 });
790 expect(devTells.status).toBe(303);
791 expect(devTells.headers.get("location")).toBe("/alice/notes/issues");
792
793 const devChanges = await get("/alice/notes/changes", {
794 cookie: `${cookie}; forge_mode=dev`,
795 });
796 expect(devChanges.status).toBe(303);
797 expect(devChanges.headers.get("location")).toBe("/alice/notes/pulls");
798
799 const escapeTells = await fetch(`${base}/alice/notes/tells?mode=dev`, {
800 headers: { cookie: `${cookie}; forge_mode=studio` },
801 redirect: "manual",
802 });
803 expect(escapeTells.status).toBe(303);
804 expect(escapeTells.headers.get("location")).toBe(
805 "/alice/notes/issues?mode=dev",
806 );
807 });
808
809 test("/tells and /changes on a repository that is not an app send a studio viewer on to /issues and /pulls too", async () => {
810 const plain = await createRepo(app, alice, {
811 name: "doorlists",
812 visibility: "private",
813 description: "no app here",
814 });
815 if (!plain.ok) throw new Error(String(plain.error));
816
817 const tells = await get("/alice/doorlists/tells");
818 expect(tells.status).toBe(303);
819 expect(tells.headers.get("location")).toBe("/alice/doorlists/issues");
820
821 const changes = await get("/alice/doorlists/changes");
822 expect(changes.status).toBe(303);
823 expect(changes.headers.get("location")).toBe("/alice/doorlists/pulls");
824 });
825
826 test("a reader without access gets the forge's not-found on /tells and /changes, exactly as /issues and /pulls", async () => {
827 const anonTells = await fetch(`${base}/alice/notes/tells`, {
828 headers: { cookie: "forge_mode=studio" },
829 redirect: "manual",
830 });
831 expect(anonTells.status).toBe(404);
832
833 const anonChanges = await fetch(`${base}/alice/notes/changes`, {
834 headers: { cookie: "forge_mode=studio" },
835 redirect: "manual",
836 });
837 expect(anonChanges.status).toBe(404);
838 });
839
717840 test("a reader without access gets the forge's not-found on a tell, exactly as dev", async () => {
718841 const told = openIssue(app, alice, repo, { title: "Private work" });
719842 if (!told.ok) throw new Error(String(told.error));
test/web.test.ts +3 −3
@@ -304,7 +304,7 @@ describe("web pages end to end", () => {
304304 });
305305
306306 test("a path under no route at all gets the forge's page for a browser, and no_route for anyone else", async () => {
307 const browser = await fetch(`${base}/ada/engine/tells`, {
307 const browser = await fetch(`${base}/ada/engine/frobnicate`, {
308308 headers: {
309309 accept:
310310 "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
@@ -323,12 +323,12 @@ describe("web pages end to end", () => {
323323
324324 // No Accept header at all — a plain script or `curl` — keeps the JSON
325325 // contract, same as before this page existed.
326 const plain = await fetch(`${base}/ada/engine/tells`);
326 const plain = await fetch(`${base}/ada/engine/frobnicate`);
327327 expect(plain.status).toBe(404);
328328 expect(plain.headers.get("content-type")).toContain("application/json");
329329 const body = (await plain.json()) as { error: string; message: string };
330330 expect(body.error).toBe("no_route");
331 expect(body.message).toContain("/ada/engine/tells");
331 expect(body.message).toContain("/ada/engine/frobnicate");
332332
333333 // An API path is unchanged even for a browser's own Accept header.
334334 const api = await fetch(`${base}/api/v1/nonsense`, {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs