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

small: one name per thing in the account menu: SSH keys, access keys and linked sites read the same on the label, the page title, the empty line and the palette #10818

Merged opened this

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

Merged

scout/forge:scout/10792-small-one-name-per-thing-in-the-account → main 4 commits fork 5 threads · 5 unresolved

  • Merged. scout/10792-small-one-name-per-thing-in-the-account was merged into main as b741b54.

Review

Sign in to review this change.
16 files changed +171 −49
src/actions/origin_words.ts +1 −1
@@ -1,6 +1,6 @@
11 /**
22 * What an origin's sync and disconnect say, in one place so the page in
3 * Settings › Origins and the terminal's Connected origins say the same thing.
3 * Settings › Origins and the terminal's Origins screen say the same thing.
44 */
55
66 export const NO_SUCH_ORIGIN = "No such origin.";
src/tui/screens/connections.ts +4 −4
@@ -1,5 +1,5 @@
11 /**
2 * Connected origins in the terminal: the hosts your mirrors come from, as
2 * Origins in the terminal: the hosts your mirrors come from, as
33 * Settings › Origins lists them. `s` syncs the selected one now, `d` disconnects
44 * it after a confirm — through `actions/origins.ts`, in `origin_words.ts`'s words.
55 */
@@ -27,7 +27,7 @@ export function connectionsScreen(f: Session, ctx: Ctx): Screen {
2727 const counts = () => (f.user === undefined ? new Map<string, { count: number }>() : mirrorsByOrigin(f.app.db, f.user.id));
2828
2929 const screen: Screen = {
30 title: "Connected origins",
30 title: "Origins",
3131 keys: [binding(["j", "k"], "move cursor"), binding(["c"], "connect"), binding(["s"], "sync now"), binding(["v"], "verify again"), binding(["d"], "disconnect"), binding(["esc"], "back")],
3232 render(area, s) {
3333 const c = ctx.theme.c;
@@ -159,10 +159,10 @@ export function connectionsScreen(f: Session, ctx: Ctx): Screen {
159159 return screen;
160160 }
161161
162 /** Connected origins: a signed-in person, not an agent. */
162 /** Origins: a signed-in person, not an agent. */
163163 export const entry: PaletteEntry = {
164164 id: "origins",
165 title: "Connected origins",
165 title: "Origins",
166166 hint: "the hosts your mirrors come from",
167167 offered: (session) => session.user !== undefined && session.user.kind !== "agent",
168168 run: (session, ctx) => ctx.push(connectionsScreen(session, ctx)),
src/tui/screens/signins.ts +6 −6
@@ -104,7 +104,7 @@ function hardWrap(text: string, width: number): string[] {
104104 function secretScreen(ctx: Ctx, secret: string, market: { add: string; install: string } | null): Screen {
105105 let copied = "";
106106 return {
107 title: "New token",
107 title: "New access token",
108108 keys: [
109109 binding(["y"], "copy it"),
110110 ...(market === null ? [] : [binding(["a"], "copy the marketplace line"), binding(["i"], "copy the install line")]),
@@ -222,7 +222,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
222222 const confirmAdmin = (name: string) =>
223223 later(ctx, () =>
224224 confirmScreen(ctx, screen, {
225 title: "Make an admin token",
225 title: "Make an admin access token",
226226 body: [`${name} will act as admin: y to make it, n to cancel.`],
227227 actions: [{ label: "Make it", variant: "danger" }, { label: "Cancel", variant: "ghost" }],
228228 yesNo: true,
@@ -237,7 +237,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
237237 const askScope = (name: string, isAdmin: boolean) =>
238238 later(ctx, () =>
239239 promptScreen(ctx, screen, {
240 title: "Make a token",
240 title: "Make an access token",
241241 label: isAdmin ? "Scope: ro, rw or admin" : "Scope: ro or rw",
242242 value: "ro",
243243 onSubmit: (word) => {
@@ -252,7 +252,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
252252
253253 const askName = (isAdmin: boolean) =>
254254 promptScreen(ctx, screen, {
255 title: "Make a token",
255 title: "Make an access token",
256256 label: "Name",
257257 value: "token",
258258 onSubmit: (name) => {
@@ -323,7 +323,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
323323 keys: [
324324 binding(["j", "k"], "move cursor"),
325325 binding(["d"], "sign a browser out or revoke a token"),
326 binding(["m"], "make a token"),
326 binding(["m"], "make an access token"),

Terminal now says 'access token' here, but the terminal's origins screen is still 'Connected origins' (connections.ts:30,165). One name per thing must hold in both doors; rename it and the palette entry.

327327 binding(["p"], "change your password"),
328328 ],
329329 render(area, s) {
@@ -337,7 +337,7 @@ export function signInsScreen(f: Session, ctx: Ctx): Screen {
337337 ? "Sign in to see where you are signed in."
338338 : f.user.kind === "agent"
339339 ? "Where you are signed in. d signs a browser out or revokes a token."
340 : "Where you are signed in. d signs a browser out or revokes a token, m makes a token, p changes your password.";
340 : "Where you are signed in. d signs a browser out or revokes a token, m makes an access token, p changes your password.";
341341 s.text(head!.x, head!.y, truncate(text, head!.w), style(c.mutedFg, c.bg), head!.x + head!.w);
342342 drawList(s, body!, c, state, rows(all));
343343 },
src/web/pages/account.ts +10 −10
@@ -751,7 +751,7 @@ function tokensView(ctx: Ctx): Html {
751751 listSessions(ctx.app.db, ctx.auth.user!.id, cookieValue(ctx.request, "forge_session")),
752752 );
753753 const list = tokens.length === 0
754 ? html`<p class="muted">No tokens yet. Mint one when a script, a CI job, or another machine needs to act as you.</p>`
754 ? html`<p class="muted">No access tokens yet. Mint one when a script, a CI job, or another machine needs to act as you.</p>`
755755 : html`<ul class="list setting-list">
756756 ${tokens.map((t) =>
757757 row({
@@ -766,18 +766,18 @@ function tokensView(ctx: Ctx): Html {
766766 </ul>`;
767767 return html`${help([
768768 [
769 "Tokens",
769 "Access tokens",
770770 "Use a token as the password when pushing over HTTPS, or as a Bearer token for the API. Your account password works for git too — tokens are for scoped or automated access.",
771771 ],

Principle 8 (plain words, one name): help term and the field label below still say "Token name"; use "Access token name" so the page reads one name.

772 ["Token name", "Name it after where it lives — laptop, CI, deploy box."],
772 ["Access token name", "Name it after where it lives — laptop, CI, deploy box."],
773773 ["Scope", "A token can never mint another token, whatever its scope."],
774774 ])}${connectCard(ctx, readFlash(ctx.request)?.code)}
775 ${block("Tokens", list)}
775 ${block("Access tokens", list)}
776776 ${block(
777 "New token",
777 "New access token",
778778 html`<form method="post" action="/settings/tokens">
779779 ${uiField({
780 label: "Token name",
780 label: "Access token name",
781781 name: "name",
782782 required: true,
783783 })}
@@ -804,7 +804,7 @@ function tokensView(ctx: Ctx): Html {
804804 </div>`
805805 : null}
806806 <div class="form-actions">
807 ${btn("Generate token", { variant: "primary" })}
807 ${btn("Generate access token", { variant: "primary" })}
808808 </div>
809809 </form>`,
810810 )}
@@ -816,7 +816,7 @@ export function tokensPage(ctx: Ctx): Response {
816816 if (!ctx.auth.user) return signInFirst(ctx);
817817 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
818818 const body = tokensView(ctx);
819 return timePhase(ctx.request, "render", () => accountShell(ctx, "tokens", "Personal access tokens", body));
819 return timePhase(ctx.request, "render", () => accountShell(ctx, "tokens", "Access tokens", body));
820820 }
821821
822822 // POST /settings/tokens
@@ -909,7 +909,7 @@ function keysView(
909909 ${block(
910910 "SSH keys",
911911 keys.length === 0
912 ? html`<p class="muted">No keys yet. Add one and git over SSH stops asking for a password.</p>`
912 ? html`<p class="muted">No SSH keys yet. Add one and git over SSH stops asking for a password.</p>`
913913 : html`<ul class="list setting-list">
914914 ${keys.map((k) =>
915915 row({
@@ -940,7 +940,7 @@ function keysView(
940940 placeholder: "ssh-ed25519 AAAA…",
941941 })}
942942 <div class="form-actions">
943 ${btn("Add key", { variant: "primary" })}
943 ${btn("Add SSH key", { variant: "primary" })}
944944 </div>
945945 ${refusal(values.error)}
946946 </form>`,
src/web/pages/origins.ts +3 −3
@@ -794,14 +794,14 @@ function originsView(
794794 </p>
795795 ${origins.length === 0
796796 ? html`<section class="settings-block">

Principle 8: the block heading above is "Connected origins" while the menu, title and h1 say "Origins"; pick one name for the section too.

797 <h2>Connected origins</h2>
797 <h2>Origins</h2>

Web says 'Origins'; the terminal and src/actions/origin_words.ts still say 'Connected origins'. Align them so the two doors read the same.

798798 <p class="small">
799 No origins connected: connect another forge, GitHub, a Forgejo
799 No origins yet: connect another forge, GitHub, a Forgejo
800800 instance or a Gitea instance below.
801801 </p>
802802 </section>`
803803 : html`<section class="settings-block">
804 <h2>Connected origins</h2>
804 <h2>Origins</h2>
805805 <ul class="list setting-list" data-origins>
806806 ${origins.map((o) =>
807807 originCard(
test/account-menu-names.test.ts added +122
@@ -0,0 +1,122 @@
1 /** A person who taps an account-menu label meets the same words on the page it opens: label, title, heading, empty line, palette. */
2 import { afterAll, beforeAll, expect, test } from "bun:test";
3 import { mkdtemp, rm } from "node:fs/promises";
4 import { tmpdir } from "node:os";
5 import { join } from "node:path";
6 import { createApp, type App } from "../src/app.ts";
7 import { serve, type Servers } from "../src/http/server.ts";
8 import { getUserByName } from "../src/core/users.ts";
9 import { createForgeApp } from "../src/tui/forge.ts";
10 import type { TuiIdentity } from "../src/tui/session.ts";
11 import { headlessTerminal } from "../src/tui/term/terminal.ts";
12 import { quietLogs } from "./helpers/globals.ts";
13 import { settleApp } from "./helpers/tui.ts";
14
15 quietLogs();
16
17 let dir = "";
18 let app: App, servers: Servers, base = "", cookie = "";
19
20 beforeAll(async () => {
21 dir = await mkdtemp(join(tmpdir(), "forge-menunames-"));
22 app = createApp({ dataDir: dir, port: 0 });
23 servers = await serve(app);
24 base = servers.url;
25 const signup = await fetch(`${base}/signup`, {
26 method: "POST",
27 body: new URLSearchParams({ username: "ada", email: "[email protected]", password: "account-password" }),
28 redirect: "manual",
29 });
30 cookie = (signup.headers.get("set-cookie") ?? "").split(";")[0]!;
31 });
32
33 afterAll(async () => {
34 servers.stop();
35 app.close();
36 await rm(dir, { recursive: true, force: true });
37 });
38
39 const PAGES: { name: string; path: string; empty: string }[] = [
40 { name: "SSH keys", path: "/settings/keys", empty: "No SSH keys yet" },
41 { name: "Access tokens", path: "/settings/tokens", empty: "No access tokens yet" },
42 { name: "Origins", path: "/settings/origins", empty: "No origins yet" },
43 ];
44
45 for (const { name, path, empty } of PAGES) {
46 test(`${name}: menu, nav, palette, title, heading and empty line use one name`, async () => {
47 const res = await fetch(`${base}${path}`, { headers: { cookie } });
48 expect(res.status).toBe(200);
49 const body = await res.text();
50 expect(body).toContain(`<title>${name}`);
51 expect(body).toContain(`<h1>${name}</h1>`);
52 expect(body).toContain(empty);
53 // The user menu and the settings nav link to this page under this label.
54 expect(body).toMatch(new RegExp(`href="${path}"[^>]*>(?:<[^>]+>)*\\s*${name}\\s*<`));
55 // The palette entry carries the same words.
56 expect(body).toContain(`"t":"${name}"`);
57 });
58 }
59
60 test("access tokens: help term, field label and button say access token", async () => {
61 const body = await (await fetch(`${base}/settings/tokens`, { headers: { cookie } })).text();
62 expect(body).toContain("Access token name");
63 expect(body).not.toContain("Token name");
64 expect(body).toContain("Generate access token");
65 });
66
67 test("origins: the section heading reads Origins, not Connected origins", async () => {
68 const body = await (await fetch(`${base}/settings/origins`, { headers: { cookie } })).text();
69 expect(body).toContain("<h2>Origins</h2>");
outdated

This greps source text, so it passes even if the screen never renders that title. Render the screens and assert their titles, including the terminal origins screen.

70 expect(body).not.toContain("Connected origins");
71 });
72
73 // The terminal is the other door: drive the real screens and read what they draw.
74 function boot() {
75 const terminal = headlessTerminal(140, 32);
76 const identity = { sshUsername: "t", remoteAddress: "127.0.0.1", user: getUserByName(app.db, "ada")! } as TuiIdentity;
77 const { app: tui } = createForgeApp(app, identity, { terminal, exitHooks: false });
78 tui.start();
79 const key = (k: string, ctrl = false) => tui.dispatch({ type: "key", key: k, ctrl, alt: false, shift: false });
80 return {
81 text: () => tui.screen.toText(),
82 keys: async (...ks: string[]) => {
83 ks.forEach((k) => key(k));
84 await settleApp(tui);
85 },
86 palette: async (words: string) => {
87 key("k", true);
88 for (const ch of words) key(ch);
89 key("enter");
90 await settleApp(tui);
91 },
92 stop: () => tui.stop(),
93 };
94 }
95
96 test("terminal: Signed in makes an access token on screens titled access token", async () => {
97 const h = boot();
98 try {
99 await h.palette("Signed in");
100 expect(h.text()).toContain("m makes an access token");
101 await h.keys("m");
102 expect(h.text()).toContain(" Make an access token ");
103 await h.keys("enter"); // the name: "token"
104 expect(h.text()).toContain(" Make an access token ");
105 await h.keys("enter"); // the scope: ro
106 expect(h.text()).toMatch(/^ada › Signed in › New access token$/m);
107 expect(h.text()).not.toMatch(/ (New|Make a) token /);
108 } finally {
109 h.stop();
110 }
111 });
112
113 test("terminal: the origins screen is titled Origins, as the page is", async () => {
114 const h = boot();
115 try {
116 await h.palette("Origins");
117 expect(h.text()).toMatch(/^ada › Origins$/m);
118 expect(h.text()).not.toContain("Connected origins");
119 } finally {
120 h.stop();
121 }
122 });
test/account-rows.test.ts +4 −4
@@ -63,8 +63,8 @@ test("no account page is a card or holds a table", async () => {
6363 test("tokens are rows under a Tokens heading, each with Revoke", async () => {
6464 const m = await main("/settings/tokens");
6565 expect(m).toContain('<section class="settings-block">');
66 const list = m.slice(m.indexOf("<h2>Tokens</h2>"));
67 expect(list).toMatch(/^<h2>Tokens<\/h2>\s*<ul class="list setting-list">/);
66 const list = m.slice(m.indexOf("<h2>Access tokens</h2>"));
67 expect(list).toMatch(/^<h2>Access tokens<\/h2>\s*<ul class="list setting-list">/);
6868 const item = list.slice(0, list.indexOf("</ul>"));
6969 expect(item).toContain("<strong>laptop</strong>");
7070 expect(item).toMatch(/settings%2Ftokens%2F[^"]+%2Fdelete/);
@@ -90,10 +90,10 @@ test("empty lists are one muted sentence, not a panel", async () => {
9090 const c = (other.headers.get("set-cookie") ?? "").split(";")[0]!;
9191 const get = async (path: string) => (await (await fetch(`${base}${path}`, { headers: { cookie: c } })).text());
9292 const tokens = await get("/settings/tokens");
93 expect(tokens).toContain('<p class="muted">No tokens yet.');
93 expect(tokens).toContain('<p class="muted">No access tokens yet.');
9494 expect(tokens).not.toContain("empty-title");
9595 const keys = await get("/settings/keys");
96 expect(keys).toContain('<p class="muted">No keys yet.');
96 expect(keys).toContain('<p class="muted">No SSH keys yet.');
9797 expect(keys).not.toContain("empty-title");
9898 const card = String(sessionsCard([], "/settings"));
9999 expect(card).toContain('<p class="muted">Not signed in on any browser.</p>');
test/keys.test.ts +1 −1
@@ -284,7 +284,7 @@ describe.skipIf(NO_KEYGEN)("ssh keys", () => {
284284 const empty = await (
285285 await fetch(`${base}/settings/keys`, { headers: { cookie } })
286286 ).text();
287 expect(empty).toContain("No keys yet");
287 expect(empty).toContain("No SSH keys yet");
288288
289289 const added = await fetch(`${base}/settings/keys`, {
290290 method: "POST",
test/onboarding.test.ts +1 −1
@@ -123,7 +123,7 @@ describe("onboarding: a person with just a login", () => {
123123 const pageHtml = await (
124124 await fetch(`${base}/settings/tokens`, { headers: { cookie } })
125125 ).text();
126 expect(pageHtml).toContain("Personal access tokens");
126 expect(pageHtml).toContain("Access tokens");
127127
128128 // Post-redirect-get: minting redirects, and the secret rides a one-shot
129129 // flash cookie so a refresh shows the list instead of minting again.
test/origins-page-shape.test.ts +5 −5
@@ -50,14 +50,14 @@ test("the origins page has settings blocks with headings and no card", async ()
5050 expect(res.status).toBe(200);
5151 const body = await res.text();
5252 expect(body).not.toMatch(/class="(?:[^"]*\s)?card(?:\s[^"]*)?"/);
53 for (const h of ["Connected origins", "Trusted internal hosts", "Connect an origin"]) {
53 for (const h of ["Origins", "Trusted internal hosts", "Connect an origin"]) {
5454 expect(body).toMatch(new RegExp(`<section class="settings-block"[^>]*>\\s*<h2>${h}</h2>`));
5555 }
5656 expect(body).toContain(
57 "No origins connected: connect another forge, GitHub, a Forgejo\n instance or a Gitea instance below.",
57 "No origins yet: connect another forge, GitHub, a Forgejo\n instance or a Gitea instance below.",
5858 );
5959 // One sentence: no full stop before its end.
60 const empty = body.split("<h2>Connected origins</h2>")[1]!.split("</p>")[0]!;
60 const empty = body.split("<h2>Origins</h2>")[1]!.split("</p>")[0]!;
6161 expect(empty.replace(/<[^>]+>/g, "").trim().split(". ").length).toBe(1);
6262 expect(body).toContain('id="connect"');
6363 });
@@ -77,7 +77,7 @@ test("connected origins are rows of one list, with no wrapper box", async () =>
7777 }
7878 const body = await (await fetch(`${servers.url}/settings/origins`, { headers: { cookie } })).text();
7979 expect(body).not.toContain("origin-item");
80 const block = body.split("<h2>Connected origins</h2>")[1]!.split("</section>")[0]!;
80 const block = body.split("<h2>Origins</h2>")[1]!.split("</section>")[0]!;
8181 expect(block.match(/<ul class="list setting-list" data-origins>/g)?.length).toBe(1);
8282 expect(block.match(/<li[ >]/g)!.length).toBeGreaterThanOrEqual(2);
8383 expect(block).toContain("Sync now");
@@ -100,7 +100,7 @@ test("an origin's error and notes render inside its own row", async () => {
100100 .query("UPDATE origin SET last_error = ?1 WHERE id = ?2")
101101 .run("upstream said 502", o.value.id);
102102 const body = await (await fetch(`${servers.url}/settings/origins`, { headers: { cookie } })).text();
103 const block = body.split("<h2>Connected origins</h2>")[1]!.split("</section>")[0]!;
103 const block = body.split("<h2>Origins</h2>")[1]!.split("</section>")[0]!;
104104 const li = block
105105 .split(/<li[ >]/)
106106 .slice(1)
test/palette-roster.test.ts +3 −3
@@ -156,7 +156,7 @@ test("a site-admin person is offered the whole roster", () => {
156156 ["cloud", "Cloud accounts", "provider tokens, what they are used for, today's calls"],
157157 ["pinned", "Pinned forges", "deploy, update and roll back a forge on your cloud"],
158158 ["bets", "Bets", "back, stop and finish a repository\'s bets"],
159 ["origins", "Connected origins", "the hosts your mirrors come from"],
159 ["origins", "Origins", "the hosts your mirrors come from"],
160160 ["domains", "Failure domains", "failure domains and their last drill"],
161161 ["feed", "Feed", "what happened, ranked; r replies on a post about an issue or pull, l likes, h who liked"],
162162 ["feature-review", "Menu entries to review", "absorbed apps whose declared name or page changed: accept them"],
@@ -248,7 +248,7 @@ test("a site-admin agent is offered the roster without the person-only entries",
248248 ]);
249249 });
250250
251 test("no agent is offered Connected origins", () => {
251 test("no agent is offered Origins", () => {
252252 expect(roster({ ...admin, kind: "agent" }).map(([id]) => id)).not.toContain("origins");
253253 });
254254
@@ -313,7 +313,7 @@ test("a member person is offered the signed-in roster without the admin entries"
313313 ["archived", "Archived repositories", "put away, not gone: open one or wake it"],
314314 ["bulk", "Bulk actions", "your last bulk actions; u undoes one inside its day"],
315315 ["bets", "Bets", "back, stop and finish a repository\'s bets"],
316 ["origins", "Connected origins", "the hosts your mirrors come from"],
316 ["origins", "Origins", "the hosts your mirrors come from"],
317317 ["feed", "Feed", "what happened, ranked; r replies on a post about an issue or pull, l likes, h who liked"],
318318 ["feature-feed", "Feature feeds", "the feed items about an absorbed app, as Settings › Features shows them"],
319319 ["front", "Front page", "the apps a first visitor sees, ranked; an admin pins or hides"],
test/tui-origins-forge.test.ts +2 −2
@@ -73,7 +73,7 @@ test("c connects another forge by address and token, and the list says what did
7373 };
7474 try {
7575 await key("k");
76 await type("Connected origins");
76 await type("Origins");
7777 await key("enter");
7878 expect(app.screen.toText()).toContain("Press c to connect one.");
7979 // A forge has no device sign-in: the terminal says so and points at the page.
@@ -123,7 +123,7 @@ test("a token the forge refuses connects nothing and says so", async () => {
123123 };
124124 try {
125125 await press("k", true);
126 await type("Connected origins");
126 await type("Origins");
127127 await press("enter");
128128 await press("c");
129129 await type(friendServers.url);
test/tui-origins-guard.test.ts +1 −1
@@ -68,7 +68,7 @@ describe("a paused origin in the terminal", () => {
6868 };
6969 try {
7070 await key("k", true);
71 for (const ch of "Connected origins") await key(ch);
71 for (const ch of "Origins") await key(ch);
7272 await key("enter");
7373 expect(app.screen.toText()).toContain("writes paused");
7474 expect(app.screen.toText()).toContain("Press v to check again");
test/tui-origins.test.ts +5 −5
@@ -1,4 +1,4 @@
1 /** Connected origins in the terminal: listed, disconnected after a confirm, never offered to a guest. */
1 /** Origins in the terminal: listed, disconnected after a confirm, never offered to a guest. */
22
33 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
44 import { mkdtemp, rm } from "node:fs/promises";
@@ -47,7 +47,7 @@ describe("connected origins in the terminal", () => {
4747
4848 const open = async (h: ReturnType<typeof boot>) => {
4949 await h.ctrl("k");
50 await h.type("Connected origins");
50 await h.type("Origins");
5151 return h;
5252 };
5353
@@ -116,7 +116,7 @@ describe("connected origins in the terminal", () => {
116116 const h = boot({ sshUsername: "t", remoteAddress: "127.0.0.1", user: getUserByName(forge.db, "carol")! } as TuiIdentity);
117117 try {
118118 await h.ctrl("k");
119 await h.type("Connected origins");
119 await h.type("Origins");
120120 await h.press("enter");
121121 expect(listOrigins(forge.db, made.value.id)[0]!.lastError).toBe("");
122122 await h.press("s");
@@ -167,7 +167,7 @@ describe("connected origins in the terminal", () => {
167167 const h = boot({ ...alice(), user: { ...getUserByName(forge.db, "alice")!, kind: "agent" } } as TuiIdentity);
168168 try {
169169 await h.ctrl("k");
170 await h.type("Connected origins");
170 await h.type("Origins");
171171 expect(h.text()).not.toContain("the hosts your mirrors come from");
172172 } finally {
173173 h.stop();
@@ -200,7 +200,7 @@ describe("connected origins in the terminal", () => {
200200 const h = boot({ sshUsername: "git", remoteAddress: "127.0.0.1", fingerprint: "SHA256:unknown" });
201201 try {
202202 await h.ctrl("k");
203 await h.type("Connected origins");
203 await h.type("Origins");
204204 expect(h.text()).not.toContain("the hosts your mirrors come from");
205205 } finally {
206206 h.stop();
test/web-origins.test.ts +2 −2
@@ -232,7 +232,7 @@ describe("origins on the web", () => {
232232
233233 test("the origins page starts empty and offers the three ways in", async () => {
234234 const html = await (await asUser(alice, "/settings/origins")).text();
235 expect(html).toContain("No origins connected");
235 expect(html).toContain("No origins yet");
236236 expect(html).toContain("Forgejo / Gitea, one click");
237237 // Another forge leads (#9388); the rest are folded.
238238 expect(html).toContain("Another forge");
@@ -363,7 +363,7 @@ describe("origins on the web", () => {
363363
364364 test("another user cannot see or act on that origin", async () => {
365365 const html = await (await asUser(bob, "/settings/origins")).text();
366 expect(html).toContain("No origins connected");
366 expect(html).toContain("No origins yet");
367367 const id = listOrigins(app.db, getUserByName(app.db, "alice")!.id)[0]!.id;
368368 const stolen = await asUser(bob, `/settings/origins/${id}/repos`);
369369 expect(stolen.status).toBe(404);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs