Public: anyone can read this; the crew can change it.
The terminal's 'Working now' tests wait for the line they expect, so they pass beside the full gate #2487
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2482-the-terminal-s-working-now-tests-wait-fo → main 1 commit fork
1 file
changed +39
−13
test/tui-working-yours.test.ts +39 −13
| @@ -29,7 +29,16 @@ let alice: User; | ||
| 29 | 29 | let bob: User; |
| 30 | 30 | let repo: Repo; |
| 31 | 31 | |
| 32 | async function palette(user: User, keys: string[] = [":"]): Promise<string> { | |
| 32 | /** The longest a screen gets to show what an assertion reads. */ | |
| 33 | const SHOW_MS = 2000; | |
| 34 | ||
| 35 | /** | |
| 36 | * Open the palette as `user`, type `keys`, and return the screen once the | |
| 37 | * list has rendered: the search is no longer "searching…", and when the | |
| 38 | * caller expects a row (`shows`) that row is on screen. A negative assertion | |
| 39 | * made on the result therefore reads a drawn list, not an empty frame. | |
| 40 | */ | |
| 41 | async function palette(user: User, keys: string[], shows?: string): Promise<string> { | |
| 33 | 42 | const terminal = headlessTerminal(120, 40); |
| 34 | 43 | const { app: tui, session } = createForgeApp( |
| 35 | 44 | app, |
| @@ -43,11 +52,24 @@ async function palette(user: User, keys: string[] = [":"]): Promise<string> { | ||
| 43 | 52 | terminal.feed(k); |
| 44 | 53 | await settleApp(tui); |
| 45 | 54 | } |
| 46 | const text = tui.screen.toText(); | |
| 55 | const deadline = Date.now() + SHOW_MS; | |
| 56 | let text = tui.screen.toText(); | |
| 57 | while (Date.now() < deadline && (text.includes("searching…") || (shows !== undefined && !text.includes(shows)))) { | |
| 58 | await new Promise<void>((r) => setTimeout(r, 10)); | |
| 59 | await settleApp(tui); | |
| 60 | text = tui.screen.toText(); | |
| 61 | } | |
| 47 | 62 | tui.stop?.(); |
| 48 | 63 | return text; |
| 49 | 64 | } |
| 50 | 65 | |
| 66 | /** A live agent run of this test's own; `done()` finishes it. */ | |
| 67 | function liveRun(summary: string, issueId?: string): { id: string; done: () => void } { | |
| 68 | const run = createRun(app.db, { repoId: repo.id, kind: "agent", issueId, actorId: alice.id, summary }); | |
| 69 | claimRun(app.db, run.id); | |
| 70 | return { id: run.id, done: () => void finishRun(app.db, run.id, { state: "ok", summary: "done" }) }; | |
| 71 | } | |
| 72 | ||
| 51 | 73 | beforeAll(async () => { |
| 52 | 74 | dir = await mkdtemp(join(tmpdir(), "forge-tui-working-")); |
| 53 | 75 | app = createApp({ dataDir: dir, port: 0 }); |
| @@ -74,32 +96,36 @@ afterAll(async () => { | ||
| 74 | 96 | describe("working now in the terminal", () => { |
| 75 | 97 | test("shown while a run is live, gone once it finishes", async () => { |
| 76 | 98 | expect(await palette(bob, [":", "Working"])).not.toContain("Working now"); |
| 77 | const run = createRun(app.db, { repoId: repo.id, kind: "agent", actorId: alice.id, summary: "LIVEJOB sanding the door" }); | |
| 78 | claimRun(app.db, run.id); | |
| 79 | expect(await palette(bob, [":", "Working"])).toContain("Working now: LIVEJOB sanding the door"); | |
| 80 | finishRun(app.db, run.id, { state: "ok", summary: "done" }); | |
| 99 | const run = liveRun("LIVEJOB sanding the door"); | |
| 100 | try { | |
| 101 | expect(await palette(bob, [":", "Working"], "LIVEJOB")).toContain("Working now: LIVEJOB sanding the door"); | |
| 102 | } finally { | |
| 103 | run.done(); | |
| 104 | } | |
| 81 | 105 | expect(await palette(bob, [":", "Working"])).not.toContain("Working now"); |
| 82 | 106 | }); |
| 83 | 107 | |
| 84 | 108 | test("a run on a sealed issue is hidden from bob and shown to its filer", async () => { |
| 85 | 109 | const sealed = createIssue(app.db, { repoId: repo.id, authorId: alice.id, title: "hole", sealed: true }); |
| 86 | 110 | if (!sealed.ok) throw new Error(sealed.error); |
| 87 | const run = createRun(app.db, { repoId: repo.id, kind: "agent", issueId: sealed.value.id, actorId: alice.id, summary: "SEALEDJOB" }); | |
| 88 | claimRun(app.db, run.id); | |
| 89 | expect(await palette(bob, [":", "Working"])).not.toContain("SEALEDJOB"); | |
| 90 | expect(await palette(alice, [":", "Working"])).toContain("SEALEDJOB"); | |
| 91 | finishRun(app.db, run.id, { state: "ok", summary: "done" }); | |
| 111 | const run = liveRun("SEALEDJOB", sealed.value.id); | |
| 112 | try { | |
| 113 | expect(await palette(alice, [":", "Working"], "SEALEDJOB")).toContain("SEALEDJOB"); | |
| 114 | expect(await palette(bob, [":", "Working"])).not.toContain("SEALEDJOB"); | |
| 115 | } finally { | |
| 116 | run.done(); | |
| 117 | } | |
| 92 | 118 | }); |
| 93 | 119 | }); |
| 94 | 120 | |
| 95 | 121 | describe("make it yours in the terminal", () => { |
| 96 | 122 | test("a writer does not see it; a reader who cannot write does", async () => { |
| 97 | 123 | expect(await palette(alice, [":", "Make it"])).not.toContain("Make it yours"); |
| 98 | expect(await palette(bob, [":", "Make it"])).toContain("alice/lobby — Make it yours"); | |
| 124 | expect(await palette(bob, [":", "Make it"], "Make it yours")).toContain("alice/lobby — Make it yours"); | |
| 99 | 125 | }); |
| 100 | 126 | |
| 101 | 127 | test("it opens the fork screen", async () => { |
| 102 | const text = await palette(bob, [":", "make it", "\r"]); | |
| 128 | const text = await palette(bob, [":", "make it", "\r"], "Fork"); | |
| 103 | 129 | expect(text).toContain("Fork"); |
| 104 | 130 | }); |
| 105 | 131 | }); |
new above
offline