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

173 lines
5.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import path from "path";
// We need to carefully handle the CJS module which uses require() internally.
// Import the functions exported from actionbook.js via the require.main guard.
const actionbookPath = path.resolve(__dirname, "..", "bin", "actionbook.js");
describe("actionbook.js wrapper", () => {
let wrapper: typeof import("../bin/actionbook.js");
beforeEach(() => {
// Clear module cache to get fresh imports
delete require.cache[require.resolve(actionbookPath)];
wrapper = require(actionbookPath);
});
describe("PLATFORM_PACKAGES", () => {
it("has entries for all 6 supported platforms", () => {
const expectedPlatforms = [
"darwin-arm64",
"darwin-x64",
"linux-x64",
"linux-arm64",
"win32-x64",
"win32-arm64",
];
for (const platform of expectedPlatforms) {
expect(wrapper.PLATFORM_PACKAGES[platform]).toBeDefined();
expect(wrapper.PLATFORM_PACKAGES[platform]).toMatch(
/^@actionbookdev\/cli-/
);
}
});
it("matches PLATFORM_PACKAGES in postinstall.js", () => {
const postinstallPath = path.resolve(
__dirname,
"..",
"scripts",
"postinstall.js"
);
delete require.cache[require.resolve(postinstallPath)];
const postinstall = require(postinstallPath);
expect(wrapper.PLATFORM_PACKAGES).toEqual(
postinstall.PLATFORM_PACKAGES
);
});
});
describe("isVersionOnlyFlag", () => {
it('returns true for ["--version"]', () => {
expect(wrapper.isVersionOnlyFlag(["--version"])).toBe(true);
});
it('returns true for ["-V"]', () => {
expect(wrapper.isVersionOnlyFlag(["-V"])).toBe(true);
});
it('returns false for ["--version", "search"]', () => {
expect(wrapper.isVersionOnlyFlag(["--version", "search"])).toBe(false);
});
it("returns false for empty args", () => {
expect(wrapper.isVersionOnlyFlag([])).toBe(false);
});
it('returns false for ["search", "--version"]', () => {
expect(wrapper.isVersionOnlyFlag(["search", "--version"])).toBe(false);
});
it('returns false for ["-v"] (lowercase)', () => {
expect(wrapper.isVersionOnlyFlag(["-v"])).toBe(false);
});
});
describe("isLikelyMusl", () => {
it("returns false on non-linux platforms", () => {
// On macOS/Windows test runners, this should return false
if (process.platform !== "linux") {
expect(wrapper.isLikelyMusl()).toBe(false);
}
});
it("returns false when process.report.getReport is not a function", () => {
// process.report is a getter-only property in modern Node.js,
// so we can't override it directly. Instead we verify the function
// handles the current environment correctly.
// On non-linux, it should always return false.
// On linux with glibc, it should return false.
const result = wrapper.isLikelyMusl();
expect(typeof result).toBe("boolean");
});
});
describe("resolvePackageDir", () => {
it("returns null when both strategies fail", () => {
// Use a fake package name that doesn't exist
const result = wrapper.resolvePackageDir(
"@actionbookdev/cli-nonexistent-platform"
);
expect(result).toBeNull();
});
it("resolves sibling directory when require.resolve fails", () => {
// The fallback checks for ../../{unscoped}/package.json
// relative to bin/actionbook.js (__dirname is bin/)
// This would be packages/{unscoped}/package.json
// In the monorepo, some platform packages may exist as siblings
const result = wrapper.resolvePackageDir(
"@actionbookdev/cli-nonexistent-xyz"
);
// The sibling dir cli-nonexistent-xyz won't exist
expect(result).toBeNull();
});
});
describe("getBinaryPath", () => {
let exitSpy: ReturnType<typeof vi.spyOn>;
let errorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
exitSpy = vi
.spyOn(process, "exit")
.mockImplementation((() => {
throw new Error("process.exit called");
}) as any);
errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
});
afterEach(() => {
exitSpy.mockRestore();
errorSpy.mockRestore();
});
it("exits with error for unsupported platform", () => {
expect(() => wrapper.getBinaryPath("freebsd-x64")).toThrow(
"process.exit called"
);
expect(exitSpy).toHaveBeenCalledWith(1);
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining("Unsupported platform")
);
});
it("exits with error when binary not found at resolved path", () => {
// All platform packages resolve to paths where the binary doesn't exist
// in the test environment (unless actually installed)
const currentPlatform = `${process.platform}-${process.arch}`;
if (wrapper.PLATFORM_PACKAGES[currentPlatform]) {
// This will either find the binary (and return a path) or exit
// In CI without platform packages, it will exit with missing package error
try {
const result = wrapper.getBinaryPath(currentPlatform);
// If it returns, the binary was found (local dev with platform package)
expect(typeof result).toBe("string");
} catch {
// If it throws, process.exit was called (expected in CI)
expect(exitSpy).toHaveBeenCalledWith(1);
}
}
});
it("shows supported platforms in error message", () => {
expect(() => wrapper.getBinaryPath("unknown-platform")).toThrow(
"process.exit called"
);
expect(errorSpy).toHaveBeenCalledWith(
expect.stringContaining("darwin-arm64")
);
});
});
});