gentle-shell-bridge 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/docs/contract.md +1 -1
- package/package.json +13 -1
- package/src/api.ts +1 -1
- package/.codegraph/.gitignore +0 -5
- package/PRD.md +0 -144
- package/examples/bash-card.ts +0 -1694
- package/gentle-shell-bridge-0.3.0.tgz +0 -0
- package/odd/tasks/palette-sections.md +0 -30
- package/odd/tasks/plugin-keymap.md +0 -32
- package/odd/tasks/plugin-surface-contract.md +0 -55
- package/test/api.test.ts +0 -395
- package/test/apply-on-move.test.ts +0 -477
- package/test/arm.test.ts +0 -220
- package/test/bridge-ready.test.ts +0 -309
- package/test/bus.test.ts +0 -165
- package/test/button-rows.test.ts +0 -409
- package/test/capture.test.ts +0 -356
- package/test/card-style-rows.test.ts +0 -510
- package/test/card-style-transform.test.ts +0 -379
- package/test/card-style-wiring.test.ts +0 -258
- package/test/card-styles-e2e.test.ts +0 -491
- package/test/card-styles.test.ts +0 -324
- package/test/card-transcript-path.test.ts +0 -178
- package/test/config.test.ts +0 -93
- package/test/e2e.test.ts +0 -443
- package/test/example-bash-identity.test.ts +0 -372
- package/test/example-bash-matrix.test.ts +0 -856
- package/test/example-designs.test.ts +0 -734
- package/test/example-neon-transform.test.ts +0 -283
- package/test/example-plain-style.test.ts +0 -1268
- package/test/extension.test.ts +0 -160
- package/test/fake-events.ts +0 -58
- package/test/fake-host.ts +0 -211
- package/test/fake-sidebar.ts +0 -516
- package/test/fake-transcript.ts +0 -486
- package/test/frame-telemetry.test.ts +0 -417
- package/test/hook.test.ts +0 -196
- package/test/manifest.test.ts +0 -105
- package/test/new-category.test.ts +0 -979
- package/test/notices.test.ts +0 -328
- package/test/plugin-integration-lines.test.ts +0 -979
- package/test/plugin-surface.test.ts +0 -152
- package/test/presence.test.ts +0 -459
- package/test/rail-surfaces.test.ts +0 -457
- package/test/rail-width.test.ts +0 -423
- package/test/regression.test.ts +0 -483
- package/test/repaint-api.test.ts +0 -200
- package/test/reserved-categories.test.ts +0 -361
- package/test/row-labels.test.ts +0 -284
- package/test/row-names.test.ts +0 -473
- package/test/row-origin.test.ts +0 -395
- package/test/section-interaction.test.ts +0 -364
- package/test/section-panel.test.ts +0 -403
- package/test/sections-rows.test.ts +0 -291
- package/test/sidebar-backend.test.ts +0 -471
- package/test/sidebar-e2e.test.ts +0 -331
- package/test/sidebar-hover.test.ts +0 -276
- package/test/sidebar-independent.test.ts +0 -349
- package/test/sidebar-render.test.ts +0 -281
- package/test/sidebar-sections.test.ts +0 -245
- package/test/status-card-styles.test.ts +0 -201
- package/test/status-section-frame.test.ts +0 -181
- package/test/status-sections.test.ts +0 -243
- package/test/status.test.ts +0 -209
- package/test/store.test.ts +0 -192
- package/test/surface-repaint-cost.test.ts +0 -520
- package/test/tool-design-chain.test.ts +0 -483
- package/test/tool-design-cost.test.ts +0 -473
- package/test/tool-design-rows.test.ts +0 -359
- package/test/tool-designs.test.ts +0 -745
- package/test/transform-pipeline.test.ts +0 -465
- package/test/transforms-bus.test.ts +0 -233
- package/test/verifier-width-regressions.test.ts +0 -164
- package/tsconfig.json +0 -18
package/test/notices.test.ts
DELETED
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The in-TUI diagnostics channel.
|
|
3
|
-
*
|
|
4
|
-
* Pi renders every diagnostic inside its own report; text written to stdout/stderr at load time
|
|
5
|
-
* appears in the terminal around Pi's startup, outside the TUI that owns the screen. So the bridge
|
|
6
|
-
* never writes to the console: a message produced before a context exists is buffered here and
|
|
7
|
-
* flushed ONCE through `ctx.ui.notify`, and `/bridge-status` keeps reporting it afterwards.
|
|
8
|
-
*
|
|
9
|
-
* These tests pin the whole contract: the bounded buffer, the dedupe, the flush-once rule, the
|
|
10
|
-
* no-UI case, the status block, and — the reason the module exists — that loading the entry, the
|
|
11
|
-
* example and an armed session produce ZERO console writes.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
|
|
17
|
-
import { BRIDGE_API_KEY, resetBridgeApiForTests } from "../src/api.ts";
|
|
18
|
-
import { resetBusForTests } from "../src/bus.ts";
|
|
19
|
-
import { resetCaptureStateForTests } from "../src/capture.ts";
|
|
20
|
-
import gentleShellBridge, {
|
|
21
|
-
type BridgeExtensionApi,
|
|
22
|
-
type BridgeSessionHandler,
|
|
23
|
-
} from "../src/extension.ts";
|
|
24
|
-
import { resetHookStateForTests } from "../src/hook.ts";
|
|
25
|
-
import {
|
|
26
|
-
MAX_NOTICES,
|
|
27
|
-
flushNotices,
|
|
28
|
-
recordNotice,
|
|
29
|
-
resetNoticesForTests,
|
|
30
|
-
snapshotNotices,
|
|
31
|
-
} from "../src/notices.ts";
|
|
32
|
-
import { resetRowBuildStateForTests } from "../src/rows.ts";
|
|
33
|
-
import { disableSidebarBackend, resetSidebarStateForTests } from "../src/sidebar.ts";
|
|
34
|
-
import { resetSectionFailuresForTests } from "../src/sidebar-render.ts";
|
|
35
|
-
import { collectStatus, formatStatus } from "../src/status.ts";
|
|
36
|
-
import { resetStoreStateForTests } from "../src/store.ts";
|
|
37
|
-
import { createFakeHost } from "./fake-host.ts";
|
|
38
|
-
import { resetSidebar } from "./fake-sidebar.ts";
|
|
39
|
-
|
|
40
|
-
const hostSlot = (): Record<symbol, unknown> => globalThis as unknown as Record<symbol, unknown>;
|
|
41
|
-
|
|
42
|
-
interface ConsoleCapture {
|
|
43
|
-
writes: string[];
|
|
44
|
-
restore: () => void;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Record every terminal write the process would show. */
|
|
48
|
-
const captureConsole = (): ConsoleCapture => {
|
|
49
|
-
const writes: string[] = [];
|
|
50
|
-
const sink = (globalThis as unknown as { console: { log: unknown; warn: unknown; error: unknown } })
|
|
51
|
-
.console;
|
|
52
|
-
const original = { log: sink.log, warn: sink.warn, error: sink.error };
|
|
53
|
-
const record =
|
|
54
|
-
(method: string) =>
|
|
55
|
-
(...args: unknown[]): void => {
|
|
56
|
-
writes.push(`${method}: ${args.map((arg) => String(arg)).join(" ")}`);
|
|
57
|
-
};
|
|
58
|
-
sink.log = record("log");
|
|
59
|
-
sink.warn = record("warn");
|
|
60
|
-
sink.error = record("error");
|
|
61
|
-
return {
|
|
62
|
-
writes,
|
|
63
|
-
restore: () => {
|
|
64
|
-
sink.log = original.log;
|
|
65
|
-
sink.warn = original.warn;
|
|
66
|
-
sink.error = original.error;
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
interface RecordingUi {
|
|
72
|
-
notify: (message: string, level?: "info" | "warning" | "error") => void;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const recordingUi = (): { ui: RecordingUi; calls: Array<[string, string | undefined]> } => {
|
|
76
|
-
const calls: Array<[string, string | undefined]> = [];
|
|
77
|
-
return {
|
|
78
|
-
ui: { notify: (message, level) => calls.push([message, level]) },
|
|
79
|
-
calls,
|
|
80
|
-
};
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const entryHandlers = (): BridgeSessionHandler[] => {
|
|
84
|
-
const handlers: BridgeSessionHandler[] = [];
|
|
85
|
-
const pi: BridgeExtensionApi = {
|
|
86
|
-
// `gentle-pi` is loaded, so the runtime gate is open and the buffered notice is flushed.
|
|
87
|
-
getCommands: () => [{ name: "gentle:customize" }],
|
|
88
|
-
on(_event, handler) {
|
|
89
|
-
handlers.push(handler);
|
|
90
|
-
},
|
|
91
|
-
registerCommand() {},
|
|
92
|
-
};
|
|
93
|
-
gentleShellBridge(pi);
|
|
94
|
-
return handlers;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
test.beforeEach(() => {
|
|
98
|
-
resetBridgeApiForTests();
|
|
99
|
-
resetNoticesForTests();
|
|
100
|
-
resetBusForTests();
|
|
101
|
-
resetSidebar();
|
|
102
|
-
resetSidebarStateForTests();
|
|
103
|
-
resetSectionFailuresForTests();
|
|
104
|
-
resetStoreStateForTests();
|
|
105
|
-
resetCaptureStateForTests();
|
|
106
|
-
resetHookStateForTests();
|
|
107
|
-
resetRowBuildStateForTests();
|
|
108
|
-
disableSidebarBackend();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
// ---------------------------------------------------------------------------------------------
|
|
112
|
-
// The bounded buffer
|
|
113
|
-
// ---------------------------------------------------------------------------------------------
|
|
114
|
-
|
|
115
|
-
test("a notice is buffered as pending, with its level", () => {
|
|
116
|
-
recordNotice("the slot holds a foreign object", "warning");
|
|
117
|
-
|
|
118
|
-
assert.deepEqual(snapshotNotices(), {
|
|
119
|
-
entries: [{ message: "the slot holds a foreign object", level: "warning", emitted: false }],
|
|
120
|
-
dropped: 0,
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
test("the default level is warning: every notice here is a problem report", () => {
|
|
125
|
-
recordNotice("no registerToolRenderer");
|
|
126
|
-
|
|
127
|
-
assert.equal(snapshotNotices().entries[0].level, "warning");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("an identical message is recorded once, not once per load", () => {
|
|
131
|
-
recordNotice("same fact", "warning");
|
|
132
|
-
recordNotice("same fact", "warning");
|
|
133
|
-
recordNotice("same fact", "error");
|
|
134
|
-
|
|
135
|
-
assert.deepEqual(
|
|
136
|
-
snapshotNotices().entries.map((entry) => [entry.message, entry.level]),
|
|
137
|
-
[
|
|
138
|
-
["same fact", "warning"],
|
|
139
|
-
["same fact", "error"],
|
|
140
|
-
],
|
|
141
|
-
);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test("the buffer is bounded and counts what it had to drop", () => {
|
|
145
|
-
for (let index = 0; index < MAX_NOTICES + 3; index += 1) recordNotice(`notice ${index}`);
|
|
146
|
-
|
|
147
|
-
const snapshot = snapshotNotices();
|
|
148
|
-
assert.equal(snapshot.entries.length, MAX_NOTICES, "never unbounded");
|
|
149
|
-
assert.equal(snapshot.dropped, 3, "and the loss is reported, not silent");
|
|
150
|
-
assert.equal(snapshot.entries[0].message, "notice 3", "the newest messages survive");
|
|
151
|
-
assert.equal(snapshot.entries.at(-1)?.message, `notice ${MAX_NOTICES + 2}`);
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("delivered entries are evicted before a pending one, and nothing is dropped", () => {
|
|
155
|
-
for (let index = 0; index < MAX_NOTICES; index += 1) recordNotice(`delivered ${index}`);
|
|
156
|
-
flushNotices(recordingUi().ui);
|
|
157
|
-
recordNotice("still pending");
|
|
158
|
-
recordNotice("later 1");
|
|
159
|
-
recordNotice("later 2");
|
|
160
|
-
|
|
161
|
-
const snapshot = snapshotNotices();
|
|
162
|
-
assert.equal(snapshot.entries.length, MAX_NOTICES);
|
|
163
|
-
assert.equal(snapshot.dropped, 0, "only already-delivered entries made room");
|
|
164
|
-
assert.equal(
|
|
165
|
-
snapshot.entries.some((entry) => entry.message === "still pending"),
|
|
166
|
-
true,
|
|
167
|
-
"a message the user never saw is never the first to go",
|
|
168
|
-
);
|
|
169
|
-
assert.equal(
|
|
170
|
-
snapshot.entries.some((entry) => entry.message === "delivered 0"),
|
|
171
|
-
false,
|
|
172
|
-
"the oldest delivered notice made the room",
|
|
173
|
-
);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
// ---------------------------------------------------------------------------------------------
|
|
177
|
-
// The flush: once, through the UI, and only when a UI exists
|
|
178
|
-
// ---------------------------------------------------------------------------------------------
|
|
179
|
-
|
|
180
|
-
test("the first UI session delivers every pending notice exactly once", () => {
|
|
181
|
-
const { ui, calls } = recordingUi();
|
|
182
|
-
recordNotice("first", "warning");
|
|
183
|
-
recordNotice("second", "error");
|
|
184
|
-
|
|
185
|
-
assert.equal(flushNotices(ui), 2);
|
|
186
|
-
assert.equal(flushNotices(ui), 0, "a second session_start never repeats a delivered message");
|
|
187
|
-
assert.deepEqual(calls, [
|
|
188
|
-
["first", "warning"],
|
|
189
|
-
["second", "error"],
|
|
190
|
-
]);
|
|
191
|
-
assert.deepEqual(
|
|
192
|
-
snapshotNotices().entries.map((entry) => entry.emitted),
|
|
193
|
-
[true, true],
|
|
194
|
-
"and they stay reported for /bridge-status",
|
|
195
|
-
);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test("a session without a UI flushes nothing and keeps the message pending", () => {
|
|
199
|
-
recordNotice("waiting for a UI");
|
|
200
|
-
|
|
201
|
-
assert.equal(flushNotices(undefined), 0);
|
|
202
|
-
assert.equal(flushNotices({}), 0, "a context without notify cannot deliver anything");
|
|
203
|
-
assert.deepEqual(
|
|
204
|
-
snapshotNotices().entries.map((entry) => entry.emitted),
|
|
205
|
-
[false],
|
|
206
|
-
"nothing was marked delivered",
|
|
207
|
-
);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("a notify that throws leaves the message pending instead of losing it", () => {
|
|
211
|
-
recordNotice("retry me");
|
|
212
|
-
|
|
213
|
-
assert.doesNotThrow(() =>
|
|
214
|
-
flushNotices({
|
|
215
|
-
notify: () => {
|
|
216
|
-
throw new Error("ui bug");
|
|
217
|
-
},
|
|
218
|
-
}),
|
|
219
|
-
);
|
|
220
|
-
assert.equal(snapshotNotices().entries[0].emitted, false);
|
|
221
|
-
|
|
222
|
-
const { ui, calls } = recordingUi();
|
|
223
|
-
assert.equal(flushNotices(ui), 1, "the next session delivers it");
|
|
224
|
-
assert.deepEqual(calls, [["retry me", "warning"]]);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
// ---------------------------------------------------------------------------------------------
|
|
228
|
-
// /bridge-status keeps the information
|
|
229
|
-
// ---------------------------------------------------------------------------------------------
|
|
230
|
-
|
|
231
|
-
test("status reports a pending notice, and the same notice once delivered", () => {
|
|
232
|
-
recordNotice("incompatible bridge object", "warning");
|
|
233
|
-
|
|
234
|
-
let lines = formatStatus(collectStatus());
|
|
235
|
-
assert.ok(lines.includes("notices: 1"));
|
|
236
|
-
assert.ok(lines.includes(" - warning (pending): incompatible bridge object"));
|
|
237
|
-
|
|
238
|
-
flushNotices(recordingUi().ui);
|
|
239
|
-
|
|
240
|
-
lines = formatStatus(collectStatus());
|
|
241
|
-
assert.ok(lines.includes("notices: 1"), "still visible after the moment passed");
|
|
242
|
-
assert.ok(lines.includes(" - warning (delivered): incompatible bridge object"));
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
test("status reports dropped notices when the bound was reached", () => {
|
|
246
|
-
for (let index = 0; index < MAX_NOTICES + 2; index += 1) recordNotice(`notice ${index}`);
|
|
247
|
-
|
|
248
|
-
const snapshot = collectStatus();
|
|
249
|
-
assert.equal(snapshot.notices?.length, MAX_NOTICES);
|
|
250
|
-
assert.equal(snapshot.noticesDropped, 2);
|
|
251
|
-
assert.ok(formatStatus(snapshot).includes("notices dropped: 2"));
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("an empty notice state adds nothing to the status report", () => {
|
|
255
|
-
const snapshot = collectStatus();
|
|
256
|
-
|
|
257
|
-
assert.equal(snapshot.notices, undefined);
|
|
258
|
-
assert.equal(snapshot.noticesDropped, undefined);
|
|
259
|
-
assert.equal(
|
|
260
|
-
formatStatus(snapshot).some((line) => line.startsWith("notices")),
|
|
261
|
-
false,
|
|
262
|
-
"the pre-change output stays exactly the same",
|
|
263
|
-
);
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// ---------------------------------------------------------------------------------------------
|
|
267
|
-
// No console writes, ever
|
|
268
|
-
// ---------------------------------------------------------------------------------------------
|
|
269
|
-
|
|
270
|
-
test("loading a copy of the handshake with an incompatible slot writes nothing to the console", async () => {
|
|
271
|
-
resetBridgeApiForTests();
|
|
272
|
-
resetNoticesForTests();
|
|
273
|
-
hostSlot()[BRIDGE_API_KEY] = { schema: "someone-else/api", version: 9 };
|
|
274
|
-
|
|
275
|
-
const captured = captureConsole();
|
|
276
|
-
try {
|
|
277
|
-
await import(new URL("../src/api.ts?console-free-skew", import.meta.url).href);
|
|
278
|
-
} finally {
|
|
279
|
-
captured.restore();
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
assert.deepEqual(captured.writes, [], "the message must not land in the terminal");
|
|
283
|
-
const entries = snapshotNotices().entries;
|
|
284
|
-
assert.equal(entries.length, 1, "it is buffered instead");
|
|
285
|
-
assert.equal(entries[0].level, "warning");
|
|
286
|
-
assert.match(entries[0].message, /incompatible|not the|version/u);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
test("loading the extension entry writes nothing to the console", async () => {
|
|
290
|
-
const captured = captureConsole();
|
|
291
|
-
try {
|
|
292
|
-
await import(new URL("../src/extension.ts?console-free-load", import.meta.url).href);
|
|
293
|
-
} finally {
|
|
294
|
-
captured.restore();
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
assert.deepEqual(captured.writes, []);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test("an armed session flushes once through notify and writes nothing to the console", () => {
|
|
301
|
-
const host = createFakeHost();
|
|
302
|
-
const { ui, calls } = recordingUi();
|
|
303
|
-
const handlers = entryHandlers();
|
|
304
|
-
recordNotice("buffered before the UI existed", "warning");
|
|
305
|
-
|
|
306
|
-
const captured = captureConsole();
|
|
307
|
-
try {
|
|
308
|
-
handlers[0]({}, { hasUI: true, ui: { ...host.ui, ...ui } });
|
|
309
|
-
handlers[0]({}, { hasUI: true, ui: { ...host.ui, ...ui } });
|
|
310
|
-
} finally {
|
|
311
|
-
captured.restore();
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
assert.deepEqual(captured.writes, []);
|
|
315
|
-
assert.deepEqual(calls, [["buffered before the UI existed", "warning"]]);
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test("a no-UI session does not flush, and the same message arrives on the next UI session", () => {
|
|
319
|
-
const { ui, calls } = recordingUi();
|
|
320
|
-
const handlers = entryHandlers();
|
|
321
|
-
recordNotice("waiting for a UI", "warning");
|
|
322
|
-
|
|
323
|
-
handlers[0]({}, { hasUI: false });
|
|
324
|
-
assert.deepEqual(calls, [], "nothing can be shown without a UI");
|
|
325
|
-
|
|
326
|
-
handlers[0]({}, { hasUI: true, ui });
|
|
327
|
-
assert.deepEqual(calls, [["waiting for a UI", "warning"]], "flushed exactly once, later");
|
|
328
|
-
});
|