mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-10 16:21:22 +00:00
fix(terminal): validate patched xterm across dev and CI
This commit is contained in:
1 parent
21880ef1ea
commit
992a303818
5 files changed
+82
-14
No files matched your search
@@ -78,6 +78,10 @@ test("PR validation runs once per commit and includes a production build", () =>
|
||||
testWorkflow,
|
||||
/- name: Test terminal keyword highlight performance\s*\n\s*env:\s*\n\s*NETCATTY_TERMINAL_PERF_SHOW_WINDOW: "1"\s*\n\s*# GitHub-hosted runners do not configure Electron's SUID sandbox helper\.\s*\n\s*run: xvfb-run -a \.\/node_modules\/\.bin\/electron --no-sandbox scripts\/xterm-keyword-highlight-performance\.live\.test\.cjs/,
|
||||
);
|
||||
assert.match(
|
||||
buildWorkflow,
|
||||
/- name: Test macOS Option column selection\s*\n\s*if: matrix\.name == 'macos'\s*\n\s*run: npm run test:xterm-macos-selection/,
|
||||
);
|
||||
assert.match(testWorkflow, /- name: Build\s*\n\s*run: npm run build/);
|
||||
assert.doesNotMatch(testWorkflow, /\n mosh-windows-conpty:/);
|
||||
});
|
||||
@@ -112,6 +116,8 @@ test("package validation avoids duplicate branch runs and scopes PR builds", ()
|
||||
"scripts/afterPackMacUuid.cjs",
|
||||
"scripts/beforePackCursorSdk.cjs",
|
||||
"scripts/nodePtyConptyPatch.cjs",
|
||||
"scripts/patch-xterm-macos-column-selection.cjs",
|
||||
"scripts/xterm-macos-column-selection.live.test.cjs",
|
||||
"scripts/linux/**",
|
||||
"skills/**",
|
||||
]) {
|
||||
|
||||
@@ -30,16 +30,19 @@ const PATCHES = [
|
||||
file: "node_modules/@xterm/xterm/src/browser/services/SelectionService.ts",
|
||||
original: "return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection);",
|
||||
replacement: "return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionIsMeta);",
|
||||
preserveStatementLength: true,
|
||||
},
|
||||
{
|
||||
file: "node_modules/@xterm/xterm/lib/xterm.js.map",
|
||||
original: "return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection);",
|
||||
replacement: "return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionIsMeta);",
|
||||
preserveStatementLength: true,
|
||||
},
|
||||
{
|
||||
file: "node_modules/@xterm/xterm/lib/xterm.mjs.map",
|
||||
original: "return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection);",
|
||||
replacement: "return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionIsMeta);",
|
||||
preserveStatementLength: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -49,11 +52,26 @@ function sameLengthExpression(expression, length) {
|
||||
return `${expression}${" ".repeat(length - expression.length)}`;
|
||||
}
|
||||
|
||||
function sameLengthStatement(statement, length) {
|
||||
if (!statement.endsWith(";") || statement.length > length) {
|
||||
throw new Error("replacement statement cannot preserve the target length");
|
||||
}
|
||||
return `${statement.slice(0, -1)}${" ".repeat(length - statement.length)};`;
|
||||
}
|
||||
|
||||
function replacementForPatch(patch) {
|
||||
if (!patch.replacement) throw new Error(`missing replacement for ${patch.file}`);
|
||||
if (patch.preserveLength) {
|
||||
return sameLengthExpression(patch.replacement, patch.original.length);
|
||||
}
|
||||
if (patch.preserveStatementLength) {
|
||||
return sameLengthStatement(patch.replacement, patch.original.length);
|
||||
}
|
||||
return patch.replacement;
|
||||
}
|
||||
|
||||
function patchXtermSource(source, patch) {
|
||||
const replacement = patch.preserveLength
|
||||
? sameLengthExpression(patch.replacement, patch.original.length)
|
||||
: patch.replacement;
|
||||
if (!replacement) throw new Error(`missing replacement for ${patch.file}`);
|
||||
const replacement = replacementForPatch(patch);
|
||||
|
||||
const originalCount = source.split(patch.original).length - 1;
|
||||
const patchedCount = source.split(replacement).length - 1;
|
||||
@@ -69,6 +87,16 @@ function patchXtermSource(source, patch) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} root
|
||||
* @param {Pick<typeof fs, "rmSync">} fsImpl
|
||||
*/
|
||||
function invalidateViteCache(root = process.cwd(), fsImpl = fs) {
|
||||
const cachePath = path.resolve(root, "node_modules/.vite");
|
||||
fsImpl.rmSync(cachePath, { recursive: true, force: true });
|
||||
return cachePath;
|
||||
}
|
||||
|
||||
function patchInstalledXterm(root = process.cwd()) {
|
||||
const packageJsonPath = path.resolve(root, "node_modules/@xterm/xterm/package.json");
|
||||
const version = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")).version;
|
||||
@@ -88,7 +116,8 @@ function patchInstalledXterm(root = process.cwd()) {
|
||||
changed++;
|
||||
}
|
||||
}
|
||||
return { changed, checked: PATCHES.length, version };
|
||||
const viteCachePath = invalidateViteCache(root);
|
||||
return { changed, checked: PATCHES.length, version, viteCachePath };
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
@@ -96,7 +125,7 @@ if (require.main === module) {
|
||||
const result = patchInstalledXterm();
|
||||
console.log(
|
||||
`[patch-xterm-macos-column-selection] version=${result.version} ` +
|
||||
`changed=${result.changed} checked=${result.checked}`,
|
||||
`changed=${result.changed} checked=${result.checked} vite-cache=invalidated`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`[patch-xterm-macos-column-selection] ERROR: ${error.message}`);
|
||||
@@ -107,7 +136,10 @@ if (require.main === module) {
|
||||
module.exports = {
|
||||
EXPECTED_XTERM_VERSION,
|
||||
PATCHES,
|
||||
invalidateViteCache,
|
||||
patchInstalledXterm,
|
||||
patchXtermSource,
|
||||
replacementForPatch,
|
||||
sameLengthExpression,
|
||||
sameLengthStatement,
|
||||
};
|
||||
@@ -1,11 +1,14 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const path = require("node:path");
|
||||
const test = require("node:test");
|
||||
|
||||
const {
|
||||
PATCHES,
|
||||
invalidateViteCache,
|
||||
patchXtermSource,
|
||||
replacementForPatch,
|
||||
sameLengthExpression,
|
||||
} = require("./patch-xterm-macos-column-selection.cjs");
|
||||
|
||||
@@ -25,17 +28,17 @@ test("patches both distributed bundles without shifting source-map offsets", ()
|
||||
|
||||
test("patches readable xterm sources and source-map content", () => {
|
||||
for (const patch of PATCHES.filter((entry) => !entry.preserveLength)) {
|
||||
const result = patchXtermSource(`before:${patch.original}:after`, patch);
|
||||
const input = `before:${patch.original}:after`;
|
||||
const result = patchXtermSource(input, patch);
|
||||
assert.equal(result.changed, true);
|
||||
assert.equal(result.source, `before:${patch.replacement}:after`);
|
||||
assert.equal(result.source, `before:${replacementForPatch(patch)}:after`);
|
||||
assert.equal(result.source.length, input.length);
|
||||
}
|
||||
});
|
||||
|
||||
test("is idempotent and fails closed on an unknown package shape", () => {
|
||||
for (const patch of PATCHES) {
|
||||
const replacement = patch.preserveLength
|
||||
? sameLengthExpression(patch.replacement, patch.original.length)
|
||||
: patch.replacement;
|
||||
const replacement = replacementForPatch(patch);
|
||||
assert.deepEqual(patchXtermSource(`before:${replacement}:after`, patch), {
|
||||
source: `before:${replacement}:after`,
|
||||
changed: false,
|
||||
@@ -53,3 +56,18 @@ test("fails closed when a generated-bundle replacement would shift offsets", ()
|
||||
/replacement expression is too long/,
|
||||
);
|
||||
});
|
||||
|
||||
test("invalidates Vite's optimized dependency cache after patching xterm", () => {
|
||||
const calls = [];
|
||||
const cachePath = invalidateViteCache("/repo", {
|
||||
rmSync(target, options) {
|
||||
calls.push({ target, options });
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(cachePath, path.resolve("/repo/node_modules/.vite"));
|
||||
assert.deepEqual(calls, [{
|
||||
target: cachePath,
|
||||
options: { recursive: true, force: true },
|
||||
}]);
|
||||
});
|
||||
@@ -13,6 +13,7 @@ if (!process.versions.electron) {
|
||||
const assert = require("node:assert/strict");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { pathToFileURL } = require("node:url");
|
||||
const electron = require("electron");
|
||||
const tempDirBridge = require("../electron/bridges/tempDirBridge.cjs");
|
||||
|
||||
@@ -46,6 +47,8 @@ if (!process.versions.electron) {
|
||||
contextIsolation: false,
|
||||
nodeIntegration: true,
|
||||
sandbox: false,
|
||||
// The harness imports the shipped ESM bundle directly from disk.
|
||||
webSecurity: false,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
});
|
||||
@@ -56,8 +59,9 @@ if (!process.versions.electron) {
|
||||
);
|
||||
|
||||
const xtermPath = require.resolve("@xterm/xterm", { paths: [appRoot] });
|
||||
await window.webContents.executeJavaScript(`(() => {
|
||||
const { Terminal } = require(${JSON.stringify(xtermPath)});
|
||||
const xtermEsmUrl = pathToFileURL(path.join(path.dirname(xtermPath), "xterm.mjs")).href;
|
||||
await window.webContents.executeJavaScript(`(async () => {
|
||||
const { Terminal } = await import(${JSON.stringify(xtermEsmUrl)});
|
||||
const term = new Terminal({
|
||||
cols: 20,
|
||||
rows: 6,
|
||||
@@ -114,8 +118,10 @@ if (!process.versions.electron) {
|
||||
const end = point(5, 2);
|
||||
/** @type {Array<"alt">} */
|
||||
const modifiers = alt ? ["alt"] : [];
|
||||
/** @type {Array<"alt" | "leftbuttondown">} */
|
||||
const moveModifiers = alt ? ["alt", "leftbuttondown"] : ["leftbuttondown"];
|
||||
window.webContents.sendInputEvent({ type: "mouseDown", ...start, button: "left", clickCount: 1, modifiers });
|
||||
window.webContents.sendInputEvent({ type: "mouseMove", ...end, button: "left", modifiers });
|
||||
window.webContents.sendInputEvent({ type: "mouseMove", ...end, button: "left", modifiers: moveModifiers });
|
||||
window.webContents.sendInputEvent({ type: "mouseUp", ...end, button: "left", clickCount: 1, modifiers });
|
||||
await delay(100);
|
||||
return window.webContents.executeJavaScript("window.harness.state()");
|
||||
|
||||
Reference in new issue
Block a user