Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
146 lines
6.0 KiB
TypeScript
146 lines
6.0 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
const runtimeSource = readFileSync(
|
|
new URL("./runtime/createXTermRuntime.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
const terminalSource = readFileSync(new URL("../Terminal.tsx", import.meta.url), "utf8");
|
|
|
|
const readCallbackBody = (source: string, marker: string): string => {
|
|
const start = source.indexOf(marker);
|
|
assert.notEqual(start, -1, `${marker} must exist`);
|
|
|
|
const bodyStart = source.indexOf("{", start);
|
|
assert.notEqual(bodyStart, -1, `${marker} must have a body`);
|
|
|
|
let depth = 0;
|
|
for (let index = bodyStart; index < source.length; index += 1) {
|
|
const char = source[index];
|
|
if (char === "{") depth += 1;
|
|
else if (char === "}") {
|
|
depth -= 1;
|
|
if (depth === 0) return source.slice(bodyStart, index + 1);
|
|
}
|
|
}
|
|
|
|
assert.fail(`${marker} body must close`);
|
|
};
|
|
|
|
test("the image addon is created from settings and only when inline images are on", () => {
|
|
assert.match(runtimeSource, /import \{ ImageAddon \} from "@xterm\/addon-image"/);
|
|
assert.match(
|
|
runtimeSource,
|
|
/const inlineImageOptions = resolveTerminalInlineImageAddonOptions\(settings\);[\s\S]{0,400}?if \(inlineImageOptions\) \{[\s\S]{0,300}?new ImageAddon\(inlineImageOptions\)[\s\S]{0,120}?term\.loadAddon\(imageAddon\)/,
|
|
"the addon must be constructed from resolved settings and skipped when they resolve to null",
|
|
);
|
|
|
|
const openIndex = runtimeSource.indexOf("term.open(ctx.container)");
|
|
const imageAddonIndex = runtimeSource.indexOf("new ImageAddon(inlineImageOptions)");
|
|
const webglIndex = runtimeSource.indexOf("const webglController = createWebglRendererController(");
|
|
|
|
assert.ok(openIndex !== -1 && imageAddonIndex !== -1 && webglIndex !== -1);
|
|
assert.ok(
|
|
openIndex < imageAddonIndex,
|
|
"the addon needs the render service, so it must be loaded after term.open",
|
|
);
|
|
assert.ok(
|
|
imageAddonIndex < webglIndex,
|
|
"the addon must patch setRenderer before the WebGL renderer is created, so image layers survive renderer swaps",
|
|
);
|
|
});
|
|
|
|
test("the image addon is disposed before the terminal that owns its layers", () => {
|
|
const disposeBody = readCallbackBody(runtimeSource, " dispose: () => {");
|
|
const imageDisposeIndex = disposeBody.indexOf("imageAddon.dispose()");
|
|
const termDisposeIndex = disposeBody.indexOf("term.dispose()");
|
|
|
|
assert.notEqual(imageDisposeIndex, -1, "runtime dispose must release the image addon");
|
|
assert.notEqual(termDisposeIndex, -1);
|
|
assert.ok(
|
|
imageDisposeIndex < termDisposeIndex,
|
|
"image storage and canvas layers must be released before xterm tears down its screen element",
|
|
);
|
|
});
|
|
|
|
test("hasInlineImages reports live image storage instead of a sticky flag", () => {
|
|
assert.match(runtimeSource, /hasInlineImages: \(\) => boolean;/);
|
|
assert.match(
|
|
runtimeSource,
|
|
/const hasInlineImages = \(\): boolean => \{[\s\S]{0,200}?imageAddon\.storageUsage > 0/,
|
|
"a session must stop blocking hibernate once its image cache is emptied",
|
|
);
|
|
assert.match(runtimeSource, /\n {4}hasInlineImages,\n/, "the runtime must expose hasInlineImages");
|
|
});
|
|
|
|
test("a session holding inline images degrades to soft-hide instead of full hibernate", () => {
|
|
assert.match(
|
|
terminalSource,
|
|
/const runtimeHasInlineImages = useCallback\(\s*\(\) => xtermRuntimeRef\.current\?\.hasInlineImages\(\) === true,/,
|
|
);
|
|
|
|
const hibernateBody = readCallbackBody(terminalSource, "const hibernateRuntime = useCallback(() =>");
|
|
const gateIndex = hibernateBody.indexOf("if (runtimeHasInlineImages()) {");
|
|
const softHideIndex = hibernateBody.indexOf("hideRuntimeOnly();", gateIndex);
|
|
const keepCountIndex = hibernateBody.indexOf("resolveHibernateKeepRendererCount(terminalSettings)");
|
|
const evictionIndex = hibernateBody.indexOf("terminalHiddenRendererStore.pickEvictionCandidate(");
|
|
const fullHibernateIndex = hibernateBody.indexOf("void fullHibernateRuntime()");
|
|
|
|
assert.notEqual(gateIndex, -1, "hibernate must check for inline images");
|
|
assert.ok(gateIndex < softHideIndex, "the inline image branch must soft-hide the runtime");
|
|
assert.ok(
|
|
gateIndex < keepCountIndex && gateIndex < evictionIndex && gateIndex < fullHibernateIndex,
|
|
"the inline image branch must run before the keep-count, eviction and full-hibernate paths",
|
|
);
|
|
});
|
|
|
|
test("full hibernate and the eviction upgrade both re-check inline images", () => {
|
|
const fullHibernateBody = readCallbackBody(
|
|
terminalSource,
|
|
"const fullHibernateRuntime = useCallback(async (): Promise<boolean> =>",
|
|
);
|
|
const canFinishIndex = fullHibernateBody.indexOf("const canFinishHibernate = () => (");
|
|
const imageCheckIndex = fullHibernateBody.indexOf("&& !runtimeHasInlineImages()", canFinishIndex);
|
|
const canFinishEndIndex = fullHibernateBody.indexOf(");", canFinishIndex);
|
|
|
|
assert.notEqual(canFinishIndex, -1);
|
|
assert.notEqual(imageCheckIndex, -1, "canFinishHibernate must reject sessions holding images");
|
|
assert.ok(
|
|
imageCheckIndex < canFinishEndIndex,
|
|
"the image check must be part of canFinishHibernate so it is re-evaluated after every async step",
|
|
);
|
|
|
|
const evictionIndex = terminalSource.indexOf(
|
|
"terminalHiddenRendererStore.consumeEvictionRequest(sessionId)",
|
|
);
|
|
const evictionGuardIndex = terminalSource.indexOf(
|
|
"if (runtimeHasInlineImages()) return;",
|
|
evictionIndex,
|
|
);
|
|
const evictionUpgradeIndex = terminalSource.indexOf(
|
|
"upgradeSoftHiddenRuntimeToHibernate();",
|
|
evictionIndex,
|
|
);
|
|
const upgradeBody = readCallbackBody(
|
|
terminalSource,
|
|
"const upgradeSoftHiddenRuntimeToHibernate = useCallback(() =>",
|
|
);
|
|
|
|
assert.notEqual(evictionIndex, -1);
|
|
assert.notEqual(
|
|
evictionGuardIndex,
|
|
-1,
|
|
"an evicted soft-hidden session with images must stay soft-hidden",
|
|
);
|
|
assert.ok(
|
|
evictionGuardIndex < evictionUpgradeIndex,
|
|
"the image guard must run before the renderer is woken for the hibernate upgrade",
|
|
);
|
|
assert.match(
|
|
upgradeBody,
|
|
/wakeSoftHiddenRuntimeRef\.current\?\.\(\);[\s\S]*?fullHibernateRuntime\(\)/,
|
|
"the shared upgrade path must wake the soft-hidden renderer before hibernating",
|
|
);
|
|
});
|