Files
s1mba 731cc340d8 [packages/cli] Add comprehensive CLI test suite (#128)
* [packages/cli]test: add comprehensive CLI test suite with E2E browser tests

- Add unit tests for wrapper (actionbook.js), postinstall, config, profile,
  search, get, and sources commands
- Add Tier 1 browser tests (arg validation, help output, defaults)
- Add Tier 2 browser E2E tests (navigation, interaction, screenshots,
  cookies, tabs, error handling, JSON output)
- Fix E2E test reliability: add --wait for element readiness after navigation,
  use eval-based form submit for login flow, match actual CLI exit codes
- Suppress macOS Keychain dialogs in tests via --use-mock-keychain config
- Set testTimeout to 60s for browser E2E operations
- Export modules from actionbook.js and postinstall.js for testability

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* [packages/cli]fix: resolve Windows .exe suffix when locating test CLI binary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* [packages/cli]fix: skip shell-script mock tests on Windows

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 14:36:15 +08:00

142 lines
3.2 KiB
TypeScript

import { execFile } from "child_process";
import { existsSync } from "fs";
import path from "path";
const BINARY_NAME =
process.platform === "win32" ? "actionbook.exe" : "actionbook";
/**
* Resolve the actionbook binary path.
* Priority: ACTIONBOOK_BINARY_PATH env > cargo debug build > empty string (skip).
*/
export function getActionbookBinary(): string {
if (process.env.ACTIONBOOK_BINARY_PATH) {
return process.env.ACTIONBOOK_BINARY_PATH;
}
const cargoBinary = path.resolve(
__dirname,
"..",
"..",
"..",
"actionbook-rs",
"target",
"debug",
BINARY_NAME
);
if (existsSync(cargoBinary)) {
return cargoBinary;
}
// Also check release build
const releaseBinary = path.resolve(
__dirname,
"..",
"..",
"..",
"actionbook-rs",
"target",
"release",
BINARY_NAME
);
if (existsSync(releaseBinary)) {
return releaseBinary;
}
return "";
}
export interface RunCliResult {
stdout: string;
stderr: string;
exitCode: number | null;
}
/**
* Run `node bin/actionbook.js` (the Node.js wrapper) with given args and env.
* Used for testing the wrapper itself.
*/
export function runWrapper(
args: string[] = [],
options: {
env?: Record<string, string>;
timeout?: number;
} = {}
): Promise<RunCliResult> {
const wrapperPath = path.resolve(__dirname, "..", "..", "bin", "actionbook.js");
const timeoutMs = options.timeout ?? 15000;
return new Promise((resolve) => {
const child = execFile(
process.execPath,
[wrapperPath, ...args],
{
env: { ...process.env, ...options.env },
timeout: timeoutMs,
maxBuffer: 10 * 1024 * 1024,
},
(error, stdout, stderr) => {
const exitCode =
error && "code" in error
? (error as NodeJS.ErrnoException & { code?: number }).code ??
(error as any).status ??
null
: 0;
resolve({
stdout: stdout.toString(),
stderr: stderr.toString(),
exitCode:
child.exitCode !== null ? child.exitCode : exitCode,
});
}
);
});
}
/**
* Run the actionbook CLI binary with the given arguments.
* Returns stdout, stderr, and exit code.
*/
export function runCli(
args: string[],
options: {
env?: Record<string, string>;
timeout?: number;
} = {}
): Promise<RunCliResult> {
const binary = getActionbookBinary();
if (!binary) {
return Promise.reject(new Error("Actionbook binary not found"));
}
const timeoutMs = options.timeout ?? 15000;
return new Promise((resolve) => {
const child = execFile(
binary,
args,
{
env: { ...process.env, ...options.env },
timeout: timeoutMs,
maxBuffer: 10 * 1024 * 1024,
},
(error, stdout, stderr) => {
const exitCode =
error && "code" in error
? (error as NodeJS.ErrnoException & { code?: number }).code ??
(error as any).status ??
null
: 0;
resolve({
stdout: stdout.toString(),
stderr: stderr.toString(),
exitCode:
child.exitCode !== null ? child.exitCode : exitCode,
});
}
);
});
}