gentle-shell-bridge 0.3.0 → 0.3.2
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/README.md +91 -722
- package/docs/contract.md +92 -35
- 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
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The sidebar section surface, proven against the fake host in `test/fake-sidebar.ts`.
|
|
3
|
-
*
|
|
4
|
-
* T1 (S9, S4): the fake host models the host's process-global rail state, its `parts` map and the
|
|
5
|
-
* card frame the layout draws from a rail's `cardGroups`. T2 (S1, S2): the section registry on the
|
|
6
|
-
* same process-global bus as the providers — one ordered snapshot, `id` replacement, a disposer
|
|
7
|
-
* that removes exactly one section.
|
|
8
|
-
*
|
|
9
|
-
* The first two tests are the scaffold's own: the fake is not the host, so it must be shown to
|
|
10
|
-
* behave like it (state slot, later registration, framed output) before any section test leans on
|
|
11
|
-
* it.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
registerRows,
|
|
19
|
-
registerSection,
|
|
20
|
-
resetBusForTests,
|
|
21
|
-
resetSectionsForTests,
|
|
22
|
-
snapshotProviders,
|
|
23
|
-
snapshotSections,
|
|
24
|
-
} from "../src/bus.ts";
|
|
25
|
-
import type { RowProvider, SidebarSection } from "../src/types.ts";
|
|
26
|
-
import {
|
|
27
|
-
SIDEBAR_STATE_KEY,
|
|
28
|
-
cardTitles,
|
|
29
|
-
fakeState,
|
|
30
|
-
registerNativeFooter,
|
|
31
|
-
renderSidebar,
|
|
32
|
-
resetSidebar,
|
|
33
|
-
} from "./fake-sidebar.ts";
|
|
34
|
-
|
|
35
|
-
const section = (id: string, extra: Partial<SidebarSection> = {}): SidebarSection => ({
|
|
36
|
-
id,
|
|
37
|
-
title: id.toUpperCase(),
|
|
38
|
-
lines: () => [`body ${id}`],
|
|
39
|
-
...extra,
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
const provider = (id: string): RowProvider => ({ id, category: "Layout", rows: () => [] });
|
|
43
|
-
|
|
44
|
-
const ids = (): string[] => snapshotSections().map((entry) => entry.id);
|
|
45
|
-
|
|
46
|
-
test.beforeEach(() => {
|
|
47
|
-
resetBusForTests();
|
|
48
|
-
resetSectionsForTests();
|
|
49
|
-
resetSidebar();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
test("the fake host carries the rail map on the host's process-global state symbol", () => {
|
|
53
|
-
assert.equal(SIDEBAR_STATE_KEY, Symbol.for("gentle-pi.experimental-sidebar.state"));
|
|
54
|
-
assert.equal(renderSidebar(40).join("\n"), "", "an empty host paints nothing");
|
|
55
|
-
|
|
56
|
-
registerNativeFooter();
|
|
57
|
-
const footer = fakeState().parts.get("footer");
|
|
58
|
-
assert.ok(footer !== undefined, "the native footer sits in the host's rail map");
|
|
59
|
-
assert.deepEqual(cardTitles(), ["FOOTER"], "the host draws the rail as one framed card");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("the host can register its footer rail after the first frame was painted", () => {
|
|
63
|
-
assert.deepEqual(cardTitles(), []);
|
|
64
|
-
registerNativeFooter();
|
|
65
|
-
assert.deepEqual(cardTitles(), ["FOOTER"]);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("registerSection returns a disposer that removes exactly that section", () => {
|
|
69
|
-
registerSection(section("a"));
|
|
70
|
-
const disposeB = registerSection(section("b"));
|
|
71
|
-
registerSection(section("c"));
|
|
72
|
-
|
|
73
|
-
assert.deepEqual(ids(), ["a", "b", "c"]);
|
|
74
|
-
|
|
75
|
-
disposeB();
|
|
76
|
-
|
|
77
|
-
assert.deepEqual(ids(), ["a", "c"], "the other sections keep their relative order");
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("registerSection stores the caller's object unchanged", () => {
|
|
81
|
-
const registered = section("a", {
|
|
82
|
-
title: "MUSIC",
|
|
83
|
-
meta: () => "1 playing",
|
|
84
|
-
lines: () => ["track -", "album -"],
|
|
85
|
-
defaultVisible: false,
|
|
86
|
-
order: 3,
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
registerSection(registered);
|
|
90
|
-
|
|
91
|
-
assert.equal(snapshotSections()[0], registered, "the snapshot hands back the registered object");
|
|
92
|
-
assert.equal(snapshotSections()[0].title, "MUSIC");
|
|
93
|
-
assert.equal(snapshotSections()[0].defaultVisible, false);
|
|
94
|
-
assert.equal(snapshotSections()[0].order, 3);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("a second registration with the same id replaces in place, keeping its position", () => {
|
|
98
|
-
registerSection(section("a"));
|
|
99
|
-
registerSection(section("b"));
|
|
100
|
-
registerSection(section("c"));
|
|
101
|
-
|
|
102
|
-
const replacement = section("b", { title: "REPLACED" });
|
|
103
|
-
registerSection(replacement);
|
|
104
|
-
|
|
105
|
-
assert.deepEqual(ids(), ["a", "b", "c"], "one entry, original position");
|
|
106
|
-
assert.equal(snapshotSections().length, 3);
|
|
107
|
-
assert.equal(snapshotSections()[1], replacement, "the new registration is the live one");
|
|
108
|
-
assert.equal(snapshotSections()[1].title, "REPLACED");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("the replaced section's disposer does not remove its replacement", () => {
|
|
112
|
-
const disposeFirst = registerSection(section("a"));
|
|
113
|
-
const replacement = section("a", { title: "REPLACED" });
|
|
114
|
-
registerSection(replacement);
|
|
115
|
-
|
|
116
|
-
disposeFirst();
|
|
117
|
-
|
|
118
|
-
assert.deepEqual(ids(), ["a"], "the stale disposer is a no-op, not a removal of the replacement");
|
|
119
|
-
assert.equal(snapshotSections()[0], replacement);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("a disposer called twice is a no-op the second time", () => {
|
|
123
|
-
registerSection(section("a"));
|
|
124
|
-
const disposeB = registerSection(section("b"));
|
|
125
|
-
registerSection(section("c"));
|
|
126
|
-
|
|
127
|
-
disposeB();
|
|
128
|
-
disposeB();
|
|
129
|
-
|
|
130
|
-
assert.deepEqual(ids(), ["a", "c"]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("sections with an order come first, ascending and ties by registration order", () => {
|
|
134
|
-
registerSection(section("un-ordered-first"));
|
|
135
|
-
registerSection(section("ten", { order: 10 }));
|
|
136
|
-
registerSection(section("one", { order: 1 }));
|
|
137
|
-
registerSection(section("tie-a", { order: 5 }));
|
|
138
|
-
registerSection(section("tie-b", { order: 5 }));
|
|
139
|
-
registerSection(section("un-ordered-second"));
|
|
140
|
-
|
|
141
|
-
assert.deepEqual(
|
|
142
|
-
ids(),
|
|
143
|
-
["one", "tie-a", "tie-b", "ten", "un-ordered-first", "un-ordered-second"],
|
|
144
|
-
"ordered sections sort ascending ahead of the unordered ones, which keep registration order",
|
|
145
|
-
);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
test("a replacement may move a section by changing its order", () => {
|
|
149
|
-
registerSection(section("a", { order: 2 }));
|
|
150
|
-
registerSection(section("b", { order: 1 }));
|
|
151
|
-
|
|
152
|
-
registerSection(section("a", { order: 0 }));
|
|
153
|
-
|
|
154
|
-
assert.deepEqual(ids(), ["a", "b"]);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("only numbered orders sort; a section without one goes to the tail", () => {
|
|
158
|
-
registerSection(section("zero", { order: 0 }));
|
|
159
|
-
registerSection(section("unordered", { order: undefined }));
|
|
160
|
-
registerSection(section("negative", { order: -3 }));
|
|
161
|
-
registerSection(section("not-a-number", { order: Number.NaN }));
|
|
162
|
-
|
|
163
|
-
assert.deepEqual(ids(), ["negative", "zero", "unordered", "not-a-number"]);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
test("a section snapshot is a stable copy taken at call time", () => {
|
|
167
|
-
registerSection(section("a"));
|
|
168
|
-
|
|
169
|
-
const during = snapshotSections();
|
|
170
|
-
const seen: string[] = [];
|
|
171
|
-
for (const entry of during) {
|
|
172
|
-
seen.push(entry.id);
|
|
173
|
-
registerSection(section("late"));
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
assert.deepEqual(seen, ["a"]);
|
|
177
|
-
assert.deepEqual(during.map((entry) => entry.id), ["a"]);
|
|
178
|
-
assert.deepEqual(ids(), ["a", "late"]);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("two independent module copies in one process share one section registry", async () => {
|
|
182
|
-
// A query string yields a second module instance with its own module scope — the closest local
|
|
183
|
-
// model of jiti's `moduleCache: false` loading two package copies.
|
|
184
|
-
const secondCopySpecifier = "../src/bus.ts?copy=2";
|
|
185
|
-
const copyTwo: typeof import("../src/bus.ts") = await import(secondCopySpecifier);
|
|
186
|
-
|
|
187
|
-
registerSection(section("from-copy-one"));
|
|
188
|
-
copyTwo.registerSection(section("from-copy-two"));
|
|
189
|
-
|
|
190
|
-
assert.deepEqual(ids(), ["from-copy-one", "from-copy-two"]);
|
|
191
|
-
assert.deepEqual(
|
|
192
|
-
copyTwo.snapshotSections().map((entry) => entry.id),
|
|
193
|
-
["from-copy-one", "from-copy-two"],
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
const dispose = copyTwo.registerSection(section("shared"));
|
|
197
|
-
assert.deepEqual(ids(), ["from-copy-one", "from-copy-two", "shared"]);
|
|
198
|
-
dispose();
|
|
199
|
-
assert.deepEqual(ids(), ["from-copy-one", "from-copy-two"], "the global registry, not a local one");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("the section list is reachable behind the same bus slot as the providers", () => {
|
|
203
|
-
registerSection(section("a"));
|
|
204
|
-
const slot = (globalThis as unknown as Record<symbol, unknown>)[
|
|
205
|
-
Symbol.for("gentle-shell-bridge.providers")
|
|
206
|
-
];
|
|
207
|
-
assert.ok(slot !== undefined, "the bus is the provider bus, not a second registry");
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test("a bus slot created before sections existed keeps its providers and grows a section list", () => {
|
|
211
|
-
// A copy of this package that predates sections writes `{ entries }` only. Reading that slot
|
|
212
|
-
// must not throw away the providers already registered in it.
|
|
213
|
-
const earlierProvider = provider("earlier");
|
|
214
|
-
const slot = globalThis as unknown as Record<symbol, unknown>;
|
|
215
|
-
slot[Symbol.for("gentle-shell-bridge.providers")] = { entries: [{ provider: earlierProvider }] };
|
|
216
|
-
|
|
217
|
-
registerSection(section("a"));
|
|
218
|
-
|
|
219
|
-
assert.deepEqual(ids(), ["a"], "the section list was added in place");
|
|
220
|
-
assert.deepEqual(
|
|
221
|
-
snapshotProviders().map((entry) => entry.id),
|
|
222
|
-
["earlier"],
|
|
223
|
-
"the providers the earlier copy registered are still there",
|
|
224
|
-
);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
test("resetSectionsForTests clears sections and leaves providers registered", () => {
|
|
228
|
-
registerRows(provider("p"));
|
|
229
|
-
registerSection(section("a"));
|
|
230
|
-
|
|
231
|
-
resetSectionsForTests();
|
|
232
|
-
|
|
233
|
-
assert.deepEqual(ids(), []);
|
|
234
|
-
assert.deepEqual(
|
|
235
|
-
snapshotProviders().map((entry) => entry.id),
|
|
236
|
-
["p"],
|
|
237
|
-
"provider state is not the section state",
|
|
238
|
-
);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test("the internal section seams are not on the public entry", async () => {
|
|
242
|
-
const entry = await import("../src/index.ts");
|
|
243
|
-
assert.equal("resetSectionsForTests" in entry, false);
|
|
244
|
-
assert.equal("snapshotSections" in entry, false);
|
|
245
|
-
});
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `/bridge-status` for the card-styles surface (S7, S12): the line mode, the registered styles in
|
|
3
|
-
* order, the live selection (including a stale one), what the last frame did, the per-style failures,
|
|
4
|
-
* and the one warning the bridge owes the user — a later post-processor can override this frame.
|
|
5
|
-
*
|
|
6
|
-
* The guard that matters most: a session with no card style registered and no card state prints the
|
|
7
|
-
* pre-feature report byte for byte, because every block below is emitted only when it has something
|
|
8
|
-
* to say.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
|
|
17
|
-
import { armBridge } from "../src/arm.ts";
|
|
18
|
-
import { registerCardStyle, resetBusForTests, resetCardStylesForTests } from "../src/bus.ts";
|
|
19
|
-
import { installCardStyleRows, resetCardStyleRowStateForTests } from "../src/card-style-rows.ts";
|
|
20
|
-
import { getCardStyleTransformFailures } from "../src/card-style-transform.ts";
|
|
21
|
-
import {
|
|
22
|
-
disableCardStyleTransform,
|
|
23
|
-
installCardStyleTransform,
|
|
24
|
-
resetCardStyleWiringForTests,
|
|
25
|
-
} from "../src/card-style-wiring.ts";
|
|
26
|
-
import { resetCardStyleTransformForTests } from "../src/card-style-transform.ts";
|
|
27
|
-
import { setCardStyleSelection } from "../src/card-styles.ts";
|
|
28
|
-
import { disableCaptureBackend, resetCaptureStateForTests } from "../src/capture.ts";
|
|
29
|
-
import { resetHookStateForTests } from "../src/hook.ts";
|
|
30
|
-
import { resetNoticesForTests } from "../src/notices.ts";
|
|
31
|
-
import { resetRowBuildStateForTests } from "../src/rows.ts";
|
|
32
|
-
import { disableSidebarBackend, resetSidebarStateForTests } from "../src/sidebar.ts";
|
|
33
|
-
import { resetSectionFailuresForTests } from "../src/sidebar-render.ts";
|
|
34
|
-
import { resetSectionsForTests } from "../src/bus.ts";
|
|
35
|
-
import { collectStatus, formatStatus } from "../src/status.ts";
|
|
36
|
-
import { resetStoreStateForTests } from "../src/store.ts";
|
|
37
|
-
import type { CardStyleRegistration } from "../src/types.ts";
|
|
38
|
-
import { createFakeHost } from "./fake-host.ts";
|
|
39
|
-
import { READ_CARD, createFakeTranscriptTui } from "./fake-transcript.ts";
|
|
40
|
-
|
|
41
|
-
let configHome: string;
|
|
42
|
-
let previousConfigHome: string | undefined;
|
|
43
|
-
|
|
44
|
-
test.before(() => {
|
|
45
|
-
configHome = mkdtempSync(join(tmpdir(), "gsb-status-cards-"));
|
|
46
|
-
previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
47
|
-
process.env.GENTLE_PI_CONFIG_HOME = configHome;
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test.after(() => {
|
|
51
|
-
if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
52
|
-
else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
|
|
53
|
-
rmSync(configHome, { recursive: true, force: true });
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test.beforeEach(() => {
|
|
57
|
-
rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
|
|
58
|
-
resetBusForTests();
|
|
59
|
-
resetSectionsForTests();
|
|
60
|
-
resetCardStylesForTests();
|
|
61
|
-
resetCardStyleRowStateForTests();
|
|
62
|
-
resetCardStyleTransformForTests();
|
|
63
|
-
resetCardStyleWiringForTests();
|
|
64
|
-
resetCaptureStateForTests();
|
|
65
|
-
resetHookStateForTests();
|
|
66
|
-
resetRowBuildStateForTests();
|
|
67
|
-
resetSidebarStateForTests();
|
|
68
|
-
resetSectionFailuresForTests();
|
|
69
|
-
resetNoticesForTests();
|
|
70
|
-
resetStoreStateForTests();
|
|
71
|
-
disableCaptureBackend();
|
|
72
|
-
disableSidebarBackend();
|
|
73
|
-
disableCardStyleTransform();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const style = (id: string, label: string, order?: number): CardStyleRegistration => ({
|
|
77
|
-
id,
|
|
78
|
-
label,
|
|
79
|
-
...(order === undefined ? {} : { order }),
|
|
80
|
-
frame: { top: () => "+", line: (ctx) => `> ${ctx.text}`, bottom: () => "-" },
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
const statusLines = (): string[] => formatStatus(collectStatus());
|
|
84
|
-
|
|
85
|
-
const cardBlock = (lines: readonly string[]): string[] => {
|
|
86
|
-
const start = lines.findIndex((line) => line.startsWith("card styles: "));
|
|
87
|
-
if (start === -1) return [];
|
|
88
|
-
const rest = lines.slice(start);
|
|
89
|
-
const end = rest.findIndex((line) => line.startsWith("card style note: "));
|
|
90
|
-
return end === -1 ? rest : rest.slice(0, end + 1);
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
test("a session with no style and no card state keeps the historical output byte for byte", () => {
|
|
94
|
-
assert.deepEqual(statusLines(), [
|
|
95
|
-
"gentle-shell-bridge status",
|
|
96
|
-
"providers: none",
|
|
97
|
-
"backend: none",
|
|
98
|
-
"last capture: none",
|
|
99
|
-
"last failures: none",
|
|
100
|
-
"store problems: none",
|
|
101
|
-
"duplicate settings: no build yet",
|
|
102
|
-
]);
|
|
103
|
-
assert.equal(collectStatus().cardStyles, undefined);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("the block reports mode, styles in order, selection, last frame and the downstream warning", () => {
|
|
107
|
-
const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
|
|
108
|
-
installCardStyleTransform(tui);
|
|
109
|
-
registerCardStyle(style("loon.cute", "cute", 10));
|
|
110
|
-
registerCardStyle(style("pix.mono", "mono"));
|
|
111
|
-
registerCardStyle({
|
|
112
|
-
id: "loon.broken",
|
|
113
|
-
label: "broken",
|
|
114
|
-
frame: {
|
|
115
|
-
top: () => {
|
|
116
|
-
throw new Error("top exploded");
|
|
117
|
-
},
|
|
118
|
-
line: (ctx) => ctx.text,
|
|
119
|
-
bottom: () => "-",
|
|
120
|
-
},
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
setCardStyleSelection("loon.broken");
|
|
124
|
-
tui.chat.render(40);
|
|
125
|
-
setCardStyleSelection("loon.cute");
|
|
126
|
-
tui.chat.render(40);
|
|
127
|
-
|
|
128
|
-
assert.deepEqual(
|
|
129
|
-
getCardStyleTransformFailures(),
|
|
130
|
-
[{ styleId: "loon.broken", message: "top exploded" }],
|
|
131
|
-
"the failing style is remembered",
|
|
132
|
-
);
|
|
133
|
-
assert.deepEqual(cardBlock(statusLines()), [
|
|
134
|
-
"card styles: separate, 3 registered",
|
|
135
|
-
' - loon.cute "cute" (order 10)',
|
|
136
|
-
' - pix.mono "mono" (registration order)',
|
|
137
|
-
' - loon.broken "broken" (registration order)',
|
|
138
|
-
'card style selection: loon.cute "cute"',
|
|
139
|
-
"card style frame: transformed 1 card",
|
|
140
|
-
"card style surface conversation: reached (chat wrapped), last frame transformed 1 card",
|
|
141
|
-
"card style surface rail: not reached (not attempted), last frame none",
|
|
142
|
-
"card style wiring: chat wrapped",
|
|
143
|
-
"card style failures: 1",
|
|
144
|
-
" - loon.broken: top exploded",
|
|
145
|
-
"card style note: a downstream post-processor can strip or override this frame; " +
|
|
146
|
-
"the bridge cannot prevent it.",
|
|
147
|
-
]);
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("a stale selection is reported as missing, never rendered (S12)", () => {
|
|
151
|
-
const tui = createFakeTranscriptTui({ lines: [...READ_CARD] });
|
|
152
|
-
installCardStyleTransform(tui);
|
|
153
|
-
const dispose = registerCardStyle(style("loon.cute", "cute"));
|
|
154
|
-
setCardStyleSelection("loon.cute");
|
|
155
|
-
tui.chat.render(40);
|
|
156
|
-
dispose();
|
|
157
|
-
|
|
158
|
-
tui.chat.render(40);
|
|
159
|
-
|
|
160
|
-
assert.deepEqual(cardBlock(statusLines()), [
|
|
161
|
-
"card styles: separate, 0 registered",
|
|
162
|
-
"card style selection: loon.cute (missing: not registered)",
|
|
163
|
-
"card style frame: pass-through (missing: a disposed style)",
|
|
164
|
-
"card style surface conversation: reached (chat wrapped), last frame pass-through (missing: a disposed style)",
|
|
165
|
-
"card style surface rail: not reached (not attempted), last frame none",
|
|
166
|
-
"card style wiring: chat wrapped",
|
|
167
|
-
"card style note: a downstream post-processor can strip or override this frame; " +
|
|
168
|
-
"the bridge cannot prevent it.",
|
|
169
|
-
]);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
test("an unarmed transform reports why, so a missing repaint has an answer", () => {
|
|
173
|
-
registerCardStyle(style("loon.cute", "cute"));
|
|
174
|
-
installCardStyleTransform({ terminal: {}, children: [] });
|
|
175
|
-
|
|
176
|
-
const lines = statusLines();
|
|
177
|
-
|
|
178
|
-
assert.ok(lines.includes("card style wiring: not wrapped (no-children)"));
|
|
179
|
-
assert.ok(lines.includes("card style frame: none"));
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test("the extended mode is reported once the host's own line was found", () => {
|
|
183
|
-
const host = createFakeHost({
|
|
184
|
-
rows: [
|
|
185
|
-
{ category: "Cards", label: "Card bash: builtin", action: () => {} },
|
|
186
|
-
{
|
|
187
|
-
category: "Cards",
|
|
188
|
-
label: () => "Card style: float",
|
|
189
|
-
action: () => {},
|
|
190
|
-
choice: { setting: "Card style", value: "float", current: () => true },
|
|
191
|
-
},
|
|
192
|
-
{ category: "Layout", label: "Layout: builtin", action: () => {} },
|
|
193
|
-
],
|
|
194
|
-
});
|
|
195
|
-
armBridge(host.ui);
|
|
196
|
-
installCardStyleRows();
|
|
197
|
-
registerCardStyle(style("loon.cute", "cute"));
|
|
198
|
-
host.buildMenu();
|
|
199
|
-
|
|
200
|
-
assert.ok(statusLines().includes("card styles: extended, 1 registered"));
|
|
201
|
-
});
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `/bridge-status` for the section frame (N11, N15): which drawing the header and rows use, whether
|
|
3
|
-
* the theme was captured, how the pointer reaches them, and — when the host's own builders cannot be
|
|
4
|
-
* reached — the gap, named.
|
|
5
|
-
*
|
|
6
|
-
* Every fact asserted here is a measurement of the run: the source comes from looking for the host's
|
|
7
|
-
* builders on the objects the bridge is handed, so the "host" case is proved by publishing them on a
|
|
8
|
-
* fake rail and watching the drawing change.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
13
|
-
import { tmpdir } from "node:os";
|
|
14
|
-
import { join } from "node:path";
|
|
15
|
-
import test from "node:test";
|
|
16
|
-
|
|
17
|
-
import { registerSection, resetBusForTests, resetSectionsForTests } from "../src/bus.ts";
|
|
18
|
-
import { setRepaintHost, resetRepaintForTests } from "../src/repaint.ts";
|
|
19
|
-
import { resetSectionPanelForTests, setSectionTheme } from "../src/section-panel.ts";
|
|
20
|
-
import { resetSectionPointerForTests } from "../src/section-pointer.ts";
|
|
21
|
-
import {
|
|
22
|
-
SIDEBAR_RAIL_KEY,
|
|
23
|
-
disableSidebarBackend,
|
|
24
|
-
installSidebarBackend,
|
|
25
|
-
resetSidebarStateForTests,
|
|
26
|
-
} from "../src/sidebar.ts";
|
|
27
|
-
import {
|
|
28
|
-
installSectionRail,
|
|
29
|
-
resetSectionFailuresForTests,
|
|
30
|
-
} from "../src/sidebar-render.ts";
|
|
31
|
-
import { collectStatus, formatStatus } from "../src/status.ts";
|
|
32
|
-
import { resetStoreStateForTests } from "../src/store.ts";
|
|
33
|
-
import type { SidebarSection } from "../src/types.ts";
|
|
34
|
-
import { setSectionCollapsed } from "../src/visibility.ts";
|
|
35
|
-
import {
|
|
36
|
-
fakeState,
|
|
37
|
-
fakeTerminal,
|
|
38
|
-
fakeTui,
|
|
39
|
-
registerNativeFooter,
|
|
40
|
-
renderSidebar,
|
|
41
|
-
resetSidebar,
|
|
42
|
-
} from "./fake-sidebar.ts";
|
|
43
|
-
|
|
44
|
-
let configHome: string;
|
|
45
|
-
let previousConfigHome: string | undefined;
|
|
46
|
-
|
|
47
|
-
test.before(() => {
|
|
48
|
-
configHome = mkdtempSync(join(tmpdir(), "gsb-frame-status-"));
|
|
49
|
-
previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
50
|
-
process.env.GENTLE_PI_CONFIG_HOME = configHome;
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test.after(() => {
|
|
54
|
-
if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
55
|
-
else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
|
|
56
|
-
rmSync(configHome, { recursive: true, force: true });
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test.beforeEach(() => {
|
|
60
|
-
rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
|
|
61
|
-
resetBusForTests();
|
|
62
|
-
resetSectionsForTests();
|
|
63
|
-
resetSidebar();
|
|
64
|
-
resetSidebarStateForTests();
|
|
65
|
-
resetSectionFailuresForTests();
|
|
66
|
-
resetSectionPanelForTests();
|
|
67
|
-
resetSectionPointerForTests();
|
|
68
|
-
resetRepaintForTests();
|
|
69
|
-
resetStoreStateForTests();
|
|
70
|
-
disableSidebarBackend();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const section = (id: string, extra: Partial<SidebarSection> = {}): SidebarSection => ({
|
|
74
|
-
id,
|
|
75
|
-
title: id.toUpperCase(),
|
|
76
|
-
lines: () => [`body ${id}`],
|
|
77
|
-
...extra,
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
function arm(): void {
|
|
81
|
-
setRepaintHost({ terminal: fakeTerminal(), requestRender: () => {} });
|
|
82
|
-
registerNativeFooter();
|
|
83
|
-
installSidebarBackend(() => fakeTui());
|
|
84
|
-
installSectionRail();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const frameLines = (): string[] => formatStatus(collectStatus()).filter((line) => line.startsWith("section "));
|
|
88
|
-
|
|
89
|
-
/** A theme shaped like Pi's, so the drawing has something to paint with. */
|
|
90
|
-
const theme = { fg: (role: string, text: string) => `<${role}>${text}`, bold: (text: string) => text };
|
|
91
|
-
|
|
92
|
-
test("with no section there is no frame block: a row-only session keeps its output", () => {
|
|
93
|
-
assert.deepEqual(frameLines(), []);
|
|
94
|
-
assert.equal(collectStatus().sectionFrame, undefined);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("the fallback reports what it looked for and found, and names the gap", () => {
|
|
98
|
-
registerSection(section("music"));
|
|
99
|
-
arm();
|
|
100
|
-
|
|
101
|
-
assert.deepEqual(frameLines(), [
|
|
102
|
-
"section frame: fallback (looked for renderPanelHeader/renderPanelRow on: the terminal sidebar state, the rail part; found none; no theme captured yet, rows drawn uncoloured)",
|
|
103
|
-
"section interaction: rail handleMouse (the host's own dispatch), hover not used yet",
|
|
104
|
-
"section frame gap: gentle-shell's lib/shell-panel.ts cannot be imported (Pi loads each extension in its own module graph) and publishes no builder on the rail part, the Pi theme or the terminal's sidebar state, so the header and rows are drawn by a byte-identical port",
|
|
105
|
-
]);
|
|
106
|
-
const frame = collectStatus().sectionFrame;
|
|
107
|
-
assert.equal(frame?.source, "fallback");
|
|
108
|
-
assert.deepEqual(frame?.lookedAt, ["the terminal sidebar state", "the rail part"]);
|
|
109
|
-
assert.equal(frame?.theme, "missing");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("a host that publishes its own builders is used, and reported as the source", () => {
|
|
113
|
-
setSectionTheme(theme);
|
|
114
|
-
registerSection(section("music"));
|
|
115
|
-
const rail = registerNativeFooter();
|
|
116
|
-
const host = rail as unknown as Record<string, unknown>;
|
|
117
|
-
host.renderPanelHeader = (_theme: unknown, _width: number, header: { title: string }) =>
|
|
118
|
-
`HOST HEADER ${header.title.toUpperCase()}`;
|
|
119
|
-
host.renderPanelRow = (_width: number, left: string) => `HOST ROW ${left}`;
|
|
120
|
-
installSidebarBackend(() => fakeTui());
|
|
121
|
-
installSectionRail();
|
|
122
|
-
|
|
123
|
-
assert.deepEqual(frameLines(), [
|
|
124
|
-
"section frame: host (the host's own renderPanelHeader/renderPanelRow, found on: the Pi theme, the terminal sidebar state, the rail part; theme captured, rows painted in the host's roles)",
|
|
125
|
-
"section interaction: rail handleMouse (the host's own dispatch), hover not used yet",
|
|
126
|
-
]);
|
|
127
|
-
assert.deepEqual(collectStatus().sectionFrame?.missing, []);
|
|
128
|
-
assert.deepEqual((fakeState().parts.get(SIDEBAR_RAIL_KEY)?.render(20) ?? []).slice(3), [
|
|
129
|
-
"HOST HEADER MUSIC",
|
|
130
|
-
// A plain string row stays the bytes the author wrote, even when the host draws structured rows.
|
|
131
|
-
"body music",
|
|
132
|
-
]);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test("the theme is reported captured once Pi hands it over", () => {
|
|
136
|
-
setSectionTheme(theme);
|
|
137
|
-
registerSection(section("music"));
|
|
138
|
-
arm();
|
|
139
|
-
|
|
140
|
-
assert.ok(
|
|
141
|
-
frameLines()[0].includes("theme captured, rows painted in the host's roles"),
|
|
142
|
-
frameLines()[0],
|
|
143
|
-
);
|
|
144
|
-
assert.equal(collectStatus().sectionFrame?.theme, "captured");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("a collapsed section says so in the sections table", () => {
|
|
148
|
-
registerSection(section("music", { title: "MUSIC" }));
|
|
149
|
-
arm();
|
|
150
|
-
setSectionCollapsed("music", true);
|
|
151
|
-
|
|
152
|
-
assert.deepEqual(
|
|
153
|
-
formatStatus(collectStatus()).filter((line) => line.includes('"MUSIC"')),
|
|
154
|
-
[' - music "MUSIC": visible, collapsed, registration order, row "music"'],
|
|
155
|
-
);
|
|
156
|
-
assert.equal(collectStatus().sections?.[0].collapsed, true);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
test("the pointer's mechanism is reported once it has been used", () => {
|
|
160
|
-
setSectionTheme(theme);
|
|
161
|
-
registerSection(
|
|
162
|
-
section("music", { lines: () => [{ label: "play", onClick: () => {} }] }),
|
|
163
|
-
);
|
|
164
|
-
arm();
|
|
165
|
-
renderSidebar();
|
|
166
|
-
|
|
167
|
-
fakeState().parts.get(SIDEBAR_RAIL_KEY)?.handleMouse?.({
|
|
168
|
-
type: "move",
|
|
169
|
-
button: "none",
|
|
170
|
-
x: 4,
|
|
171
|
-
y: 4,
|
|
172
|
-
width: 40,
|
|
173
|
-
height: 20,
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
assert.equal(collectStatus().sectionFrame?.hover, "host-state");
|
|
177
|
-
assert.ok(
|
|
178
|
-
frameLines()[1].includes("hover owner claimed on the host's sidebar state"),
|
|
179
|
-
frameLines()[1],
|
|
180
|
-
);
|
|
181
|
-
});
|