[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
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:
506
components/terminal/completionEngine.test.ts
Normal file
506
components/terminal/completionEngine.test.ts
Normal file
@@ -0,0 +1,506 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import type { FigSpec } from "./autocomplete/figSpecLoader.ts";
|
||||
|
||||
type LocalStorageMock = {
|
||||
clear(): void;
|
||||
getItem(key: string): string | null;
|
||||
setItem(key: string, value: string): void;
|
||||
removeItem(key: string): void;
|
||||
};
|
||||
|
||||
type MockDirEntry = {
|
||||
name: string;
|
||||
type: "file" | "directory" | "symlink";
|
||||
};
|
||||
|
||||
function installLocalStorage(): LocalStorageMock {
|
||||
const store = new Map<string, string>();
|
||||
const localStorage: LocalStorageMock = {
|
||||
clear() {
|
||||
store.clear();
|
||||
},
|
||||
getItem(key: string) {
|
||||
return store.has(key) ? store.get(key)! : null;
|
||||
},
|
||||
setItem(key: string, value: string) {
|
||||
store.set(key, String(value));
|
||||
},
|
||||
removeItem(key: string) {
|
||||
store.delete(key);
|
||||
},
|
||||
};
|
||||
Object.defineProperty(globalThis, "localStorage", {
|
||||
value: localStorage,
|
||||
configurable: true,
|
||||
});
|
||||
return localStorage;
|
||||
}
|
||||
|
||||
const localStorage = installLocalStorage();
|
||||
const storySpec: FigSpec = {
|
||||
name: "story",
|
||||
subcommands: [
|
||||
{
|
||||
name: "open",
|
||||
args: { template: "filepaths" },
|
||||
},
|
||||
{
|
||||
name: "pick",
|
||||
args: { name: "item", generators: {} },
|
||||
},
|
||||
],
|
||||
};
|
||||
const bridgeState: {
|
||||
localEntries: MockDirEntry[];
|
||||
remoteEntriesByPath: Map<string, MockDirEntry[]>;
|
||||
remoteCalls: string[];
|
||||
remoteDelayMs: number;
|
||||
} = {
|
||||
localEntries: [],
|
||||
remoteEntriesByPath: new Map(),
|
||||
remoteCalls: [],
|
||||
remoteDelayMs: 0,
|
||||
};
|
||||
|
||||
Object.defineProperty(globalThis, "window", {
|
||||
value: {
|
||||
netcatty: {
|
||||
listFigSpecs: async () => ["story"],
|
||||
loadFigSpec: async (commandName: string) => commandName === "story" ? storySpec : null,
|
||||
listAutocompleteLocalDir: async (
|
||||
_path: string,
|
||||
foldersOnly: boolean,
|
||||
filterPrefix?: string,
|
||||
limit?: number,
|
||||
) => {
|
||||
const prefix = (filterPrefix ?? "").toLowerCase();
|
||||
const entries = bridgeState.localEntries
|
||||
.filter((entry) => !foldersOnly || entry.type === "directory")
|
||||
.filter((entry) => !prefix || entry.name.toLowerCase().startsWith(prefix))
|
||||
.slice(0, limit ?? bridgeState.localEntries.length);
|
||||
return { success: true, entries };
|
||||
},
|
||||
listAutocompleteRemoteDir: async (
|
||||
_sessionId: string,
|
||||
path: string,
|
||||
foldersOnly: boolean,
|
||||
filterPrefix?: string,
|
||||
limit?: number,
|
||||
) => {
|
||||
bridgeState.remoteCalls.push(path);
|
||||
if (bridgeState.remoteDelayMs > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, bridgeState.remoteDelayMs));
|
||||
}
|
||||
const prefix = (filterPrefix ?? "").toLowerCase();
|
||||
const remoteEntries = bridgeState.remoteEntriesByPath.get(path) ?? [];
|
||||
const entries = remoteEntries
|
||||
.filter((entry) => !foldersOnly || entry.type === "directory")
|
||||
.filter((entry) => !prefix || entry.name.toLowerCase().startsWith(prefix))
|
||||
.slice(0, limit ?? remoteEntries.length);
|
||||
return { success: true, entries };
|
||||
},
|
||||
},
|
||||
},
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const {
|
||||
getCompletions,
|
||||
getPathSuggestionsWithinBudget,
|
||||
} = await import("./autocomplete/completionEngine.ts");
|
||||
const {
|
||||
clearHistory,
|
||||
recordCommand,
|
||||
removeCommandHistoryEntry,
|
||||
} = await import("./autocomplete/commandHistoryStore.ts");
|
||||
const {
|
||||
normalizePathTokenForLookup,
|
||||
shouldPreferRemoteShellCwd,
|
||||
} = await import("./autocomplete/remotePathCompleter.ts");
|
||||
|
||||
test.beforeEach(() => {
|
||||
localStorage.clear();
|
||||
clearHistory();
|
||||
bridgeState.localEntries = [{ name: "package.json", type: "file" }];
|
||||
bridgeState.remoteEntriesByPath = new Map();
|
||||
bridgeState.remoteCalls = [];
|
||||
bridgeState.remoteDelayMs = 0;
|
||||
});
|
||||
|
||||
test("getCompletions prioritizes spec-driven path suggestions over history", async () => {
|
||||
recordCommand("story open package-lock.json", "host-1");
|
||||
|
||||
const completions = await getCompletions("story open pa", {
|
||||
hostId: "host-1",
|
||||
protocol: "local",
|
||||
cwd: "/repo",
|
||||
});
|
||||
|
||||
assert.ok(completions.length > 0);
|
||||
assert.equal(completions[0]?.source, "path");
|
||||
assert.equal(completions[0]?.text, "story open package.json");
|
||||
|
||||
const historyIndex = completions.findIndex((entry) =>
|
||||
entry.source === "history" && entry.text === "story open package-lock.json"
|
||||
);
|
||||
assert.ok(historyIndex > 0);
|
||||
assert.equal(completions[historyIndex]?.historyMatch, "path-argument");
|
||||
});
|
||||
|
||||
test("path completion marks a matching history replacement even when its full line is shorter", async () => {
|
||||
const historyCommand = "story package.json";
|
||||
const input = "story open --number p";
|
||||
recordCommand(historyCommand, "host-1");
|
||||
|
||||
const completions = await getCompletions(input, {
|
||||
hostId: "host-1",
|
||||
protocol: "local",
|
||||
cwd: "/repo",
|
||||
});
|
||||
const history = completions.find((entry) => entry.text === historyCommand);
|
||||
|
||||
assert.ok(history);
|
||||
assert.ok(history.text.length < input.length);
|
||||
assert.equal(history.historyMatch, "path-argument");
|
||||
});
|
||||
|
||||
test("getCompletions does not treat generator-only spec args as path contexts", async () => {
|
||||
recordCommand("story pick package-choice", "host-1");
|
||||
|
||||
const completions = await getCompletions("story pick pa", {
|
||||
hostId: "host-1",
|
||||
protocol: "local",
|
||||
cwd: "/repo",
|
||||
});
|
||||
|
||||
assert.ok(completions.length > 0);
|
||||
assert.equal(completions[0]?.source, "history");
|
||||
assert.equal(completions[0]?.text, "story pick package-choice");
|
||||
assert.equal(completions.some((entry) => entry.source === "path"), false);
|
||||
});
|
||||
|
||||
test("history suggestions stop when an edited argument no longer matches the command prefix", async () => {
|
||||
const historyCommand = "python3.14 -m robot -d /home/wx0043/Desktop/suite9";
|
||||
recordCommand(historyCommand, "host-1");
|
||||
|
||||
const matching = await getCompletions("python3.14 -m r", {
|
||||
hostId: "host-1",
|
||||
historyScope: "host",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
assert.equal(
|
||||
matching.some((entry) => entry.source === "history" && entry.text === historyCommand),
|
||||
true,
|
||||
);
|
||||
|
||||
const changedArgument = await getCompletions("python3.14 -m p", {
|
||||
hostId: "host-1",
|
||||
historyScope: "host",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
assert.equal(
|
||||
changedArgument.some((entry) => entry.source === "history" && entry.text === historyCommand),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("single-token history queries retain fuzzy command-name matching", async () => {
|
||||
const historyCommand = "docker compose up";
|
||||
recordCommand(historyCommand, "host-1");
|
||||
|
||||
const completions = await getCompletions("dcu", {
|
||||
hostId: "host-1",
|
||||
historyScope: "host",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
completions.some((entry) => entry.source === "history" && entry.text === historyCommand),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test("removeCommandHistoryEntry removes only the matching host's autocomplete record", async () => {
|
||||
recordCommand("bad-command --flag", "host-1");
|
||||
recordCommand("bad-command --flag", "host-2");
|
||||
|
||||
removeCommandHistoryEntry("bad-command --flag", "host-1");
|
||||
|
||||
const completions = await getCompletions("bad-command", {
|
||||
hostId: "host-1",
|
||||
historyScope: "global",
|
||||
protocol: "local",
|
||||
cwd: "/repo",
|
||||
});
|
||||
assert.equal(
|
||||
completions.some((entry) => entry.source === "history" && entry.text === "bad-command --flag"),
|
||||
true,
|
||||
);
|
||||
|
||||
const hostOnlyCompletions = await getCompletions("bad-command", {
|
||||
hostId: "host-1",
|
||||
historyScope: "host",
|
||||
protocol: "local",
|
||||
cwd: "/repo",
|
||||
});
|
||||
assert.equal(
|
||||
hostOnlyCompletions.some((entry) => entry.source === "history" && entry.text === "bad-command --flag"),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("removeCommandHistoryEntry trims command text like recordCommand", async () => {
|
||||
recordCommand("padded-cmd", "host-trim");
|
||||
assert.equal(removeCommandHistoryEntry(" padded-cmd ", "host-trim"), true);
|
||||
|
||||
const hostOnlyCompletions = await getCompletions("padded", {
|
||||
hostId: "host-trim",
|
||||
historyScope: "host",
|
||||
protocol: "local",
|
||||
cwd: "/repo",
|
||||
});
|
||||
assert.equal(
|
||||
hostOnlyCompletions.some((entry) => entry.source === "history" && entry.text === "padded-cmd"),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("getCompletions uses the remote shell cwd for relative path arguments instead of stale home", async () => {
|
||||
bridgeState.remoteEntriesByPath.set("~", [{ name: "home-only.txt", type: "file" }]);
|
||||
bridgeState.remoteEntriesByPath.set(".", [{ name: "worktree.txt", type: "file" }]);
|
||||
|
||||
const completions = await getCompletions("cat wo", {
|
||||
hostId: "host-1",
|
||||
os: "linux",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-1",
|
||||
cwd: "~",
|
||||
});
|
||||
|
||||
assert.deepEqual(bridgeState.remoteCalls, ["."]);
|
||||
assert.equal(completions[0]?.source, "path");
|
||||
assert.equal(completions[0]?.text, "cat worktree.txt");
|
||||
assert.equal(completions.some((entry) => entry.text.includes("~")), false);
|
||||
});
|
||||
|
||||
test("getCompletions uses absolute prompt cwd for remote relative path arguments", async () => {
|
||||
bridgeState.remoteEntriesByPath.set(".", [{ name: "old-user-file.txt", type: "file" }]);
|
||||
bridgeState.remoteEntriesByPath.set("/etc", [{ name: "passwd", type: "file" }]);
|
||||
|
||||
const completions = await getCompletions("cat pa", {
|
||||
hostId: "host-1",
|
||||
os: "linux",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-1",
|
||||
cwd: "/etc",
|
||||
cwdSource: "prompt",
|
||||
});
|
||||
|
||||
assert.deepEqual(bridgeState.remoteCalls, ["/etc"]);
|
||||
assert.equal(completions[0]?.source, "path");
|
||||
assert.equal(completions[0]?.text, "cat passwd");
|
||||
assert.equal(completions.some((entry) => entry.text === "cat old-user-file.txt"), false);
|
||||
});
|
||||
|
||||
test("remote subdirectory lookups keep absolute prompt cwd", () => {
|
||||
const preferRelativeCwd = shouldPreferRemoteShellCwd("ssh", "session-1", "linux", "/etc", "prompt");
|
||||
|
||||
assert.equal(preferRelativeCwd, false);
|
||||
assert.equal(
|
||||
normalizePathTokenForLookup("pam.d/", "/etc", { preferRelativeCwd }),
|
||||
"/etc/pam.d/",
|
||||
);
|
||||
});
|
||||
|
||||
test("getCompletions keeps remote shell cwd when absolute cwd is only a fallback", async () => {
|
||||
bridgeState.remoteEntriesByPath.set("/old", [{ name: "old-user-file.txt", type: "file" }]);
|
||||
bridgeState.remoteEntriesByPath.set(".", [{ name: "worktree.txt", type: "file" }]);
|
||||
|
||||
const completions = await getCompletions("cat wo", {
|
||||
hostId: "host-1",
|
||||
os: "linux",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-1",
|
||||
cwd: "/old",
|
||||
cwdSource: "fallback",
|
||||
});
|
||||
|
||||
assert.deepEqual(bridgeState.remoteCalls, ["."]);
|
||||
assert.equal(completions[0]?.source, "path");
|
||||
assert.equal(completions[0]?.text, "cat worktree.txt");
|
||||
assert.equal(completions.some((entry) => entry.text === "cat old-user-file.txt"), false);
|
||||
});
|
||||
|
||||
test("getCompletions does not reuse cached remote relative listings after cwd changes", async () => {
|
||||
bridgeState.remoteEntriesByPath.set(".", [{ name: "home-only.txt", type: "file" }]);
|
||||
|
||||
await getCompletions("cat ", {
|
||||
hostId: "host-1",
|
||||
os: "linux",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
bridgeState.remoteEntriesByPath.set(".", [{ name: "worktree.txt", type: "file" }]);
|
||||
|
||||
const completions = await getCompletions("cat wo", {
|
||||
hostId: "host-1",
|
||||
os: "linux",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-1",
|
||||
});
|
||||
|
||||
assert.equal(bridgeState.remoteCalls.length, 2);
|
||||
assert.equal(completions[0]?.text, "cat worktree.txt");
|
||||
});
|
||||
|
||||
test("getCompletions does not reuse in-flight remote relative listings after cwd changes", async () => {
|
||||
bridgeState.remoteDelayMs = 150;
|
||||
bridgeState.remoteEntriesByPath.set(".", [{ name: "home-only.txt", type: "file" }]);
|
||||
|
||||
const first = getCompletions("cat ", {
|
||||
hostId: "host-1",
|
||||
os: "linux",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-inflight-cwd",
|
||||
pathBudgetMs: Infinity,
|
||||
});
|
||||
|
||||
// First listing must be in flight before the shell cwd listing changes.
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
bridgeState.remoteEntriesByPath.set(".", [{ name: "worktree.txt", type: "file" }]);
|
||||
|
||||
const second = await getCompletions("cat wo", {
|
||||
hostId: "host-1",
|
||||
os: "linux",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-inflight-cwd",
|
||||
pathBudgetMs: Infinity,
|
||||
});
|
||||
await first;
|
||||
|
||||
assert.equal(bridgeState.remoteCalls.length, 2);
|
||||
assert.equal(second[0]?.text, "cat worktree.txt");
|
||||
assert.equal(second.some((entry) => entry.text === "cat home-only.txt"), false);
|
||||
});
|
||||
|
||||
test("getCompletions returns local history before a slow remote path listing finishes", async () => {
|
||||
recordCommand("cat worktree.txt", "host-1");
|
||||
bridgeState.remoteDelayMs = 250;
|
||||
bridgeState.remoteEntriesByPath.set(".", [{ name: "worktree.txt", type: "file" }]);
|
||||
|
||||
const started = Date.now();
|
||||
const completions = await getCompletions("cat wo", {
|
||||
hostId: "host-1",
|
||||
os: "linux",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-slow-path",
|
||||
cwd: "~",
|
||||
pathBudgetMs: 40,
|
||||
});
|
||||
const elapsed = Date.now() - started;
|
||||
|
||||
assert.ok(elapsed < 200, `expected local suggestions within budget, took ${elapsed}ms`);
|
||||
assert.ok(
|
||||
completions.some((entry) => entry.source === "history" && entry.text === "cat worktree.txt"),
|
||||
);
|
||||
assert.equal(completions.some((entry) => entry.source === "path"), false);
|
||||
});
|
||||
|
||||
test("getCompletions surfaces late path suggestions for cache-bypassed relative SSH cwd", async () => {
|
||||
recordCommand("cat worktree.txt", "host-1");
|
||||
bridgeState.remoteDelayMs = 250;
|
||||
bridgeState.remoteEntriesByPath.set(".", [{ name: "worktree.txt", type: "file" }]);
|
||||
|
||||
let latePathSuggestions: Awaited<ReturnType<typeof getCompletions>> | null = null;
|
||||
const latePathPromise = new Promise<void>((resolve) => {
|
||||
void getCompletions("cat wo", {
|
||||
hostId: "host-1",
|
||||
os: "linux",
|
||||
protocol: "ssh",
|
||||
sessionId: "session-late-path",
|
||||
cwd: "/stale-fallback",
|
||||
cwdSource: "fallback",
|
||||
pathBudgetMs: 40,
|
||||
onLatePathSuggestions: (suggestions) => {
|
||||
latePathSuggestions = suggestions;
|
||||
resolve();
|
||||
},
|
||||
}).then((completions) => {
|
||||
assert.equal(completions.some((entry) => entry.source === "path"), false);
|
||||
assert.ok(
|
||||
completions.some((entry) => entry.source === "history" && entry.text === "cat worktree.txt"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await latePathPromise;
|
||||
assert.ok(latePathSuggestions);
|
||||
assert.equal(latePathSuggestions![0]?.source, "path");
|
||||
assert.equal(latePathSuggestions![0]?.text, "cat worktree.txt");
|
||||
});
|
||||
|
||||
test("getPathSuggestionsWithinBudget ignores late rejections after the soft timeout", async () => {
|
||||
const unhandled: unknown[] = [];
|
||||
const onUnhandled = (reason: unknown) => {
|
||||
unhandled.push(reason);
|
||||
};
|
||||
process.on("unhandledRejection", onUnhandled);
|
||||
try {
|
||||
const pathPromise = new Promise<{ name: string; type: "file" }[]>((_resolve, reject) => {
|
||||
setTimeout(() => reject(new Error("late path failure")), 80);
|
||||
});
|
||||
const entries = await getPathSuggestionsWithinBudget(pathPromise, 20);
|
||||
assert.deepEqual(entries, []);
|
||||
await new Promise((resolve) => setTimeout(resolve, 120));
|
||||
assert.equal(unhandled.length, 0);
|
||||
} finally {
|
||||
process.off("unhandledRejection", onUnhandled);
|
||||
}
|
||||
});
|
||||
|
||||
test("getPathSuggestionsWithinBudget delivers late entries after the soft timeout", async () => {
|
||||
const pathPromise = new Promise<{ name: string; type: "file" }[]>((resolve) => {
|
||||
setTimeout(() => resolve([{ name: "late.txt", type: "file" }]), 80);
|
||||
});
|
||||
let lateEntries: { name: string; type: "file" }[] | null = null;
|
||||
const entries = await getPathSuggestionsWithinBudget(pathPromise, 20, (late) => {
|
||||
lateEntries = late as { name: string; type: "file" }[];
|
||||
});
|
||||
assert.deepEqual(entries, []);
|
||||
await new Promise((resolve) => setTimeout(resolve, 120));
|
||||
assert.deepEqual(lateEntries, [{ name: "late.txt", type: "file" }]);
|
||||
});
|
||||
|
||||
test("getCompletions includes other hosts' history when historyScope is global", async () => {
|
||||
recordCommand("systemctl restart nginx", "host-a");
|
||||
recordCommand("systemctl status nginx", "host-b");
|
||||
|
||||
const hostScoped = await getCompletions("systemctl", {
|
||||
hostId: "host-a",
|
||||
historyScope: "host",
|
||||
});
|
||||
assert.ok(
|
||||
hostScoped.some((entry) => entry.source === "history" && entry.text === "systemctl restart nginx"),
|
||||
);
|
||||
assert.equal(
|
||||
hostScoped.some((entry) => entry.source === "history" && entry.text === "systemctl status nginx"),
|
||||
false,
|
||||
);
|
||||
|
||||
const globalScoped = await getCompletions("systemctl", {
|
||||
hostId: "host-a",
|
||||
historyScope: "global",
|
||||
});
|
||||
assert.ok(
|
||||
globalScoped.some((entry) => entry.source === "history" && entry.text === "systemctl restart nginx"),
|
||||
);
|
||||
assert.ok(
|
||||
globalScoped.some((entry) => entry.source === "history" && entry.text === "systemctl status nginx"),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user