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

`U` in the terminal's workspace asks before it stops mirroring to the source, naming the source and saying there is no way back #7994

Merged opened this

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

Merged

torque/forge:torque/7986-u-in-the-terminal-s-workspace-asks-befor → main 1 commit fork

  • Merged. torque/7986-u-in-the-terminal-s-workspace-asks-befor was merged into main as ca695c7.

Review

Sign in to review this change.
3 files changed +173 −6
src/tui/screens/workspace.ts +27 −5
@@ -1881,12 +1881,34 @@ export function workspaceScreen(
18811881 admin() &&
18821882 getMirrorTarget(f.db, repo.id) !== undefined
18831883 ) {
1884 const stopped = stopMirroring(f.app, f.user, repo);
1885 ctx.toast(
1886 stopped.ok && stopped.value ? "stopped mirroring" : "not mirrored",
1887 stopped.ok && stopped.value ? "success" : "danger",
1884 const source = getMirrorTarget(f.db, repo.id)?.fullName ?? "";
1885 // The only way out of a mirror, and there is no way back in from
1886 // here: ask first, naming the repository and where it goes.
1887 ctx.push(
1888 confirmScreen(ctx, screen, {
1889 title: "Stop mirroring",
1890 body: [
1891 `Stop mirroring ${repo.ownerName}/${repo.name} to ${source}?`,
1892 "Nothing more is pushed there, and this cannot be",
1893 "turned back on from here.",
1894 ],
1895 actions: [
1896 { label: "Cancel", variant: "ghost" },
1897 { label: "Stop mirroring", variant: "danger" },
1898 ],
1899 selected: 0,
1900 width: 64,
1901 onChoose: (choice) => {
1902 if (choice !== 1) return;
1903 const stopped = stopMirroring(f.app, f.user, repo);
1904 ctx.toast(
1905 stopped.ok && stopped.value ? "stopped mirroring" : "not mirrored",
1906 stopped.ok && stopped.value ? "success" : "danger",
1907 );
1908 ctx.invalidate();
1909 },
1910 }),
18881911 );
1889 ctx.invalidate();
18901912 return true;
18911913 }
18921914 if (plain && e.key === "Y" && f.signedIn && repo !== undefined) {
test/tui-workspace-unmirror.test.ts added +144
@@ -0,0 +1,144 @@
1 /**
2 * `U` in the workspace is the only way out of a mirror, and nothing turns it
3 * back on from the terminal — so it asks first, naming the repository and
4 * the source, with Cancel chosen. Driven headlessly against a real forge.
5 */
6
7 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
8 import { mkdtemp, rm } from "node:fs/promises";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { commitToBranch, createRepo } from "../src/actions/repos.ts";
12 import { createApp, type App as Forge } from "../src/app.ts";
13 import { getMirrorTarget, setMirrorTarget } from "../src/core/migrate_mirror.ts";
14 import type { Repo } from "../src/core/repos.ts";
15 import { createUser, type User } from "../src/core/users.ts";
16 import { createForgeApp } from "../src/tui/forge.ts";
17 import { headlessTerminal } from "../src/tui/term/terminal.ts";
18 import { quietLogs } from "./helpers/globals.ts";
19 import { seedAdmin } from "./helpers/admin.ts";
20 import { settleApp, settleUntil } from "./helpers/tui.ts";
21
22 quietLogs();
23
24 describe("U in the workspace asks before it stops mirroring", () => {
25 let dataDir: string;
26 let forge: Forge;
27 let alice: User;
28 let bob: User;
29 let repo: Repo;
30
31 beforeAll(async () => {
32 dataDir = await mkdtemp(join(tmpdir(), "forge-tui-unmirror-"));
33 forge = createApp({ dataDir, port: 0 });
34 const a = await seedAdmin(forge, {
35 username: "alice",
36 email: "[email protected]",
37 password: "a-long-passphrase",
38 });
39 if (!a.ok) throw new Error("signup failed");
40 alice = a.value;
41 const b = await createUser(forge.db, {
42 origin: "seed",
43 trusted: true,
44 username: "bob",
45 email: "[email protected]",
46 password: "a-good-passphrase",
47 });
48 if (!b.ok) throw new Error(b.error);
49 bob = b.value;
50 const made = await createRepo(forge, alice, {
51 name: "lighthouse",
52 visibility: "public",
53 description: "a beacon",
54 });
55 if (!made.ok) throw new Error("repo failed");
56 repo = made.value;
57 await commitToBranch(forge, alice, repo, {
58 message: "seed",
59 files: [{ path: "README.md", content: "# Lighthouse\n" }],
60 });
61 // The origin row is not what is under test; the mirror target is.
62 forge.db.run("PRAGMA foreign_keys = OFF");
63 });
64
65 afterAll(async () => {
66 forge.db.run("PRAGMA foreign_keys = ON");
67 forge.close();
68 await rm(dataDir, { recursive: true, force: true });
69 });
70
71 const mirror = (): void =>
72 setMirrorTarget(forge.db, {
73 repoId: repo.id,
74 originId: "origin-none",
75 fullName: "octo/lighthouse",
76 cloneUrl: "https://example.test/octo/lighthouse.git",
77 tips: {},
78 });
79
80 async function workspace(user: User) {
81 const { app } = createForgeApp(
82 forge,
83 { sshUsername: user.username, remoteAddress: "127.0.0.1", fingerprint: "SHA256:test", user },
84 { terminal: headlessTerminal(120, 40), exitHooks: false },
85 );
86 app.start();
87 const press = async (...keys: string[]): Promise<void> => {
88 for (const key of keys)
89 app.dispatch({ type: "key", key, ctrl: false, alt: false, shift: false });
90 await settleApp(app);
91 };
92 await settleApp(app);
93 await press("/", ..."lighthouse", "enter", "enter");
94 await settleUntil(app, "alice/lighthouse");
95 return { app, press, text: () => app.screen.toText() };
96 }
97
98 test("U names the repository and the source, and Cancel leaves the mirror in place", async () => {
99 mirror();
100 const h = await workspace(alice);
101 try {
102 await h.press("U");
103 const dialog = h.text();
104 expect(dialog).toContain("Stop mirroring alice/lighthouse to octo/lighthouse?");
105 expect(dialog).toContain("cannot be");
106 expect(dialog).toContain("turned back on from here.");
107 // Cancel is chosen first: enter on the dialog as it opens keeps the mirror.
108 await h.press("enter");
109 expect(getMirrorTarget(forge.db, repo.id)).toBeDefined();
110 await h.press("U", "esc");
111 expect(getMirrorTarget(forge.db, repo.id)).toBeDefined();
112 expect(h.text()).toContain("Mirrored to octo/lighthouse");
113 } finally {
114 h.app.stop();
115 }
116 });
117
118 test("U then Stop mirroring removes the mirror target", async () => {
119 mirror();
120 const h = await workspace(alice);
121 try {
122 await h.press("U", "right", "enter");
123 expect(getMirrorTarget(forge.db, repo.id)).toBeUndefined();
124 expect(h.text()).toContain("stopped mirroring");
125 expect(h.text()).not.toContain("Mirrored to octo/lighthouse");
126 } finally {
127 h.app.stop();
128 }
129 });
130
131 test("a person who is not the repository's admin is not offered U", async () => {
132 mirror();
133 const h = await workspace(bob);
134 try {
135 await h.press("?");
136 expect(h.text()).not.toContain("stop mirroring to the source");
137 await h.press("esc", "U");
138 expect(h.text()).not.toContain("Stop mirroring alice/lighthouse");
139 expect(getMirrorTarget(forge.db, repo.id)).toBeDefined();
140 } finally {
141 h.app.stop();
142 }
143 });
144 });
test/tui-workspace.test.ts +2 −1
@@ -220,7 +220,8 @@ describe("the terminal workspace", () => {
220220 await h.settle();
221221 await open(h);
222222 expect(h.text()).toContain("Mirrored to octo/lighthouse, stopped:");
223 await h.press("U");
223 // U asks first (test/tui-workspace-unmirror.test.ts); choose Stop mirroring.
224 await h.press("U", "right", "enter");
224225 expect(getMirrorTarget(forge.db, repo.id)).toBeUndefined();
225226 expect(h.text()).not.toContain("Mirrored to octo/lighthouse");
226227 } finally {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs