File size: 5,589 Bytes
34810d2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | // Claw banner tests: static/animated gating and the final-frame invariant.
import { describe, expect, it, vi } from "vitest";
import { stripAnsi } from "../../packages/terminal-core/src/ansi.js";
import type { RuntimeEnv } from "../runtime.js";
import { printClawBanner } from "./claw-banner.js";
const runtimeStub = () => {
const log = vi.fn();
return { runtime: { log } as unknown as RuntimeEnv, log };
};
async function runAnimated(rng: () => number) {
const chunks: string[] = [];
const { runtime } = runtimeStub();
await printClawBanner(runtime, {
columns: 120,
isTty: true,
rich: true,
env: {},
rng,
sleep: async () => {},
write: (chunk) => chunks.push(chunk),
});
return chunks;
}
async function runStatic() {
const { runtime, log } = runtimeStub();
await printClawBanner(runtime, { columns: 120, isTty: false, env: {} });
return stripAnsi(String(log.mock.calls[0]?.[0]))
.split("\n")
.filter((row) => row.length > 0);
}
const EXPECTED_MASCOT = [
" β’ββ:. .:βββ’",
":ββββ: :ββββ:",
".ββββ:.:β’βββ’:.:ββββ.",
" .βββ: β’βββββ’ :βββ.",
" ..:β’β’βββββββββ’β’:..",
".::β’β’β’β’βββββββ’β’β’β’::.",
" . .: β’βββββ’ :. .",
" . :ββββ: .",
" .ββββββ.",
" :β’β’β’β’:",
] as const;
describe("printClawBanner", () => {
it("prints the static banner when not animatable", async () => {
const { runtime, log } = runtimeStub();
await printClawBanner(runtime, { columns: 120, isTty: false, env: {} });
const output = stripAnsi(String(log.mock.calls[0]?.[0]));
const rows = output.split("\n").filter((row) => row.length > 0);
expect(rows.map((row) => row.slice(0, 20).trimEnd())).toEqual(EXPECTED_MASCOT);
expect(output).toContain("βββββ βββββ βββββ ββ β");
});
it("stays static under CI even on a rich TTY", async () => {
const { runtime, log } = runtimeStub();
await printClawBanner(runtime, { columns: 120, isTty: true, rich: true, env: { CI: "1" } });
expect(log).toHaveBeenCalledTimes(1);
});
it("falls back to the plain title on narrow terminals", async () => {
const { runtime, log } = runtimeStub();
await printClawBanner(runtime, { columns: 50, isTty: true, rich: true, env: {} });
const output = String(log.mock.calls[0]?.[0]);
expect(output).toContain("OPENCLAW");
expect(output).not.toContain("β");
});
it("animates on a rich TTY and settles on the exact static banner", async () => {
const staticRows = await runStatic();
const chunks = await runAnimated(() => 0);
expect(chunks[0]).toBe("\x1b[?25l");
expect(chunks).toContain("\x1b[?25h");
const frames = chunks.filter((chunk) => chunk.includes("\x1b[K"));
expect(frames.length).toBeGreaterThan(10);
expect(
frames.some((frame) => {
const [first = "", second = ""] = stripAnsi(frame).split("\n");
return (
first.slice(0, 20).trimEnd() === "β’ββ’.:. .:.β’ββ’" &&
second.slice(0, 20).trimEnd() === ":ββββ’: :β’βββ:"
);
}),
).toBe(true);
const finalRows = stripAnsi(frames[frames.length - 1] ?? "")
.split("\n")
.filter((row) => row.length > 0);
expect(finalRows).toEqual(staticRows);
});
it("installs scoped signal handlers only while animating", async () => {
const before = process.listenerCount("SIGINT");
let during = -1;
const { runtime } = runtimeStub();
await printClawBanner(runtime, {
columns: 120,
isTty: true,
rich: true,
env: {},
rng: () => 0.99,
sleep: async () => {
during = Math.max(during, process.listenerCount("SIGINT"));
},
write: () => {},
});
expect(during).toBe(before + 1);
expect(process.listenerCount("SIGINT")).toBe(before);
});
it("settles on the static frame when parallel work finishes first", async () => {
const staticRows = await runStatic();
const chunks: string[] = [];
const beforeSigint = process.listenerCount("SIGINT");
let settle!: () => void;
const settleWhen = new Promise<void>((resolve) => {
settle = resolve;
});
const { runtime } = runtimeStub();
const banner = printClawBanner(runtime, {
columns: 120,
isTty: true,
rich: true,
env: {},
rng: () => 0.99,
settleWhen,
sleep: () => new Promise<void>(() => {}),
write: (chunk) => chunks.push(chunk),
});
expect(chunks[0]).toBe("\x1b[?25l");
expect(process.listenerCount("SIGINT")).toBe(beforeSigint + 1);
settle();
await expect(banner).resolves.toBe("settled");
const frames = chunks.filter((chunk) => chunk.includes("\x1b[K"));
const finalRows = stripAnsi(frames.at(-1) ?? "")
.split("\n")
.filter((row) => row.length > 0);
expect(finalRows).toEqual(staticRows);
expect(chunks.at(-2)).toBe("\x1b[?25h");
expect(chunks.at(-1)).toBe("\n");
expect(process.listenerCount("SIGINT")).toBe(beforeSigint);
});
it("varies snips and shimmer passes with the rng", async () => {
// rng below the thresholds adds a second shimmer pass and a second snip.
const maximal = (await runAnimated(() => 0)).filter((c) => c.includes("\x1b[K"));
const minimal = (await runAnimated(() => 0.99)).filter((c) => c.includes("\x1b[K"));
expect(maximal.length).toBeGreaterThan(minimal.length);
});
});
|