mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-09-24 15:49:09 +00:00
5dcba28621
* fix(#2826): automated Cursor CLI fix * fix(scripts): confirm bulk delete and clear host bindings Scripts side-panel mass-delete skipped confirmation and, when onSnippetsChange was wired, filtered snippets without clearing host login/connect script bindings. Confirm first, always dispatch the shared delete event, and apply deletes via deleteSelectedSnippetsFromVault. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(scripts): harden delete handler against stale vault state Read snippets/hosts from refs in the shared delete listener so rapid confirms cannot resurrect already-removed items, and clear pending bulk delete when the scripts panel hides so the confirm dialog does not resurface after a mid-prompt hide. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix(scripts): keep delete refs current within the same tick Update snippetsRef/hostsRef immediately after applying a delete so a same-tick second event cannot rebuild vault state from the pre-delete snapshot and resurrect removed snippets or host bindings. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: address Codex review on PR #2828 * fix(scripts): hoist bulk-delete confirm outside toolbar popover Portalled Dialog focus dismissed the compact Scripts popover and cleared pendingDeleteIds before confirm. Own the confirm in TerminalToolbar and defer via onBulkDeleteRequest when nested in that popover. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: serialize snippet saves with bulk-delete vault lock updateSnippets wrote STORAGE_KEY_SNIPPETS without withVaultImportLock, so a concurrent edit could land between bulk-delete's snapshot read and journal commit and be discarded. Queue ordinary snippet writes on the shared lock and include them in waitForPendingVaultWrites. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: rebase queued snippet saves onto latest vault snapshot Locking updateSnippets alone still wrote a pre-delete full array, so a popup bulk-delete could be resurrected (or a storage-event version bump could cancel the local edit). Rebase under the vault lock and supersede only newer local writers via snippetsWriteOwnerRef. Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com> * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 * fix: address Codex review on PR #2828 --------- Co-authored-by: netcatty-bot <308658023+netcatty-bot@users.noreply.github.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
118 lines
4.7 KiB
TypeScript
118 lines
4.7 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { readFileSync } from "node:fs";
|
|
|
|
import {
|
|
buildScriptsSidePanelRows,
|
|
collectScriptsSidePanelPackagePaths,
|
|
} from "./ScriptsSidePanel.tsx";
|
|
import type { Snippet } from "../types";
|
|
|
|
const snippet = (overrides: Partial<Snippet>): Snippet => ({
|
|
id: overrides.id ?? "snippet",
|
|
label: overrides.label ?? "Snippet",
|
|
command: overrides.command ?? "echo ok",
|
|
package: overrides.package ?? "",
|
|
order: overrides.order,
|
|
});
|
|
|
|
const source = readFileSync(new URL("./ScriptsSidePanel.tsx", import.meta.url), "utf8");
|
|
|
|
test("scripts side panel rows keep manual snippet order inside a package", () => {
|
|
const rows = buildScriptsSidePanelRows({
|
|
snippets: [
|
|
snippet({ id: "alpha", label: "Alpha", package: "ops", order: 3000 }),
|
|
snippet({ id: "zulu", label: "Zulu", package: "ops", order: 1000 }),
|
|
snippet({ id: "beta", label: "Beta", package: "ops", order: 2000 }),
|
|
],
|
|
packages: ["ops"],
|
|
expandedPaths: new Set(["ops"]),
|
|
});
|
|
|
|
assert.deepEqual(
|
|
rows.filter((row) => row.type === "snippet").map((row) => row.id),
|
|
["zulu", "beta", "alpha"],
|
|
);
|
|
});
|
|
|
|
test("scripts side panel active tabs pair the accent background with its foreground", () => {
|
|
assert.equal(source.match(/bg-accent text-accent-foreground/g)?.length, 2);
|
|
});
|
|
|
|
test("scripts side panel offers run-on-all-tabs for every snippet, not only automation scripts", () => {
|
|
// Guard against re-introducing the isScriptSnippet gate on workspace run actions.
|
|
assert.doesNotMatch(
|
|
source,
|
|
/onRunParallel=\{isScriptSnippet\(item\.row\.snippet\) && onRunScriptOnWorkspace/,
|
|
);
|
|
assert.match(source, /onRunParallel=\{onRunScriptOnWorkspace/);
|
|
// Sequential remains script-only (mode is meaningful only for automation runs).
|
|
assert.match(source, /onRunSequential=\{isScript && onRunScriptOnWorkspace/);
|
|
assert.match(source, /scripts\.actions\.runOnAllTabs/);
|
|
assert.match(source, /snippets\.action\.newPackage/);
|
|
assert.match(source, /openPackageDialog/);
|
|
});
|
|
|
|
test("scripts side panel add control is a split button with secondary create actions", () => {
|
|
assert.match(source, /ChevronDown/);
|
|
assert.match(source, /snippets\.action\.newScript/);
|
|
assert.match(source, /snippets\.action\.newSnippet/);
|
|
});
|
|
|
|
test("collectScriptsSidePanelPackagePaths includes implied ancestors", () => {
|
|
assert.deepEqual(
|
|
collectScriptsSidePanelPackagePaths(["ops/linux"], [
|
|
snippet({ id: "a", package: "ops/linux/disk" }),
|
|
snippet({ id: "b", package: "" }),
|
|
]).sort(),
|
|
["ops", "ops/linux", "ops/linux/disk"],
|
|
);
|
|
});
|
|
|
|
test("scripts side panel toolbar exposes expand, collapse, and delete-selected actions", () => {
|
|
assert.match(source, /vault\.tree\.expandAll/);
|
|
assert.match(source, /vault\.tree\.collapseAll/);
|
|
assert.match(source, /snippets\.selection\.deleteSelected/);
|
|
assert.match(source, /expandAllGroups/);
|
|
assert.match(source, /collapseAllGroups/);
|
|
assert.match(source, /deleteSelectedSnippets/);
|
|
assert.match(source, /isMultiSelectMode/);
|
|
});
|
|
|
|
test("scripts side panel confirms bulk delete and routes it through the shared delete event", () => {
|
|
// Mass-delete must confirm first (SnippetsManager parity) and must not bypass
|
|
// AppSideEffects via onSnippetsChange filtering — that path skips host binding cleanup.
|
|
assert.match(source, /VaultDeleteConfirmDialog/);
|
|
assert.match(source, /snippets\.selection\.deleteConfirmTitle/);
|
|
assert.match(source, /snippets\.selection\.deleteConfirmDesc/);
|
|
assert.match(source, /detail:\s*\{\s*ids\s*\}/);
|
|
assert.doesNotMatch(
|
|
source,
|
|
/onSnippetsChange\(snippets\.filter\(\(snippet\) => !selectedSnippetIds\.has/,
|
|
);
|
|
});
|
|
|
|
test("scripts side panel defers bulk-delete confirm when a parent owns the dialog", () => {
|
|
// Compact TerminalToolbar nests this panel in a Popover; the portalled confirm
|
|
// must be owned outside that tree so focus cannot unmount the prompt.
|
|
assert.match(source, /onBulkDeleteRequest\?:/);
|
|
assert.match(source, /if\s*\(\s*onBulkDeleteRequest\s*\)\s*\{/);
|
|
assert.match(source, /onBulkDeleteRequest\(ids\)/);
|
|
assert.match(source, /!onBulkDeleteRequest/);
|
|
});
|
|
|
|
test("scripts side panel clears pending bulk delete when the panel hides", () => {
|
|
// Returning null while isVisible is false unmounts the confirm dialog; drop
|
|
// pending deletes so a later re-show does not resurrect a half-dismissed prompt.
|
|
assert.match(
|
|
source,
|
|
/if\s*\(\s*!isVisible\s*\)\s*setPendingDeleteIds\(\s*null\s*\)/,
|
|
);
|
|
});
|
|
|
|
test("scripts side panel package dialog traps focus and exposes dialog close contract", () => {
|
|
assert.match(source, /packageDialogRef/);
|
|
assert.match(source, /data-dialog-close="true"/);
|
|
assert.match(source, /isPackageDialogOpen/);
|
|
});
|