mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
182 lines
8.6 KiB
TypeScript
182 lines
8.6 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
import {
|
|
buildRemoteFindCommand,
|
|
buildRemoteGrepCommand,
|
|
formatRemoteSearchOutput,
|
|
resolveRemoteSearchPath,
|
|
runRemoteFind,
|
|
runRemoteGrep,
|
|
} from "../src/remote-search.ts";
|
|
import type { RemoteExecOptions, RemoteTransport } from "../src/ssh2-transport.ts";
|
|
|
|
class SearchTransport implements RemoteTransport {
|
|
command = "";
|
|
cwd = "";
|
|
private readonly output: string;
|
|
private readonly exitCode: number | null;
|
|
constructor(output: string, exitCode: number | null = 0) {
|
|
this.output = output;
|
|
this.exitCode = exitCode;
|
|
}
|
|
connect(): Promise<void> { return Promise.resolve(); }
|
|
dispose(): Promise<void> { return Promise.resolve(); }
|
|
exec(command: string, cwd: string, options: RemoteExecOptions): Promise<{ exitCode: number | null }> {
|
|
this.command = command;
|
|
this.cwd = cwd;
|
|
options.onData(Buffer.from(this.output));
|
|
return Promise.resolve({ exitCode: this.exitCode });
|
|
}
|
|
capture(): Promise<{ exitCode: number | null; output: Buffer }> { throw new Error("not used"); }
|
|
readFile(): Promise<Buffer> { throw new Error("not used"); }
|
|
ensureReadable(): Promise<void> { throw new Error("not used"); }
|
|
ensureReadableWritable(): Promise<void> { throw new Error("not used"); }
|
|
detectImageMimeType(): Promise<string | null> { throw new Error("not used"); }
|
|
mkdir(): Promise<void> { throw new Error("not used"); }
|
|
writeFile(): Promise<void> { throw new Error("not used"); }
|
|
}
|
|
|
|
test("resolves remote search roots without using local filesystem semantics", () => {
|
|
assert.equal(resolveRemoteSearchPath(undefined, "/srv/app", "/home/build"), "/srv/app");
|
|
assert.equal(resolveRemoteSearchPath("src", "/srv/app", "/home/build"), "/srv/app/src");
|
|
assert.equal(resolveRemoteSearchPath("~/logs", "/srv/app", "/home/build"), "/home/build/logs");
|
|
assert.equal(resolveRemoteSearchPath("/var/log", "/srv/app", "/home/build"), "/var/log");
|
|
});
|
|
|
|
test("builds bounded capability-adaptive commands with shell-quoted user input", () => {
|
|
const find = buildRemoteFindCommand({ pattern: "it's-app", limit: 12 }, "/srv/app dir");
|
|
assert.match(find.command, /command -v fd/);
|
|
assert.match(find.command, /git-ls-files/);
|
|
assert.match(find.command, /head -n 13/);
|
|
assert.match(find.command, /'it'"'"'s-app'/);
|
|
assert.match(find.command, /'\/srv\/app dir'/);
|
|
|
|
const grep = buildRemoteGrepCommand({ pattern: "TODO", include: "*.ts", limit: 20 }, "/srv/app");
|
|
assert.match(grep.command, /command -v rg/);
|
|
assert.match(grep.command, /git-grep/);
|
|
assert.match(grep.command, /find .* -exec grep/);
|
|
assert.match(grep.command, /head -n 21/);
|
|
assert.throws(() => buildRemoteGrepCommand({ pattern: "bad\npattern" }, "/srv/app"), /newline/);
|
|
assert.throws(() => buildRemoteFindCommand({ pattern: "x", limit: 201 }, "/srv/app"), /limit/);
|
|
});
|
|
|
|
test("normalizes git paths, truncates rows, and reports the backend", () => {
|
|
const result = formatRemoteSearchOutput(
|
|
"__PI_SSH_SEARCH_BACKEND__:git-grep\nsrc/a.ts:2:TODO\nsrc/b.ts:3:TODO\nsrc/c.ts:4:TODO\n",
|
|
"/srv/app",
|
|
2,
|
|
"grep",
|
|
);
|
|
assert.equal(result.backend, "git-grep");
|
|
assert.equal(result.matchCount, 2);
|
|
assert.equal(result.truncated, true);
|
|
assert.match(result.text, /\/srv\/app\/src\/a\.ts:2:TODO/);
|
|
assert.doesNotMatch(result.text, /src\/c\.ts/);
|
|
});
|
|
|
|
test("executes remote find through the structured transport with bounded output", async () => {
|
|
const transport = new SearchTransport("__PI_SSH_SEARCH_BACKEND__:fd\n/srv/app/a.ts\n");
|
|
const result = await runRemoteFind(transport, { pattern: "a", limit: 5 }, "/srv/app", "/home/build");
|
|
assert.equal(transport.cwd, "/srv/app");
|
|
assert.match(transport.command, /command -v fd/);
|
|
assert.equal(result.matchCount, 1);
|
|
assert.match(result.text, /backend: fd/);
|
|
});
|
|
|
|
test("propagates backend failures without exposing the internal marker", async () => {
|
|
const transport = new SearchTransport(
|
|
"__PI_SSH_SEARCH_BACKEND__:git-grep\nfatal: invalid regular expression\n",
|
|
128,
|
|
);
|
|
await assert.rejects(
|
|
runRemoteGrep(transport, { pattern: "[", literal: false }, "/srv/app", "/home/build"),
|
|
(error: unknown) => {
|
|
assert.match(String(error), /remote grep failed with exit code 128: fatal: invalid regular expression/);
|
|
assert.doesNotMatch(String(error), /__PI_SSH_SEARCH_BACKEND__/);
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
test("adaptive commands preserve no-match success and propagate real backend errors", async () => {
|
|
const root = mkdtempSync(join(tmpdir(), "pi-ssh-search-"));
|
|
try {
|
|
mkdirSync(join(root, "src"));
|
|
writeFileSync(join(root, "src", "a.ts"), "const value = 'TODO';\n", "utf8");
|
|
execFileSync("/usr/bin/git", ["init", "-q", root]);
|
|
execFileSync("/usr/bin/git", ["-C", root, "add", "src/a.ts"]);
|
|
const environment = { ...process.env, PATH: "/usr/bin:/bin" };
|
|
const execute = (command: string) => execFileSync("/bin/bash", ["-c", command], {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
env: environment,
|
|
});
|
|
|
|
const find = buildRemoteFindCommand({ pattern: "a.ts", limit: 5 }, root);
|
|
const findResult = formatRemoteSearchOutput(execute(find.command), root, find.limit, "find");
|
|
assert.equal(findResult.backend, "git-ls-files");
|
|
assert.match(findResult.text, /src\/a\.ts/);
|
|
|
|
const grep = buildRemoteGrepCommand({ pattern: "TODO", include: "*.ts", limit: 5 }, root);
|
|
const grepResult = formatRemoteSearchOutput(execute(grep.command), root, grep.limit, "grep");
|
|
assert.equal(grepResult.backend, "git-grep");
|
|
assert.match(grepResult.text, /src\/a\.ts:1:/);
|
|
|
|
const noMatch = buildRemoteGrepCommand({ pattern: "ABSENT", include: "*.ts" }, root);
|
|
const noMatchProcess = spawnSync("/bin/bash", ["-c", noMatch.command], { cwd: root, encoding: "utf8", env: environment });
|
|
assert.equal(noMatchProcess.status, 0);
|
|
assert.equal(formatRemoteSearchOutput(noMatchProcess.stdout, root, noMatch.limit, "grep").matchCount, 0);
|
|
|
|
const invalidRegex = buildRemoteGrepCommand({ pattern: "[", literal: false }, root);
|
|
const invalidProcess = spawnSync("/bin/bash", ["-c", invalidRegex.command], { cwd: root, encoding: "utf8", env: environment });
|
|
assert.notEqual(invalidProcess.status, 0);
|
|
assert.match(`${invalidProcess.stdout}${invalidProcess.stderr}`, /git-grep|fatal|regular expression/i);
|
|
|
|
const missingRoot = join(root, "missing");
|
|
const missing = buildRemoteFindCommand({ pattern: "anything" }, missingRoot);
|
|
const missingProcess = spawnSync("/bin/bash", ["-c", missing.command], { cwd: root, encoding: "utf8", env: environment });
|
|
assert.notEqual(missingProcess.status, 0);
|
|
assert.match(`${missingProcess.stdout}${missingProcess.stderr}`, /find|No such file|not found/i);
|
|
|
|
const transport = new SearchTransport(execute(grep.command));
|
|
const throughTransport = await runRemoteGrep(transport, { pattern: "TODO", include: "*.ts" }, root, root);
|
|
assert.equal(throughTransport.matchCount, 1);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("propagates fd and ripgrep failures while preserving ripgrep no-match", () => {
|
|
const root = mkdtempSync(join(tmpdir(), "pi-ssh-search-backends-"));
|
|
try {
|
|
const bin = join(root, "bin");
|
|
mkdirSync(bin);
|
|
const fd = join(bin, "fd");
|
|
const rg = join(bin, "rg");
|
|
writeFileSync(fd, "#!/bin/sh\necho 'fd exploded' >&2\nexit 3\n", { mode: 0o755 });
|
|
writeFileSync(rg, "#!/bin/sh\necho 'rg exploded' >&2\nexit 2\n", { mode: 0o755 });
|
|
const environment = { ...process.env, PATH: `${bin}:/usr/bin:/bin` };
|
|
|
|
const find = buildRemoteFindCommand({ pattern: "anything" }, root);
|
|
const failedFind = spawnSync("/bin/bash", ["-c", find.command], { cwd: root, encoding: "utf8", env: environment });
|
|
assert.equal(failedFind.status, 3);
|
|
assert.match(failedFind.stdout, /fd exploded/);
|
|
|
|
const grep = buildRemoteGrepCommand({ pattern: "anything" }, root);
|
|
const failedGrep = spawnSync("/bin/bash", ["-c", grep.command], { cwd: root, encoding: "utf8", env: environment });
|
|
assert.equal(failedGrep.status, 2);
|
|
assert.match(failedGrep.stdout, /rg exploded/);
|
|
|
|
writeFileSync(rg, "#!/bin/sh\nexit 1\n", { mode: 0o755 });
|
|
const noMatch = spawnSync("/bin/bash", ["-c", grep.command], { cwd: root, encoding: "utf8", env: environment });
|
|
assert.equal(noMatch.status, 0);
|
|
assert.equal(formatRemoteSearchOutput(noMatch.stdout, root, grep.limit, "grep").matchCount, 0);
|
|
} finally {
|
|
rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|