[Init] Initial commit - NetMesh terminal manager
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

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,216 @@
import assert from "node:assert/strict";
import test from "node:test";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import {
VaultImportDestinationControls,
VaultImportProgressPanel,
VaultImportProgressView,
} from "./ImportVaultDialog.tsx";
const messages: Record<string, string> = {
"vault.import.progress.title": "Importing hosts",
"vault.import.progress.reading": "Reading file",
"vault.import.progress.parsing": "Parsing hosts",
"vault.import.progress.preparing": "Preparing changes",
"vault.import.progress.saving": "Saving hosts",
"vault.import.progress.complete": "Import complete",
"vault.import.progress.failed": "Import failed",
"vault.import.progress.summary": "Imported {count} hosts; skipped {skipped}; duplicates {duplicates}.",
"vault.import.progress.keepOpen": "You can keep using Netcatty while this runs.",
"vault.import.progress.fileSummary": "{name} · {count} files",
"vault.import.progress.fileCount": "{completed} of {total} files",
"common.close": "Close",
"common.cancel": "Cancel",
};
const t = (key: string, values?: Record<string, unknown>) => {
let value = messages[key] ?? key;
for (const [name, replacement] of Object.entries(values ?? {})) {
value = value.replace(`{${name}}`, String(replacement));
}
return value;
};
test("vault import progress renders the current background stage and percent", () => {
const html = renderToStaticMarkup(
<VaultImportProgressView
progress={{
status: "running",
stage: "parsing",
percent: 55,
formatLabel: "CSV",
fileName: "hosts.csv",
}}
onClose={() => {}}
onCancel={() => {}}
t={t}
/>,
);
assert.match(html, /Importing hosts/);
assert.match(html, /hosts\.csv/);
assert.match(html, /Parsing hosts/);
assert.match(html, /aria-valuenow="55"/);
assert.match(html, /role="status"/);
assert.match(html, /aria-live="polite"/);
assert.match(html, /<span[^>]*role="status"[^>]*>Parsing hosts<\/span>/);
assert.match(html, />Cancel</);
assert.doesNotMatch(html, />Close</);
});
test("vault import progress keeps the final result visible until the user closes it", () => {
const html = renderToStaticMarkup(
<VaultImportProgressView
progress={{
status: "complete",
stage: "complete",
percent: 100,
formatLabel: "CSV",
fileName: "hosts.csv",
imported: 8000,
skipped: 3,
duplicates: 2,
}}
onClose={() => {}}
t={t}
/>,
);
assert.match(html, /Import complete/);
assert.match(html, /Imported 8000 hosts; skipped 3; duplicates 2\./);
assert.match(
html,
/<span[^>]*role="status"[^>]*>Import complete\. Imported 8000 hosts; skipped 3; duplicates 2\.<\/span>/,
);
assert.match(html, /aria-valuenow="100"/);
assert.match(html, />Close</);
});
test("vault import progress announces the failure reason", () => {
const html = renderToStaticMarkup(
<VaultImportProgressView
progress={{
status: "error",
stage: "failed",
percent: 85,
formatLabel: "CSV",
fileName: "hosts.csv",
error: "Saved Vault data is unreadable",
}}
onClose={() => {}}
t={t}
/>,
);
assert.match(
html,
/<span[^>]*role="status"[^>]*>Import failed\. Saved Vault data is unreadable<\/span>/,
);
});
test("vault import progress shows SecureCRT batch file progress", () => {
const html = renderToStaticMarkup(
<VaultImportProgressView
progress={{
status: "running",
stage: "parsing",
percent: 43,
formatLabel: "SecureCRT",
fileName: "Sessions",
completedFiles: 2,
totalFiles: 3,
currentFileName: "DB.ini",
}}
onClose={() => {}}
t={t}
/>,
);
assert.match(html, /Sessions · 3 files/);
assert.match(html, /2 of 3 files/);
assert.match(html, /DB\.ini/);
});
test("vault import progress is shown in a non-blocking floating panel", () => {
const html = renderToStaticMarkup(
<VaultImportProgressPanel
progress={{
status: "running",
stage: "parsing",
percent: 55,
formatLabel: "CSV",
fileName: "hosts.csv",
}}
onClose={() => {}}
onCancel={() => {}}
t={t}
/>,
);
assert.match(html, /data-vault-import-progress-panel/);
assert.doesNotMatch(html, /role="dialog"/);
assert.match(html, /fixed/);
assert.match(html, /max-h-\[calc\(100vh-2rem\)\]/);
assert.match(html, /overflow-y-auto/);
});
test("vault import destination controls offer preserve, existing, and new groups", () => {
const html = renderToStaticMarkup(
<VaultImportDestinationControls
mode="existing"
onModeChange={() => {}}
groups={["Production", "Staging"]}
existingGroup="Production"
onExistingGroupChange={() => {}}
newGroup=""
onNewGroupChange={() => {}}
t={t}
/>,
);
assert.equal((html.match(/data-import-destination-mode=/g) ?? []).length, 3);
assert.match(html, /Production/);
assert.doesNotMatch(html, /Staging/);
assert.match(html, /vault\.import\.destination\.preserve/);
assert.match(html, /vault\.import\.destination\.existing/);
assert.match(html, /vault\.import\.destination\.new/);
});
test("import format step keeps destination settings off the main card grid", async () => {
// Source-level guard: format tiles must not embed SecureCRT copy, and the
// destination picker lives behind a dedicated footer entry point.
const { readFileSync } = await import("node:fs");
const source = readFileSync(
new URL("./ImportVaultDialog.tsx", import.meta.url),
"utf8",
);
assert.match(source, /data-import-destination-settings="true"/);
assert.match(source, /data-import-securecrt-prompt="true"/);
assert.match(source, /step === "destination"/);
assert.match(source, /FolderTree/);
assert.doesNotMatch(
source,
/data-import-format=\{opt\.format\}[\s\S]*securecrt\.directoryHint/,
);
});
test("vault import destination search caps very large group suggestions", () => {
const groups = Array.from({ length: 1000 }, (_, index) => `Group ${index}`);
const html = renderToStaticMarkup(
<VaultImportDestinationControls
mode="existing"
onModeChange={() => {}}
groups={groups}
existingGroup=""
onExistingGroupChange={() => {}}
newGroup=""
onNewGroupChange={() => {}}
t={t}
/>,
);
assert.equal((html.match(/<option/g) ?? []).length, 50);
assert.match(html, /list=/);
});