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

The terminal's People screen says when mail is not configured, so newcomers cannot verify themselves, as Settings › People does #7479

Merged opened this

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

Merged

torque/forge:torque/7474-the-terminal-s-people-screen-says-when-m → main 2 commits fork

  • Merged. torque/7474-the-terminal-s-people-screen-says-when-m was merged into main as 1c19d98.

Review

Sign in to review this change.
2 files changed +103 −2
src/tui/screens/people.ts +19 −2
@@ -5,10 +5,12 @@
55 * is the action's (`actions/roles.ts`). `s` suspends or lifts a suspension
66 * after a confirm, with the actions and words MCP `account_suspend` uses.
77 * `v` vouches for the email of the person under the cursor, after a y/n,
8 * with the action MCP `account_verify` uses.
8 * with the action MCP `account_verify` uses. Without mail a line above the
9 * list says so, as the accounts page's card does.
910 */
1011
1112 import { suspendAccount, unsuspendAccount, verifyAccount } from "../../actions/accounts.ts";
13 import { mailConfigured } from "../../actions/verify.ts";
1214 import { ADMIN_WORDS } from "../../actions/admin_words.ts";
1315 import { lastOwnerSays } from "../../core/orgs.ts";
1416 import { setRole } from "../../actions/roles.ts";
@@ -17,8 +19,16 @@ import { listPeople, type Person } from "../../core/roles.ts";
1719 import type { Ctx, Screen } from "../app/app.ts";
1820 import { binding } from "../app/keys.ts";
1921 import type { Session } from "../session.ts";
22 import { style } from "../term/style.ts";
23 import { truncate } from "../term/width.ts";
2024 import { confirmScreen, drawList, later, ListState, promptScreen, runAsync, type ListRow } from "./common.ts";
2125
26 /**
27 * The accounts page's no-mail card in one line that fits 80 columns: without
28 * mail a newcomer cannot prove an address, so the admin's word is the way in.
29 */
30 export const NO_MAIL_LINE = "Mail is not configured: verify newcomers here with v, or set MAIL_SMTP_URL.";
31
2232 /** One person's row: name, role, allowance, and since when they are suspended. */
2333 function rowOf(p: Person): string {
2434 const allowance = p.usdPerMonth === null ? "" : ` $${p.usdPerMonth.toFixed(2)} a month`;
@@ -28,6 +38,8 @@ function rowOf(p: Person): string {
2838
2939 export function peopleScreen(f: Session, ctx: Ctx): Screen {
3040 const list = new ListState();
41 // Read once per open: the answer lists the forge's secrets.
42 const noMail = !mailConfigured(f.app);
3143 const verify = (name: string): void => {
3244 const done = verifyAccount(f.app, f.user, name);
3345 if (done.ok) ctx.toast(`${done.value.username}'s email is verified.`);
@@ -75,7 +87,12 @@ export function peopleScreen(f: Session, ctx: Ctx): Screen {
7587 title: rowOf(p),
7688 ...(p.verified ? {} : { meta: "unverified" }),
7789 }));
78 drawList(s, area, ctx.theme.c, list, rows);
90 const c = ctx.theme.c;
91 if (noMail && area.h > 1) {
92 s.text(area.x, area.y, truncate(NO_MAIL_LINE, area.w), style(c.attn, c.bg), area.x + area.w);
93 area = { ...area, y: area.y + 2, h: Math.max(0, area.h - 2) };
94 }
95 drawList(s, area, c, list, rows);
7996 },
8097 key(e) {
8198 const people = listPeople(f.app.db);
test/tui-people-mail.test.ts added +84
@@ -0,0 +1,84 @@
1 /** The People screen says, above the list, when mail is not configured — as the accounts page does. */
2
3 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
4 import { mkdtemp, rm } from "node:fs/promises";
5 import { tmpdir } from "node:os";
6 import { join } from "node:path";
7 import { createApp, type App as Forge } from "../src/app.ts";
8 import { type User } from "../src/core/users.ts";
9 import { createForgeApp } from "../src/tui/forge.ts";
10 import { NO_MAIL_LINE } from "../src/tui/screens/people.ts";
11 import { headlessTerminal } from "../src/tui/term/terminal.ts";
12 import { quietLogs } from "./helpers/globals.ts";
13 import { settleApp, settleUntil } from "./helpers/tui.ts";
14 import { seedAdmin } from "./helpers/admin.ts";
15
16 quietLogs();
17
18 async function openPeople(forge: Forge, user: User) {
19 const terminal = headlessTerminal(80, 30);
20 const { app } = createForgeApp(forge, { sshUsername: user.username, remoteAddress: "127.0.0.1", fingerprint: "SHA256:test", user }, { terminal, exitHooks: false });
21 app.start();
22 const press = async (...keys: string[]): Promise<void> => {
23 for (const key of keys) app.dispatch({ type: "key", key, ctrl: false, alt: false, shift: false });
24 await settleApp(app);
25 };
26 await settleApp(app);
27 await press("ctrl+k");
28 for (const ch of "People") await press(ch);
29 await press("enter");
30 await settleUntil(app, "alice");
31 return { app, text: () => app.screen.toText() };
32 }
33
34 describe("the People screen and mail", () => {
35 const dirs: string[] = [];
36 afterAll(async () => {
37 for (const d of dirs) await rm(d, { recursive: true, force: true });
38 });
39
40 async function forgeWith(mail: boolean): Promise<{ forge: Forge; alice: User }> {
41 const dataDir = await mkdtemp(join(tmpdir(), "forge-tui-people-mail-"));
42 dirs.push(dataDir);
43 const forge = createApp({ dataDir, port: 0, ...(mail ? { mailer: async () => true } : {}) });
44 const a = await seedAdmin(forge, { username: "alice", email: "[email protected]", password: "a-long-passphrase" });
45 if (!a.ok) throw new Error("signup failed");
46 return { forge, alice: a.value };
47 }
48
49 let without: { forge: Forge; alice: User };
50 let withMail: { forge: Forge; alice: User };
51 beforeAll(async () => {
52 without = await forgeWith(false);
53 withMail = await forgeWith(true);
54 });
55
56 test("the line fits an 80-column terminal and names the key and the secret", () => {
57 expect(NO_MAIL_LINE.length).toBeLessThanOrEqual(80);
58 expect(NO_MAIL_LINE).toContain("with v");
59 expect(NO_MAIL_LINE).toContain("MAIL_SMTP_URL");
60 });
61
62 test("without mail the line is drawn once, above the list", async () => {
63 const h = await openPeople(without.forge, without.alice);
64 try {
65 const lines = h.text().split("\n");
66 const at = lines.findIndex((l) => l.includes(NO_MAIL_LINE));
67 expect(at).toBeGreaterThanOrEqual(0);
68 expect(lines.filter((l) => l.includes("Mail is not configured")).length).toBe(1);
69 expect(lines.findLastIndex((l) => l.includes("alice"))).toBeGreaterThan(at);
70 } finally {
71 h.app.stop();
72 }
73 });
74
75 test("with mail configured nothing is drawn", async () => {
76 const h = await openPeople(withMail.forge, withMail.alice);
77 try {
78 expect(h.text()).toContain("alice");
79 expect(h.text()).not.toContain("Mail is not configured");
80 } finally {
81 h.app.stop();
82 }
83 });
84 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs