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

The terminal's Adapter screen runs a pull now with one key #10146

Merged opened this

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

Merged

mason/forge:mason/10126-the-terminal-s-adapter-screen-runs-a-pul → main 2 commits fork

  • Merged. mason/10126-the-terminal-s-adapter-screen-runs-a-pul was merged into main as 9a64fe3.

Review

Sign in to review this change.
3 files changed +83 −5
docs/features/adapters-card.md +3 −1
@@ -9,7 +9,9 @@ when), the **next pull** and the **cursor** of the last good pull.
99 (`canSeeOwnerFacts`) get it.
1010 - **Terminal:** open the repository, press the palette key and choose
1111 "<owner>/<repo> — Adapter" (`src/tui/screens/adapter.ts`). Same gate, same
12 rows.
12 rows. A writer presses `p` there to pull now (`pullNow`): the rows are
13 read again, and a refusal shows its `PULL_NOW_REFUSALS` sentence in the
14 status line. Anyone else is shown no key and `p` does nothing.
1315 - Both list the **last ten pull runs** under the facts (`pullRunLines`,
1416 limit 10): when, ok or failed, written/skipped/failed and, for a failed
1517 run, its error sentence. With no runs, one muted line says "No pull runs
src/tui/screens/adapter.ts +32 −4
@@ -3,12 +3,17 @@
33 * (`web/studio/adapter.ts`), read from the same `adapterFactsOf` and
44 * gated the same way — owner facts (`canSeeOwnerFacts`) only. The palette
55 * offers it while a repository with an app is open; anyone else is offered
6 * nothing and, were the screen reached, sees no rows. Read-only.
6 * nothing and, were the screen reached, sees no rows. A writer presses `p`
7 * to run a pull now (`pullNow`, #10126): the rows are read again, a refusal
8 * is told in `PULL_NOW_REFUSALS`' words. Anyone else is shown no key and
9 * `p` does nothing.
710 */
811
12 import { pullNow, PULL_NOW_REFUSALS } from "../../actions/app_pull.ts";
13 import { binding } from "../app/keys.ts";
914 import { NO_RUNS_WORDS, adapterFactsOf, adapterRows, adapterRunRows, type AdapterFacts } from "../../apps/adapter_facts.ts";
1015 import { getAppByRepo, type AppRecord } from "../../core/apps.ts";
11 import { canSeeOwnerFacts, type Repo } from "../../core/repos.ts";
16 import { canSeeOwnerFacts, canWrite, type Repo } from "../../core/repos.ts";
1217 import type { Ctx, Screen } from "../app/app.ts";
1318 import type { Command } from "../app/palette.ts";
1419 import type { Session } from "../session.ts";
@@ -37,7 +42,10 @@ export function adapterScreen(
3742 ctx: Ctx,
3843 repo: Repo,
3944 facts: (record: AppRecord) => Promise<AdapterFacts | undefined> = (record) => adapterFactsOf(f.app, record),
45 pull: typeof pullNow = pullNow,
4046 ): Screen {
47 const mayPull = (): boolean => f.user !== undefined && canWrite(f.db, repo, f.user);
48 let pulling = false;
4149 let rows: Array<[string, string]> | undefined;
4250 let loaded = false;
4351 const load = (): void => {
@@ -68,7 +76,9 @@ export function adapterScreen(
6876 load();
6977 return {
7078 title: `${repo.ownerName}/${repo.name} — Adapter`,
71 keys: [],
79 get keys() {
80 return mayPull() ? [binding(["p"], "pull now")] : [];
81 },
7282 render(area, s) {
7383 const c = ctx.theme.c;
7484 if (rows === undefined) {
@@ -78,6 +88,24 @@ export function adapterScreen(
7888 }
7989 drawFacts(s, area, c, rows);
8090 },
81 key: () => false,
91 key(e) {
92 if (e.key !== "p" || !mayPull()) return false;
93 if (pulling) return true;
94 pulling = true;
95 void pull(f.app, f.user, repo).then(
96 (done) => {
97 pulling = false;
98 if (done.ok) {
99 ctx.toast(`pulled: ${done.value.state}`, "success");
100 load();
101 } else ctx.toast(PULL_NOW_REFUSALS[done.error], "danger");
102 },
103 () => {
104 pulling = false;
105 ctx.toast("The pull could not be run.", "danger");
106 },
107 );
108 return true;
109 },
82110 };
83111 }
test/adapter-tui.test.ts +48
@@ -14,6 +14,10 @@ import type { Repo } from "../src/core/repos.ts";
1414 import { createUser, type User } from "../src/core/users.ts";
1515 import { recordPull } from "../src/apps/pull.ts";
1616 import { shownRuns, type AdapterFacts } from "../src/apps/adapter_facts.ts";
17 import { PULL_NOW_REFUSALS } from "../src/actions/app_pull.ts";
18 import type { Ctx } from "../src/tui/app/app.ts";
19 import type { Session } from "../src/tui/session.ts";
20 import { theme } from "../src/tui/ui/theme.ts";
1721 import { createForgeApp } from "../src/tui/forge.ts";
1822 import { adapterCommands, adapterScreen } from "../src/tui/screens/adapter.ts";
1923 import { headlessTerminal } from "../src/tui/term/terminal.ts";
@@ -124,3 +128,47 @@ test("a guest gets no entry, and the screen reads nothing", async () => {
124128 expect(seen.text).not.toContain("c-secret-9");
125129 expect(seen.text).not.toContain("upstream said no");
126130 });
131
132 // Pull now (#10126): one key for a writer, none for anyone else.
133 function press(user: User, pull: Parameters<typeof adapterScreen>[4], reads: { n: number }) {
134 const toasts: string[] = [];
135 const ctx = {
136 theme: theme("dark"),
137 invalidate() {},
138 toast: (t: string) => toasts.push(t),
139 } as unknown as Ctx;
140 const f = { app, db: app.db, user } as unknown as Session;
141 const screen = adapterScreen(f, ctx, repo, async () => (reads.n++, facts), pull);
142 return { screen, toasts, send: (k: string) => screen.key({ key: k } as never) };
143 }
144 const settle = () => new Promise((r) => setTimeout(r, 10));
145
146 test("a writer sees the p key, pulls now, and the rows are read again", async () => {
147 const reads = { n: 0 };
148 const calls: string[] = [];
149 const run = { state: "ok" } as never;
150 const t = press(olive, async (_a, actor) => (calls.push(actor!.username), { ok: true, value: run }), reads);
151 expect(t.screen.keys.map((b) => b.keys[0])).toEqual(["p"]);
152 expect(t.send("p")).toBe(true);
153 await settle();
154 expect(calls).toEqual(["olive"]);
155 expect(reads.n).toBe(2);
156 expect(t.toasts).toEqual(["pulled: ok"]);
157 });
158
159 test("a refusal shows its sentence", async () => {
160 const t = press(olive, async () => ({ ok: false, error: "asleep" }), { n: 0 });
161 t.send("p");
162 await settle();
163 expect(t.toasts).toEqual([PULL_NOW_REFUSALS.asleep]);
164 });
165
166 test("a reader is shown no key and p does nothing", async () => {
167 const calls: number[] = [];
168 const t = press(otto, async () => (calls.push(1), { ok: false, error: "forbidden" }), { n: 0 });
169 expect(t.screen.keys).toEqual([]);
170 expect(t.send("p")).toBe(false);
171 await settle();
172 expect(calls).toEqual([]);
173 expect(t.toasts).toEqual([]);
174 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs