test(desktop): harden Buzz Term banner invariants

Co-authored-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
Signed-off-by: npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta <d8473ee32b973aa31a21a65adddcc4b69cc2a8a4dee8121ecd51926e0cddbc02@buzz.block.builderlab.xyz>
This commit is contained in:
npub1mprnacetjua2xx3p5eddmhxyk6wv929ymm5py8kd2xfxurxahspqqlgyta
2026-08-02 10:49:03 -04:00
parent 5c92315eb6
commit 98ebc8f904
5 changed files with 209 additions and 156 deletions
@@ -50,7 +50,19 @@ after(() => dom.window.close());
beforeEach(() => {
cleanup?.();
dom.window.localStorage.clear();
dom.window.HTMLCanvasElement.prototype.getBoundingClientRect = () => ({
bottom: 782,
height: 782,
left: 0,
right: 940.8,
top: 0,
width: 940.8,
x: 0,
y: 0,
toJSON() {},
});
dom.window.HTMLCanvasElement.prototype.getContext = () => ({
clearRect() {},
fillRect() {},
fillStyle: "",
fillText() {},
@@ -81,7 +93,7 @@ function fixture(overrides = {}) {
sessions: [{ active: true, closing: false, id: "one", title: "SHELL" }],
...overrides,
};
const view = render(
const renderTree = (nextProps) =>
createElement(
ThemeProvider,
null,
@@ -89,10 +101,17 @@ function fixture(overrides = {}) {
className: "buzz-huddle-app-surface",
tabIndex: -1,
}),
createElement(TerminalSubstrate, props),
),
);
return { calls, props, view };
createElement(TerminalSubstrate, nextProps),
);
const view = render(renderTree(props));
return {
calls,
props,
view,
rerender(nextOverrides) {
view.rerender(renderTree({ ...props, ...nextOverrides }));
},
};
}
async function ready(view) {
@@ -136,62 +155,81 @@ test("mounted IME paths neither toggle nor emit preedit text", async () => {
assert.deepEqual(calls.input, ["か"]);
});
test("first active PTY output dismisses the welcome overlay", async () => {
const baseFrame = {
cursor: { column: 0, line: 0, visible: false },
full: false,
rows: [
{
line: 0,
spans: [
{
style: { fg: 0, bg: 0, flags: 0 },
clusters: [{ column: 0, text: "$", width: 1 }],
},
],
},
{
line: 8,
spans: [
{
style: { fg: 0, bg: 0, flags: 0 },
clusters: [{ column: 70, text: "x", width: 1 }],
},
],
},
],
viewport: { columns: 80, generation: 1, screenLines: 24 },
};
const { view } = fixture({ frame: baseFrame });
await ready(view);
await waitFor(() =>
assert.equal(view.container.querySelector(".buzz-terminal-welcome"), null),
);
const EMPTY_FRAME = {
cursor: { column: 0, line: 0, visible: false },
full: false,
rows: [],
viewport: { columns: 112, generation: 1, screenLines: 46 },
};
cleanup();
const intersecting = fixture({
frame: {
...baseFrame,
rows: [
const VISIBLE_FRAME = {
...EMPTY_FRAME,
viewport: { ...EMPTY_FRAME.viewport, generation: 2 },
rows: [
{
line: 0,
spans: [
{
line: 8,
spans: [
{
style: { fg: 0, bg: 0, flags: 0 },
clusters: [{ column: 30, text: "x", width: 1 }],
},
],
style: { fg: 0, bg: 0, flags: 0 },
clusters: [{ column: 0, text: "$", width: 1 }],
},
],
},
});
await ready(intersecting.view);
],
};
async function expectWelcome(view, present) {
await waitFor(() =>
assert.equal(
intersecting.view.container.querySelector(".buzz-terminal-welcome"),
null,
),
present
? assert.ok(view.container.querySelector(".buzz-terminal-welcome"))
: assert.equal(
view.container.querySelector(".buzz-terminal-welcome"),
null,
),
);
}
test("non-empty output from the active PTY dismisses the welcome overlay", async () => {
const subject = fixture({ frame: EMPTY_FRAME });
await ready(subject.view);
await expectWelcome(subject.view, true);
subject.rerender({ frame: VISIBLE_FRAME });
await expectWelcome(subject.view, false);
});
test("empty active output keeps the welcome overlay", async () => {
const subject = fixture({ frame: EMPTY_FRAME });
await ready(subject.view);
await expectWelcome(subject.view, true);
subject.rerender({
frame: {
...EMPTY_FRAME,
viewport: { ...EMPTY_FRAME.viewport, generation: 2 },
},
});
await expectWelcome(subject.view, true);
});
test("non-empty output from an inactive PTY keeps the welcome overlay", async () => {
const subject = fixture({
sessionFrames: [{ frame: EMPTY_FRAME, sessionId: "one" }],
sessions: [
{ active: true, closing: false, id: "one", title: "SHELL" },
{ active: false, closing: false, id: "two", title: "LOG" },
],
});
await ready(subject.view);
await expectWelcome(subject.view, true);
subject.rerender({
sessionFrames: [
{ frame: EMPTY_FRAME, sessionId: "one" },
{ frame: VISIBLE_FRAME, sessionId: "two" },
],
});
await expectWelcome(subject.view, true);
});
test("mounted wheel path accumulates fractional lines per active session", async () => {
@@ -12,6 +12,50 @@ function layers(banner) {
);
}
function frameBounds(banner) {
const cells = banner.cells.flatMap((row, y) =>
row.flatMap((cell, x) =>
cell.layer === "bevel_hi" || cell.layer === "bevel_lo" ? [{ x, y }] : [],
),
);
return {
top: Math.min(...cells.map(({ y }) => y)),
left: Math.min(...cells.map(({ x }) => x)),
bottom: Math.max(...cells.map(({ y }) => y)) + 1,
right: Math.max(...cells.map(({ x }) => x)) + 1,
};
}
function headRows(banner) {
const cells = banner.cells.flatMap((row, y) =>
row.flatMap((cell, x) =>
cell.layer === "head" ? [{ ...cell, x, y }] : [],
),
);
const left = Math.min(...cells.map(({ x }) => x));
const right = Math.max(...cells.map(({ x }) => x));
const top = Math.min(...cells.map(({ y }) => y));
const bottom = Math.max(...cells.map(({ y }) => y));
return Array.from({ length: bottom - top + 1 }, (_, row) =>
Array.from(
{ length: right - left + 1 },
(_, column) =>
cells.find(({ x, y }) => x === left + column && y === top + row)
?.char ?? " ",
)
.join("")
.trimEnd(),
);
}
const EXPECTED_BUZZ_TERM = [
"██ ██",
"██▄▄▄ ██ ██ ██████ ██████ █████ ▄███▄ ██ ▄██ ██▄██▄██",
"██▀▀██ ██ ██ ▄██ ▄██ ██ ██▄▄▄█ ███▀▀ ██ ██ ██",
"██ ██ ██ ██ ▄██▀ ▄██▀ ██ ██ ██ ██ ██ ██",
"██████ ▀█████ ██████ ██████ ███ ▀███▀ ██ ██ ██ ██",
];
test("builds the amended four-layer Buzz Term composite", () => {
const banner = buildTerminalBanner(183, 69, 17 / 8.4);
assert.ok(banner);
@@ -22,16 +66,33 @@ test("builds the amended four-layer Buzz Term composite", () => {
assert.equal(Math.min(...sweep), 0);
assert.equal(Math.max(...sweep), 1);
assert.ok(head.every((cell) => cell.layer === "head"));
const { top, left, bottom, right } = banner.reserved;
const { top, left, bottom, right } = frameBounds(banner);
assert.equal(banner.cells[top][left].layer, "bevel_hi");
assert.equal(banner.cells[bottom - 1][right - 1].layer, "bevel_lo");
assert.match(
seen
.get("head")
.map((cell) => cell.char)
.join(""),
/█/,
);
assert.deepEqual(headRows(banner), EXPECTED_BUZZ_TERM);
});
test("requires every frame emitter independently", () => {
const banner = buildTerminalBanner(183, 69, 17 / 8.4);
assert.ok(banner);
const { top, left, bottom, right } = frameBounds(banner);
for (let x = left; x < right; x += 1)
assert.equal(banner.cells[top][x].layer, "bevel_hi", `top_row:${x}`);
for (let y = top + 1; y < bottom - 1; y += 1) {
assert.equal(banner.cells[y][left].layer, "bevel_hi", `left_rail:${y}`);
assert.equal(
banner.cells[y][right - 1].layer,
"bevel_lo",
`right_rail:${y}`,
);
}
for (let x = left; x < right; x += 1)
assert.equal(
banner.cells[bottom - 1][x].layer,
"bevel_lo",
`bottom_row:${x}`,
);
});
test("emits complete hexagons only and fades per ray", () => {
@@ -43,18 +104,6 @@ test("emits complete hexagons only and fades per ray", () => {
assert.ok(field.some((cell) => cell.t > 0.5));
});
test("safe mode replaces ambiguous frame rails without changing geometry", () => {
const ink = buildTerminalBanner(183, 69, 17 / 8.4, false);
const safe = buildTerminalBanner(183, 69, 17 / 8.4, true);
assert.ok(ink && safe);
assert.deepEqual(ink.reserved, safe.reserved);
assert.ok(ink.cells.flat().some((cell) => cell.char === "▌"));
assert.equal(
safe.cells.flat().some((cell) => cell.char === "▌"),
false,
);
});
test("declines viewports too small for the complete wordmark", () => {
assert.equal(buildTerminalBanner(40, 20, 17 / 8.4), null);
});
+48 -36
View File
@@ -8,12 +8,6 @@ export type TerminalBannerCell = Readonly<{
export type TerminalBanner = Readonly<{
cells: readonly (readonly TerminalBannerCell[])[];
reserved: Readonly<{
top: number;
left: number;
bottom: number;
right: number;
}>;
}>;
const HEX = [
@@ -48,22 +42,27 @@ const INK_FRAME = {
left: "▌",
right: "▐",
} as const;
const SAFE_FRAME = {
topLeft: "▛",
topRight: "▜",
bottomLeft: "▙",
bottomRight: "▟",
top: "░",
bottom: "░",
left: "▐",
right: "▐",
} as const;
const LAYERS: readonly TerminalBannerLayer[] = [
"field",
"head",
"bevel_hi",
"bevel_lo",
];
type TerminalBannerEmitter =
| "field"
| "top_row"
| "left_rail"
| "right_rail"
| "bottom_row"
| "wordmark";
const EMITTERS: readonly TerminalBannerEmitter[] = [
"field",
"top_row",
"left_rail",
"right_rail",
"bottom_row",
"wordmark",
];
function trimRight(value: string): string {
return value.replace(/\s+$/, "");
@@ -123,7 +122,6 @@ export function buildTerminalBanner(
columns: number,
rows: number,
cellAspect: number,
safe = false,
): TerminalBanner | null {
const mark = fitWordmark(columns - 10);
if (!mark || rows < mark.length + 6) return null;
@@ -142,15 +140,19 @@ export function buildTerminalBanner(
const grid: TerminalBannerCell[][] = Array.from({ length: rows }, () =>
Array.from({ length: columns }, () => ({ char: " ", t: 0 })),
);
const emitted = new Set<TerminalBannerEmitter>();
const put = (
y: number,
x: number,
char: string,
layer: TerminalBannerLayer,
t = 0,
emitter?: TerminalBannerEmitter,
) => {
if (y >= 0 && y < rows && x >= 0 && x < columns)
if (y >= 0 && y < rows && x >= 0 && x < columns) {
grid[y][x] = { char, layer, t };
if (emitter) emitted.add(emitter);
}
};
const halfWidth = frameWidth / 2;
@@ -190,7 +192,7 @@ export function buildTerminalBanner(
)
continue;
for (const [offsetY, offsetX, char] of HEX)
put(hex.y + offsetY, hex.x + offsetX, char, "field", ray * ray);
put(hex.y + offsetY, hex.x + offsetX, char, "field", ray * ray, "field");
}
for (
@@ -206,23 +208,39 @@ export function buildTerminalBanner(
grid[y][x] = { char: " ", t: 0 };
}
const frame = safe ? SAFE_FRAME : INK_FRAME;
put(top, left, frame.topLeft, "bevel_hi");
const frame = INK_FRAME;
put(top, left, frame.topLeft, "bevel_hi", 0, "top_row");
for (let x = 0; x < innerWidth; x += 1)
put(top, left + x + 1, frame.top, "bevel_hi");
put(top, left + frameWidth - 1, frame.topRight, "bevel_hi");
put(top, left + x + 1, frame.top, "bevel_hi", 0, "top_row");
put(top, left + frameWidth - 1, frame.topRight, "bevel_hi", 0, "top_row");
for (let y = top + 1; y < top + frameHeight - 1; y += 1) {
put(y, left, frame.left, "bevel_hi");
put(y, left + frameWidth - 1, frame.right, "bevel_lo");
put(y, left, frame.left, "bevel_hi", 0, "left_rail");
put(y, left + frameWidth - 1, frame.right, "bevel_lo", 0, "right_rail");
}
put(top + frameHeight - 1, left, frame.bottomLeft, "bevel_lo");
put(
top + frameHeight - 1,
left,
frame.bottomLeft,
"bevel_lo",
0,
"bottom_row",
);
for (let x = 0; x < innerWidth; x += 1)
put(top + frameHeight - 1, left + x + 1, frame.bottom, "bevel_lo");
put(
top + frameHeight - 1,
left + x + 1,
frame.bottom,
"bevel_lo",
0,
"bottom_row",
);
put(
top + frameHeight - 1,
left + frameWidth - 1,
frame.bottomRight,
"bevel_lo",
0,
"bottom_row",
);
const markLeft = left + Math.floor((frameWidth - width) / 2);
@@ -241,6 +259,7 @@ export function buildTerminalBanner(
cell.char,
"head",
(cell.x - minX) / span,
"wordmark",
);
const seen = new Map<TerminalBannerLayer, number[]>();
@@ -249,16 +268,9 @@ export function buildTerminalBanner(
if (cell.layer)
seen.set(cell.layer, [...(seen.get(cell.layer) ?? []), cell.t]);
if (LAYERS.some((layer) => !seen.has(layer))) return null;
if (EMITTERS.some((emitter) => !emitted.has(emitter))) return null;
const sweep = seen.get("head") ?? [];
if (Math.min(...sweep) !== 0 || Math.max(...sweep) !== 1) return null;
return {
cells: grid,
reserved: {
top,
left,
bottom: top + frameHeight,
right: left + frameWidth,
},
};
return { cells: grid };
}
@@ -7,7 +7,6 @@ import {
encodePaste,
encodeTerminalKey,
reduceHandoff,
updateWelcomeForOutput,
} from "./terminalState.ts";
test("ownership changes on completed chord, never key-down or repeat", () => {
@@ -96,21 +95,3 @@ test("pixel scrolling retains fractional lines in both directions", () => {
assert.equal(result.lines, -1);
assert.equal(result.state.remainderPx, -1);
});
test("welcome survives an ordinary prompt outside its reserved rectangle", () => {
const welcome = {
visible: true,
reserved: { top: 4, left: 10, bottom: 10, right: 40 },
};
assert.equal(
updateWelcomeForOutput(welcome, [{ top: 0, left: 0, bottom: 1, right: 20 }])
.visible,
true,
);
assert.equal(
updateWelcomeForOutput(welcome, [
{ top: 9, left: 39, bottom: 11, right: 42 },
]).visible,
false,
);
});
@@ -115,30 +115,3 @@ export function accumulateScrollLines(
lines,
};
}
export type Rect = { top: number; left: number; bottom: number; right: number };
export function rectsIntersect(a: Rect, b: Rect): boolean {
return !(
a.right <= b.left ||
b.right <= a.left ||
a.bottom <= b.top ||
b.bottom <= a.top
);
}
export type WelcomeState = {
visible: boolean;
reserved: Rect | null;
};
export function updateWelcomeForOutput(
state: WelcomeState,
changed: readonly Rect[],
): WelcomeState {
const reserved = state.reserved;
if (!state.visible || !reserved) return state;
return changed.some((rect) => rectsIntersect(rect, reserved))
? { ...state, visible: false }
: state;
}