mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-11-13 01:50:47 +00:00
* refactor: migrate architecture-debt hooks into application/state Move SFTP Wave A stores/hooks and related vault/AI/terminal/systemManager hooks out of components into application/state, update importers, and keep thin deprecated re-exports at the old paths. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * refactor(sftp): align local bookmarks store API with global bookmarks Export subscribe/get/set/rehydrate helpers from localSftpBookmarks to match the globalSftpBookmarks external-store pattern. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(electron): reduce baseline memory via spellcheck/GPU/prewarm guards Disable spellcheck on BrowserWindows and the default session, drop spare renderers, gate aggressive GPU switches behind NETCATTY_COMPAT_GPU, and make settings prewarm opt-in via NETCATTY_PREWARM_SETTINGS. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * Isolate notes and customAccent from TerminalLayer hot props Route vault notes through notesStore (shellHistoryStore pattern) and app accent through appearanceChromeStore so note edits and accent-drag ticks no longer rebuild TerminalLayer. Notes/AI slots and Terminal leaves subscribe locally; VaultView and open-note callbacks stay unchanged. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(ui): cut unnecessary AppView/VaultView/settings re-renders Stabilize TopTabs close handler, memo AppHostTreeLayer, exclude accent from settingsVersion, ignore connectionLogs off the logs section, share lastConnected host refs, and keep rename dialog drafts local so chrome domain stays stable. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * chore: package size, dead code, and consolidation cleanup Remove unused direct deps, exclude Vite-bundled renderer packages from electron-builder, delete high-confidence dead code/no-op transfer warm wiring, consolidate cloud provider IDs and KnownHost/SFTP helpers, and land UI/AGENTS quick wins (rAF text batching, notes draft debounce, streaming plain text, tempDirBridge, diagnostics adapter, External MCP setters). Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: stabilize rAF text batching and diagnostics tests Polyfill requestAnimationFrame for Node, flush pending text before steer buffering, bypass rAF while steer is in flight, and stub global localStorage for AI panel diagnostics adapter tests. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: correct import paths after application/state hook migrations Migrated vault/AI/system hooks still used components/-relative imports; point them at repo-root domain/components/types/lib instead. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: address Bugbot/Codex regressions from perf cleanup - Keep blank Enter from closing RenameDraftDialog; only reset rename state after a non-empty submit - Flush note drafts before vault mutations so pending title/body edits are not overwritten - Re-apply appearanceChromeStore accent on appearanceTheme in Terminal, and keep layer themes unaccented Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(notes): flush draft before markdown export paths Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: keep editor-close hotkey ref fresh during render Assign handleRequestCloseEditorTabRef in render instead of useEffect so Cmd/Ctrl+W never hits the App stub before the effect runs. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: keep accent in chrome themes without TerminalLayer thrash Restore accent in globalAppearance for chrome/injection consumers. Keep resolveFocusedAppearance identity stable across accent drag via accent ref, while Terminal leaf still re-applies appearanceChromeStore. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: keep compose chrome accent live and merge note renames Subscribe theme-panel focusedAppearance to appearanceChromeStore so compose-bar chrome updates during color-picker drag even when resolveFocusedAppearance identity stays stable. Tree renames now merge title into the post-flush note instead of replacing from a stale row. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: clear terminal accent when switching back to theme mode Re-resolve appearanceTheme by catalog id before applyCustomAccent so a stale baked custom cursor/selection cannot stick after accentMode returns to theme while TerminalLayer memo ignores accent props. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: register unsaved prompt singleton during render AppView close handlers call promptUnsavedChanges outside the old render-prop. Assign the singleton during render so Cmd/Ctrl+W and tab close never hit the pre-effect null window that auto-cancels. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: keep vault import hooks UI-free and flush notes on teardown Move ImportOptions into application/state and inject a notifier so useVaultImportHandlers no longer imports components. Notes drafts also flush on pagehide/beforeunload when Electron tears down without React unmount. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: move AI terminal session context builders into domain buildAITerminalSessionInfo and AIPanelContext now live under domain/ so application hooks no longer import TerminalLayerSupport and create a UI → application → UI cycle. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: drop duplicate AIPanelContext type from TerminalLayerSupport The type is re-exported from domain; the local definition conflicted. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: keep migrated state hooks free of UI-layer imports Move aiChatStreamingSupport into infrastructure/ai and poll list helpers into domain/systemManager so useAIChatStreaming / useSystemManager no longer import from components. Leave thin re-exports at the old paths. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: move remaining migrated-hook deps out of components Host-key verification types/helpers now live in domain/, and SFTP column layout types live in application/state/sftp/columnLayout. Application hooks no longer import those definitions from the UI layer. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: clear lint blockers and flush notes when panels hide Move AI panel diagnostics persistence into application/state so components stop importing localStorageAdapter. Flush note drafts when retained NotesManager mounts become inactive, and silence related lint warnings in theme/host-key helpers. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(notes): publish notesStore sync on vault note updates updateNotes/updateNoteGroups now publishNotesSnapshot immediately so AI/notes consumers do not briefly read a stale catalog after a hide flush. Flush inactive drafts in useLayoutEffect before paint. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
331 lines
11 KiB
JavaScript
331 lines
11 KiB
JavaScript
const test = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
|
|
const config = require("../electron-builder.config.cjs");
|
|
|
|
test("unpacked MCP server includes its shared CommonJS dependencies", () => {
|
|
assert.ok(
|
|
config.asarUnpack.includes("electron/mcp/**/*"),
|
|
"MCP server must stay unpacked so Codex can launch it as a child process",
|
|
);
|
|
assert.ok(
|
|
config.asarUnpack.includes("lib/**/*.cjs"),
|
|
"MCP server requires ../../lib/commandBlocklist.cjs from the unpacked runtime path",
|
|
);
|
|
assert.ok(
|
|
config.asarUnpack.includes("lib/**/*.json"),
|
|
"unpacked lib CommonJS modules require sibling JSON data files at runtime",
|
|
);
|
|
});
|
|
|
|
test("build.files includes shared terminal flow constants for main process", () => {
|
|
assert.ok(
|
|
config.files.includes("infrastructure/config/terminalFlowConstants.cjs"),
|
|
"terminalFlowAck.cjs requires infrastructure/config/terminalFlowConstants.cjs at packaged startup",
|
|
);
|
|
assert.ok(
|
|
config.files.includes("infrastructure/config/terminalFlowConstants.json"),
|
|
"terminalFlowConstants.cjs requires sibling terminalFlowConstants.json at packaged startup",
|
|
);
|
|
});
|
|
|
|
test("unpacked Tool CLI includes capability runtime dependencies", () => {
|
|
assert.ok(
|
|
config.asarUnpack.includes("electron/cli/**/*"),
|
|
"Tool CLI launcher and scripts must stay unpacked so agents can launch them as child processes",
|
|
);
|
|
assert.ok(
|
|
config.asarUnpack.includes("electron/capabilities/**/*"),
|
|
"Tool CLI requires capability catalog, registry, policy, timeout, and RPC transport modules from the unpacked runtime path",
|
|
);
|
|
assert.ok(
|
|
config.asarUnpack.includes("electron/shared/**/*"),
|
|
"Capability policy may load shared permission grant helpers from the unpacked runtime path",
|
|
);
|
|
});
|
|
|
|
test("build.files excludes per-platform agent binaries", () => {
|
|
const files = config.files;
|
|
const expectExclusions = [
|
|
"!**/@anthropic-ai/claude-agent-sdk-*/**/*",
|
|
"!node_modules/@anthropic-ai/claude-code-*/**/*",
|
|
"!node_modules/@openai/codex-{darwin,linux,linuxmusl,win32}-*/**/*",
|
|
"!node_modules/@github/copilot-{darwin,linux,linuxmusl,win32}-*/**/*",
|
|
"!node_modules/@github/copilot/**/*",
|
|
"!node_modules/opencode-{darwin,linux,linuxmusl,windows}-*/**/*",
|
|
"!node_modules/opencode-ai/**/*",
|
|
];
|
|
for (const glob of expectExclusions) {
|
|
assert.ok(
|
|
files.includes(glob),
|
|
`build.files must exclude platform binary glob: ${glob}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("asarUnpack no longer references removed legacy agent packages", () => {
|
|
const unpack = config.asarUnpack.join("\n");
|
|
for (const stale of [
|
|
"@agentclientprotocol/claude-agent-acp",
|
|
"@agentclientprotocol/sdk",
|
|
"@zed-industries/codex-acp",
|
|
]) {
|
|
assert.ok(
|
|
!unpack.includes(stale),
|
|
`asarUnpack must not reference removed package: ${stale}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("asarUnpack keeps MCP server runtime deps unpacked", () => {
|
|
// @modelcontextprotocol/sdk is now a direct dep and the MCP server hard-requires it.
|
|
assert.ok(config.asarUnpack.includes("node_modules/@modelcontextprotocol/sdk/**/*"));
|
|
});
|
|
|
|
test("asarUnpack keeps Cursor SDK runtime deps unpacked", () => {
|
|
assert.ok(
|
|
!config.asarUnpack.includes("node_modules/@cursor/sdk/**/*"),
|
|
"Cursor SDK JavaScript can load from app.asar and should not be duplicated into app.asar.unpacked",
|
|
);
|
|
assert.ok(config.asarUnpack.includes("node_modules/@cursor/sdk-*/**/*"));
|
|
assert.ok(config.asarUnpack.includes("node_modules/sqlite3/**/*"));
|
|
});
|
|
|
|
test("beforePack installs missing Cursor SDK platform runtime packages", () => {
|
|
assert.equal(config.beforePack, "./scripts/beforePackCursorSdk.cjs");
|
|
});
|
|
|
|
test("packaged app declares ssh, telnet, and jms URL protocol support", () => {
|
|
assert.deepEqual(config.protocols, [
|
|
{
|
|
name: "SSH URL",
|
|
schemes: ["ssh"],
|
|
},
|
|
{
|
|
name: "Telnet URL",
|
|
schemes: ["telnet"],
|
|
},
|
|
{
|
|
name: "JumpServer URL",
|
|
schemes: ["jms"],
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("build.files trims release-only dependency payloads", () => {
|
|
const files = config.files;
|
|
for (const glob of [
|
|
"!node_modules/@cursor/sdk/dist/cjs/**/*",
|
|
"!node_modules/@cursor/sdk/dist/**/*.d.ts",
|
|
"!node_modules/@cursor/sdk/dist/**/*.d.ts.map",
|
|
"!node_modules/sqlite3/deps/**/*",
|
|
"!node_modules/**/docs/**/*",
|
|
"!node_modules/**/doc/**/*",
|
|
"!node_modules/**/benchmark/**/*",
|
|
"!node_modules/**/benchmarks/**/*",
|
|
]) {
|
|
assert.ok(files.includes(glob), `build.files must exclude release-only payload: ${glob}`);
|
|
}
|
|
});
|
|
|
|
test("build.files excludes Vite-bundled renderer-only packages", () => {
|
|
const files = config.files;
|
|
for (const glob of [
|
|
"!node_modules/react/**/*",
|
|
"!node_modules/react-dom/**/*",
|
|
"!node_modules/@radix-ui/**/*",
|
|
"!node_modules/ai/**/*",
|
|
"!node_modules/@ai-sdk/**/*",
|
|
"!node_modules/@mdxeditor/**/*",
|
|
"!node_modules/streamdown/**/*",
|
|
"!node_modules/@streamdown/**/*",
|
|
"!node_modules/@tanstack/react-virtual/**/*",
|
|
"!node_modules/pinyin-pro/**/*",
|
|
"!node_modules/re2js/**/*",
|
|
"!node_modules/@eslint-community/regexpp/**/*",
|
|
"!node_modules/clsx/**/*",
|
|
"!node_modules/tailwind-merge/**/*",
|
|
"!node_modules/use-stick-to-bottom/**/*",
|
|
"!node_modules/lexical/**/*",
|
|
"!node_modules/@lexical/**/*",
|
|
"!node_modules/@codemirror/**/*",
|
|
"!node_modules/shiki/**/*",
|
|
"!node_modules/@shiki/**/*",
|
|
]) {
|
|
assert.ok(
|
|
files.includes(glob),
|
|
`build.files must exclude Vite-bundled renderer package: ${glob}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test("linux packaging uses multi-size build/icons instead of a single 1024px override", async () => {
|
|
assert.equal(
|
|
config.linux.icon,
|
|
"icons",
|
|
"linux.icon must point at build/icons so electron-builder installs hicolor/* sizes",
|
|
);
|
|
assert.equal(config.directories.buildResources, "build");
|
|
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const iconsDir = path.join(__dirname, "..", "build", "icons");
|
|
for (const size of [16, 32, 48, 64, 128, 256, 512]) {
|
|
const file = path.join(iconsDir, `${size}x${size}.png`);
|
|
assert.ok(fs.existsSync(file), `expected Linux icon: build/icons/${size}x${size}.png`);
|
|
}
|
|
|
|
const { convertIcon } = require("app-builder-lib/out/util/iconConverter");
|
|
const projectDir = path.join(__dirname, "..");
|
|
const buildResources = path.join(projectDir, config.directories.buildResources);
|
|
const sources = [config.linux.icon, config.mac?.icon ?? config.icon].filter(Boolean);
|
|
const result = await convertIcon({
|
|
sources,
|
|
fallbackSources: [buildResources],
|
|
roots: [buildResources, projectDir],
|
|
format: "set",
|
|
outDir: path.join(projectDir, "release", ".icon-config-test"),
|
|
});
|
|
const sizes = result.icons.map((icon) => icon.size);
|
|
assert.ok(
|
|
sizes.includes(48) && sizes.includes(256) && !sizes.every((size) => size === 1024),
|
|
`expected standard hicolor sizes, got: ${sizes.join(", ")}`,
|
|
);
|
|
});
|
|
|
|
test("linux packaging includes an Arch Linux pacman package target", () => {
|
|
assert.deepEqual(
|
|
config.linux.target,
|
|
["AppImage", "deb", "rpm", "pacman"],
|
|
"linux package builds must publish AppImage, Debian, RPM, and Arch pacman artifacts",
|
|
);
|
|
});
|
|
|
|
test("rpm packaging disables generated build-id symlinks", () => {
|
|
assert.deepEqual(
|
|
config.rpm?.fpm,
|
|
[
|
|
"--rpm-rpmbuild-define",
|
|
"_build_id_links none",
|
|
"--rpm-rpmbuild-define",
|
|
"__os_install_post %{nil}",
|
|
],
|
|
"RPM packages must skip build-id links and host brp post scripts on RHEL builders",
|
|
);
|
|
});
|
|
|
|
test("rpm packaging uses gzip compression for RHEL-family package hosts", () => {
|
|
// Default electron-builder/fpm RPM compression is xzmt. AlmaLinux/RHEL 8 CI
|
|
// images provide `xz` but not the `xzmt` shim, which makes rpmbuild exit 127.
|
|
assert.equal(
|
|
config.rpm?.compression,
|
|
"gzip",
|
|
"RPM compression must avoid xzmt on RHEL 8 / AlmaLinux 8 package builders",
|
|
);
|
|
});
|
|
|
|
test("Windows package arch is controlled by pack script CLI flags", () => {
|
|
assert.deepEqual(
|
|
config.win.target,
|
|
["nsis", "portable", "zip"],
|
|
"win.target must not hard-code x64 and arm64 or pack:win-x64 will still emit broken arm64 installers",
|
|
);
|
|
});
|
|
|
|
test("windows packaging includes a zip archive target", () => {
|
|
const winTargets = config.win.target.map((entry) => (
|
|
typeof entry === "string" ? entry : entry.target
|
|
));
|
|
assert.ok(
|
|
winTargets.includes("zip"),
|
|
"windows package builds must publish a zip archive for no-install environments",
|
|
);
|
|
});
|
|
|
|
test("windows installer registers and removes Explorer folder context menu entries", () => {
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
assert.equal(
|
|
config.nsis.include,
|
|
"build/installer.nsh",
|
|
"NSIS packaging must include the Explorer context-menu installer hooks",
|
|
);
|
|
|
|
const installerScript = fs.readFileSync(
|
|
path.join(__dirname, "..", config.nsis.include),
|
|
"utf8",
|
|
);
|
|
const folderKey = String.raw`Software\Classes\Directory\shell\Netcatty`;
|
|
const backgroundKey = String.raw`Software\Classes\Directory\Background\shell\Netcatty`;
|
|
|
|
assert.match(installerScript, /!macro customInstall\b/);
|
|
assert.match(installerScript, new RegExp(`WriteRegStr SHCTX "${folderKey.replaceAll("\\", "\\\\")}"`));
|
|
assert.match(installerScript, new RegExp(`WriteRegStr SHCTX "${backgroundKey.replaceAll("\\", "\\\\")}"`));
|
|
assert.match(installerScript, /-- --open-terminal-path="%1\."/);
|
|
assert.match(installerScript, /-- --open-terminal-path="%V\."/);
|
|
|
|
assert.match(installerScript, /!macro customUnInstall\b/);
|
|
assert.match(installerScript, new RegExp(`DeleteRegKey SHCTX "${folderKey.replaceAll("\\", "\\\\")}"`));
|
|
assert.match(installerScript, new RegExp(`DeleteRegKey SHCTX "${backgroundKey.replaceAll("\\", "\\\\")}"`));
|
|
});
|
|
|
|
test("windows zip follows the requested build architecture", () => {
|
|
const { Arch } = require("builder-util");
|
|
const { Platform } = require("app-builder-lib");
|
|
const { computeArchToTargetNamesMap } = require("app-builder-lib/out/targets/targetFactory");
|
|
|
|
const rawCliTargets = new Map([[Arch.x64, []]]);
|
|
const targetsByArch = computeArchToTargetNamesMap(
|
|
rawCliTargets,
|
|
{
|
|
platformSpecificBuildOptions: config.win,
|
|
defaultTarget: ["nsis"],
|
|
},
|
|
Platform.WINDOWS,
|
|
);
|
|
|
|
assert.deepEqual(
|
|
targetsByArch.get(Arch.x64)?.slice().sort(),
|
|
["nsis", "portable", "zip"].sort(),
|
|
"pack:win-x64 must publish x64 nsis, portable, and zip",
|
|
);
|
|
assert.equal(
|
|
targetsByArch.has(Arch.arm64),
|
|
false,
|
|
"pack:win-x64 must not publish arm64 nsis/portable/zip without a dedicated arm64 job",
|
|
);
|
|
});
|
|
|
|
test("linux FPM packages use the custom post-install template", () => {
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
|
|
for (const [target, afterInstall] of Object.entries({
|
|
deb: config.deb.afterInstall,
|
|
rpm: config.rpm.afterInstall,
|
|
pacman: config.pacman.afterInstall,
|
|
})) {
|
|
assert.equal(
|
|
afterInstall,
|
|
"scripts/linux/after-install.tpl",
|
|
`${target}.afterInstall must point at the custom FPM post-install template`,
|
|
);
|
|
}
|
|
assert.equal(
|
|
config.pacman.afterRemove,
|
|
"scripts/linux/after-remove.tpl",
|
|
"pacman.afterRemove must point at the custom FPM post-remove template",
|
|
);
|
|
|
|
for (const relPath of [config.deb.afterInstall, config.pacman.afterRemove]) {
|
|
const file = path.join(__dirname, "..", relPath);
|
|
const contents = fs.readFileSync(file, "utf8");
|
|
assert.match(
|
|
contents,
|
|
/gtk-update-icon-cache.*\/usr\/share\/icons\/hicolor/,
|
|
`${relPath} must refresh the hicolor icon cache`,
|
|
);
|
|
}
|
|
});
|