Files
cline/apps/cli/src/main.test.ts
T
Saoud RizwanandSaoud Rizwan e10183f2a8 Add Cline Desktop launch notice to the CLI (#14123)
* Add Cline Desktop launch CTA to extension home banner and CLI startup notice

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Show Cline Desktop CTA on all platforms

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

* Drop hardcoded extension desktop banner in favor of remote banner

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>

---------

Co-authored-by: Saoud Rizwan <saoudrizwan@users.noreply.github.com>
2026-09-14 16:11:36 -07:00

2019 lines
61 KiB
TypeScript

import { fstatSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type {
CliMigrationNotice,
CliMigrationNoticeOptions,
} from "./kanban-migration/notice";
/** Real `fstatSync`: used when tests stub only stdin (fd 0); throwing for every fd breaks imports and session I/O. */
const fsActual = vi.hoisted(() => ({
realFstatSync: null as null | typeof import("node:fs").fstatSync,
}));
vi.mock("node:fs", async () => {
const actual = await vi.importActual<typeof import("node:fs")>("node:fs");
fsActual.realFstatSync = actual.fstatSync;
return { ...actual, fstatSync: vi.fn(actual.fstatSync) };
});
const originalArgv = [...process.argv];
const originalStdinIsTTY = process.stdin.isTTY;
const originalStdoutIsTTY = process.stdout.isTTY;
const originalGlobalSettingsPath = process.env.CLINE_GLOBAL_SETTINGS_PATH;
const mockState = vi.hoisted(() => ({
runAgentImports: 0,
runInteractiveImports: 0,
runAgentCalls: 0,
}));
const authMocks = vi.hoisted(() => ({
ensureOAuthProviderApiKey: vi.fn(),
getPersistedProviderApiKey: vi.fn(() => undefined),
isOAuthProvider: vi.fn(() => false),
normalizeProviderId: vi.fn((providerId?: string) => providerId ?? "cline"),
parseAuthCommandArgs: vi.fn(),
runAuthCommand: vi.fn(),
}));
const providerSettingsMocks = vi.hoisted(() => ({
getLastUsedProviderSettings: vi.fn<(options?: unknown) => unknown>(
() => undefined,
),
getProviderConfig: vi.fn<(providerId: string, options?: unknown) => unknown>(
() => undefined,
),
getProviderSettings: vi.fn<(providerId: string) => unknown>(() => undefined),
saveProviderSettings: vi.fn<(settings: unknown, options?: unknown) => void>(
() => {},
),
}));
const sessionMocks = vi.hoisted(() => ({
deleteSession: vi.fn(),
getSessionRow: vi.fn(),
listSessions: vi.fn(async () => []),
updateSession: vi.fn(),
}));
const llmMocks = vi.hoisted(() => ({
resolveProviderConfig: vi.fn(async (): Promise<unknown> => undefined),
}));
const promptMocks = vi.hoisted(() => ({
resolveSystemPrompt: vi.fn(async () => "system prompt"),
}));
const kanbanMocks = vi.hoisted(() => ({
launchKanban: vi.fn(),
}));
const dashboardMocks = vi.hoisted(() => ({
runDashboardCommand: vi.fn(),
}));
const connectMocks = vi.hoisted(() => ({
formatAdapterList: vi.fn(() => ""),
runCleanupConnectorInstance: vi.fn(async () => 0),
runConnectAdapter: vi.fn(async () => 0),
runRestartConnector: vi.fn(async () => 0),
runStopAllConnectors: vi.fn(async () => 0),
runStopConnector: vi.fn(async () => 0),
}));
const migrationNoticeMocks = vi.hoisted(() => ({
getClineCliMigrationNotice: vi.fn<
(
dataDir?: string,
env?: NodeJS.ProcessEnv,
options?: CliMigrationNoticeOptions,
) => CliMigrationNotice | undefined
>(() => undefined),
markClineCliMigrationNoticeShown: vi.fn(),
}));
const updateMocks = vi.hoisted(() => ({
autoUpdateOnStartup: vi.fn(),
checkForUpdates: vi.fn(async () => 0),
getPreferredKanbanInstaller: vi.fn(() => undefined),
}));
const runtimeMocks = vi.hoisted(() => ({
runAgent: vi.fn(async () => {
mockState.runAgentCalls += 1;
}),
runInteractive: vi.fn(),
}));
const worktreeMocks = vi.hoisted(() => ({
createTaskWorktree: vi.fn(),
}));
const historyMocks = vi.hoisted(() => ({
runHistoryList: vi.fn<() => Promise<number>>(async () => 0),
runHistoryDelete: vi.fn(async () => 0),
runHistoryExport: vi.fn(async () => 0),
runHistoryUpdate: vi.fn(async () => 0),
}));
const loggingMocks = vi.hoisted(() => ({
createCliLoggerAdapter: vi.fn(() => ({
core: {
debug: vi.fn(),
log: vi.fn(),
error: vi.fn(),
},
runtimeConfig: undefined,
})),
flushCliLoggerAdapters: vi.fn(),
}));
const hubRuntimeMocks = vi.hoisted(() => ({
ensureCliHubServer: vi.fn(async () => ({
url: "ws://127.0.0.1:25463",
authToken: "test-token",
})),
}));
const telemetryMocks = vi.hoisted(() => ({
captureCliExtensionActivated: vi.fn(),
identifyTelemetryAccount: vi.fn(),
getCliTelemetryService: vi.fn(),
disposeCliTelemetryService: vi.fn(async () => {}),
}));
const featureFlagMocks = vi.hoisted(() => ({
getBooleanFlagEnabled: vi.fn(() => false),
setCliFeatureFlagsAccountContext: vi.fn(),
refreshCliFeatureFlagsInBackground: vi.fn(),
}));
function forcePromptModeInput() {
Object.defineProperty(process.stdin, "isTTY", {
value: true,
configurable: true,
});
vi.mocked(fstatSync).mockImplementation((fd, ...rest) => {
if (fd === 0) {
throw new Error("stdin not piped");
}
const real = fsActual.realFstatSync;
if (!real) {
throw new Error("node:fs fstatSync mock not initialized");
}
return real(fd, ...rest) as
| import("node:fs").Stats
| import("node:fs").BigIntStats;
});
}
vi.mock("./runtime/run-agent", () => {
mockState.runAgentImports += 1;
return {
runAgent: runtimeMocks.runAgent,
};
});
vi.mock("./runtime/run-interactive", () => {
mockState.runInteractiveImports += 1;
return {
runInteractive: runtimeMocks.runInteractive,
};
});
vi.mock("./utils/session", () => sessionMocks);
vi.mock("./session/session", () => sessionMocks);
vi.mock("@cline/core", async () => {
// Keep dispatch tests independent of the full SDK runtime import graph.
// Only persisted-settings behavior needs its real implementation here.
const { readGlobalSettings } = await vi.importActual<
typeof import("../../../sdk/packages/core/src/services/global-settings")
>("../../../sdk/packages/core/src/services/global-settings");
return {
readGlobalSettings,
setSdkLogger: vi.fn(),
resolveProviderConfig: llmMocks.resolveProviderConfig,
createTeamName: vi.fn(() => "team-test"),
createUserInstructionConfigService: vi.fn(() => ({
start: vi.fn(async () => {}),
stop: vi.fn(),
})),
ProviderSettingsManager: class {
getLastUsedProviderSettings(options?: unknown) {
return providerSettingsMocks.getLastUsedProviderSettings(options);
}
getProviderSettings(providerId: string) {
return providerSettingsMocks.getProviderSettings(providerId);
}
getProviderConfig(providerId: string, options?: unknown) {
return providerSettingsMocks.getProviderConfig(providerId, options);
}
saveProviderSettings(settings: unknown, options?: unknown) {
providerSettingsMocks.saveProviderSettings(settings, options);
}
},
};
});
vi.mock("./utils/provider-auth", () => authMocks);
vi.mock("./utils/feature-flags", () => ({
getCliFeatureFlagsService: () => ({
getBooleanFlagEnabled: featureFlagMocks.getBooleanFlagEnabled,
}),
refreshCliFeatureFlagsInBackground:
featureFlagMocks.refreshCliFeatureFlagsInBackground,
setCliFeatureFlagsAccountContext:
featureFlagMocks.setCliFeatureFlagsAccountContext,
}));
vi.mock("./runtime/prompt", () => ({
resolveSystemPrompt: promptMocks.resolveSystemPrompt,
}));
vi.mock("./commands/kanban", () => kanbanMocks);
vi.mock("./commands/dashboard", () => dashboardMocks);
vi.mock("./commands/connect", () => connectMocks);
vi.mock("./kanban-migration/notice", () => migrationNoticeMocks);
vi.mock("./commands/update", () => updateMocks);
vi.mock("./commands/history", () => historyMocks);
vi.mock("./logging/adapter", () => loggingMocks);
vi.mock("./utils/hub-runtime", () => hubRuntimeMocks);
vi.mock("./utils/telemetry", () => telemetryMocks);
vi.mock("./utils/worktree", () => worktreeMocks);
describe("runCli lightweight command dispatch", () => {
let globalSettingsRoot: string | undefined;
beforeEach(() => {
process.exitCode = undefined;
// Startup now reads persisted general settings; point the resolver at a
// fresh temp file so the developer's real settings cannot leak in.
globalSettingsRoot = mkdtempSync(join(tmpdir(), "cline-cli-main-test-"));
process.env.CLINE_GLOBAL_SETTINGS_PATH = join(
globalSettingsRoot,
"global-settings.json",
);
mockState.runAgentImports = 0;
mockState.runInteractiveImports = 0;
mockState.runAgentCalls = 0;
historyMocks.runHistoryList.mockReset();
historyMocks.runHistoryList.mockResolvedValue(0);
historyMocks.runHistoryDelete.mockReset();
historyMocks.runHistoryDelete.mockResolvedValue(0);
historyMocks.runHistoryExport.mockReset();
historyMocks.runHistoryExport.mockResolvedValue(0);
historyMocks.runHistoryUpdate.mockReset();
historyMocks.runHistoryUpdate.mockResolvedValue(0);
sessionMocks.getSessionRow.mockReset();
sessionMocks.getSessionRow.mockResolvedValue({
sessionId: "sess_123",
});
runtimeMocks.runAgent.mockReset();
runtimeMocks.runAgent.mockImplementation(async () => {
mockState.runAgentCalls += 1;
});
runtimeMocks.runInteractive.mockReset();
worktreeMocks.createTaskWorktree.mockReset();
worktreeMocks.createTaskWorktree.mockResolvedValue({
success: true,
message: "Worktree created",
path: "/tmp/cline-worktree",
taskId: "task-1",
repoRoot: "/tmp/source",
});
hubRuntimeMocks.ensureCliHubServer.mockReset();
hubRuntimeMocks.ensureCliHubServer.mockResolvedValue({
url: "ws://127.0.0.1:25463",
authToken: "test-token",
});
llmMocks.resolveProviderConfig.mockReset();
llmMocks.resolveProviderConfig.mockResolvedValue(undefined);
authMocks.ensureOAuthProviderApiKey.mockReset();
authMocks.getPersistedProviderApiKey.mockReset();
authMocks.getPersistedProviderApiKey.mockReturnValue(undefined);
authMocks.isOAuthProvider.mockReset();
authMocks.isOAuthProvider.mockReturnValue(false);
authMocks.normalizeProviderId.mockReset();
authMocks.normalizeProviderId.mockImplementation(
(providerId?: string) => providerId ?? "cline",
);
authMocks.parseAuthCommandArgs.mockReset();
authMocks.runAuthCommand.mockReset();
providerSettingsMocks.getLastUsedProviderSettings.mockReset();
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue(
undefined,
);
providerSettingsMocks.getProviderSettings.mockReset();
providerSettingsMocks.getProviderSettings.mockReturnValue(undefined);
providerSettingsMocks.saveProviderSettings.mockReset();
featureFlagMocks.getBooleanFlagEnabled.mockReset();
featureFlagMocks.getBooleanFlagEnabled.mockReturnValue(false);
featureFlagMocks.setCliFeatureFlagsAccountContext.mockReset();
featureFlagMocks.refreshCliFeatureFlagsInBackground.mockReset();
kanbanMocks.launchKanban.mockReset();
kanbanMocks.launchKanban.mockResolvedValue(0);
dashboardMocks.runDashboardCommand.mockReset();
dashboardMocks.runDashboardCommand.mockResolvedValue(0);
connectMocks.formatAdapterList.mockReset();
connectMocks.formatAdapterList.mockReturnValue("");
connectMocks.runConnectAdapter.mockReset();
connectMocks.runConnectAdapter.mockResolvedValue(0);
connectMocks.runRestartConnector.mockReset();
connectMocks.runRestartConnector.mockResolvedValue(0);
connectMocks.runStopAllConnectors.mockReset();
connectMocks.runStopAllConnectors.mockResolvedValue(0);
connectMocks.runStopConnector.mockReset();
connectMocks.runStopConnector.mockResolvedValue(0);
migrationNoticeMocks.getClineCliMigrationNotice.mockReset();
migrationNoticeMocks.getClineCliMigrationNotice.mockReturnValue(undefined);
migrationNoticeMocks.markClineCliMigrationNoticeShown.mockReset();
updateMocks.autoUpdateOnStartup.mockReset();
updateMocks.checkForUpdates.mockReset();
updateMocks.checkForUpdates.mockResolvedValue(0);
updateMocks.getPreferredKanbanInstaller.mockReset();
updateMocks.getPreferredKanbanInstaller.mockReturnValue(undefined);
telemetryMocks.captureCliExtensionActivated.mockReset();
telemetryMocks.identifyTelemetryAccount.mockReset();
telemetryMocks.getCliTelemetryService.mockReset();
telemetryMocks.disposeCliTelemetryService.mockReset();
telemetryMocks.disposeCliTelemetryService.mockResolvedValue(undefined);
// CI: fd 0 is often a pipe with no EOF. If routing ever falls through to agent bootstrap,
// `main` can block forever in `for await (process.stdin)` (see `!process.stdin.isTTY && …`).
// Mark stdin as a TTY so that path is skipped in unit tests (real piped-input behavior is
// covered elsewhere). `forcePromptModeInput()` in agent tests still tightens fstat on fd 0.
Object.defineProperty(process.stdin, "isTTY", {
value: true,
configurable: true,
});
Object.defineProperty(process.stdout, "isTTY", {
value: true,
configurable: true,
});
});
afterEach(() => {
process.exitCode = undefined;
if (originalGlobalSettingsPath === undefined) {
delete process.env.CLINE_GLOBAL_SETTINGS_PATH;
} else {
process.env.CLINE_GLOBAL_SETTINGS_PATH = originalGlobalSettingsPath;
}
if (globalSettingsRoot) {
rmSync(globalSettingsRoot, { recursive: true, force: true });
globalSettingsRoot = undefined;
}
process.argv = [...originalArgv];
Object.defineProperty(process.stdin, "isTTY", {
value: originalStdinIsTTY,
configurable: true,
});
Object.defineProperty(process.stdout, "isTTY", {
value: originalStdoutIsTTY,
configurable: true,
});
vi.restoreAllMocks();
vi.resetModules();
});
it("does not load runtime modules for history json listing", async () => {
mockState.runAgentImports = 0;
mockState.runInteractiveImports = 0;
process.argv = ["bun", "src/index.ts", "history", "--json"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(0);
expect(historyMocks.runHistoryList).toHaveBeenCalledWith(
expect.objectContaining({
limit: 50,
outputMode: "json",
}),
);
const historyListCalls = historyMocks.runHistoryList.mock
.calls as unknown as Array<[Record<string, unknown>]>;
expect(historyListCalls[0]?.[0]).not.toHaveProperty("workspaceRoot");
expect(mockState.runAgentImports).toBe(0);
expect(mockState.runInteractiveImports).toBe(0);
}, 30_000);
it("routes connector restart arguments through the restart lifecycle", async () => {
process.argv = [
"bun",
"src/index.ts",
"connect",
"--restart",
"telegram",
"-k",
"token",
];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(0);
expect(connectMocks.runRestartConnector).toHaveBeenCalledWith(
"telegram",
["-k", "token"],
expect.any(Object),
undefined,
);
expect(connectMocks.runConnectAdapter).not.toHaveBeenCalled();
expect(connectMocks.runStopConnector).not.toHaveBeenCalled();
});
it("routes a supervised cleanup to one connector instance", async () => {
connectMocks.runCleanupConnectorInstance.mockClear();
connectMocks.runConnectAdapter.mockClear();
process.argv = [
"bun",
"src/index.ts",
"connect",
"--cleanup-instance",
"cline-slack",
"slack",
];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(0);
expect(connectMocks.runCleanupConnectorInstance).toHaveBeenCalledWith(
"slack",
"cline-slack",
expect.any(Object),
);
expect(connectMocks.runConnectAdapter).not.toHaveBeenCalled();
});
it("rejects combining cleanup with another connect mode", async () => {
connectMocks.runCleanupConnectorInstance.mockClear();
connectMocks.runStopConnector.mockClear();
process.argv = [
"bun",
"src/index.ts",
"connect",
"--cleanup-instance",
"cline-slack",
"--stop",
"slack",
];
const { runCli } = await import("./main");
await runCli();
expect(process.exitCode).toBe(1);
expect(connectMocks.runCleanupConnectorInstance).not.toHaveBeenCalled();
expect(connectMocks.runStopConnector).not.toHaveBeenCalled();
});
it("requires a channel for a supervised cleanup", async () => {
connectMocks.runCleanupConnectorInstance.mockClear();
process.argv = [
"bun",
"src/index.ts",
"connect",
"--cleanup-instance",
"x",
];
const { runCli } = await import("./main");
await runCli();
expect(process.exitCode).toBe(1);
expect(connectMocks.runCleanupConnectorInstance).not.toHaveBeenCalled();
});
it("routes a targeted connector restart to one instance", async () => {
process.argv = [
"bun",
"src/index.ts",
"connect",
"--restart-instance",
"cline_bot",
"telegram",
"-k",
"token",
];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(0);
expect(connectMocks.runRestartConnector).toHaveBeenCalledWith(
"telegram",
["-k", "token"],
expect.any(Object),
"cline_bot",
);
});
it("does not load runtime modules for root update", async () => {
mockState.runAgentImports = 0;
mockState.runInteractiveImports = 0;
process.argv = ["bun", "src/index.ts", "--update", "--verbose"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(0);
expect(updateMocks.checkForUpdates).toHaveBeenCalledWith({
verbose: true,
});
expect(mockState.runAgentImports).toBe(0);
expect(mockState.runInteractiveImports).toBe(0);
});
it("does not load runtime modules for root kanban flag", async () => {
mockState.runAgentImports = 0;
mockState.runInteractiveImports = 0;
process.argv = ["bun", "src/index.ts", "--kanban"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(0);
expect(kanbanMocks.launchKanban).toHaveBeenCalledTimes(1);
expect(mockState.runAgentImports).toBe(0);
expect(mockState.runInteractiveImports).toBe(0);
});
it("rejects root kanban with a prompt", async () => {
mockState.runAgentImports = 0;
mockState.runInteractiveImports = 0;
process.argv = ["bun", "src/index.ts", "--kanban", "hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(kanbanMocks.launchKanban).not.toHaveBeenCalled();
expect(mockState.runAgentImports).toBe(0);
expect(mockState.runInteractiveImports).toBe(0);
});
it("exits gracefully for handled command errors", async () => {
mockState.runAgentImports = 0;
mockState.runInteractiveImports = 0;
process.argv = ["bun", "src/index.ts", "history", "delete"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(mockState.runAgentImports).toBe(0);
expect(mockState.runInteractiveImports).toBe(0);
});
it("does not load runtime modules for history export", async () => {
mockState.runAgentImports = 0;
mockState.runInteractiveImports = 0;
process.argv = ["bun", "src/index.ts", "history", "export", "sess_1"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(0);
expect(historyMocks.runHistoryExport).toHaveBeenCalledWith(
"sess_1",
undefined,
"text",
expect.any(Object),
);
expect(mockState.runAgentImports).toBe(0);
expect(mockState.runInteractiveImports).toBe(0);
});
it("does not load interactive runtime for single-prompt mode", async () => {
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledTimes(1);
expect(mockState.runAgentImports).toBe(1);
expect(mockState.runInteractiveImports).toBe(0);
});
it("rejects a single bare positional prompt token", async () => {
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "nonexistent-command"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining(
"Unknown command or unquoted prompt: nonexistent-command",
),
);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining('Use "cline --help"'),
);
expect(runtimeMocks.runAgent).not.toHaveBeenCalled();
expect(mockState.runAgentImports).toBe(0);
expect(mockState.runInteractiveImports).toBe(0);
});
it("rejects multiple bare positional prompt tokens", async () => {
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "hello", "world"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining(
"Unknown command or unquoted prompt: hello world",
),
);
expect(runtimeMocks.runAgent).not.toHaveBeenCalled();
expect(mockState.runAgentImports).toBe(0);
expect(mockState.runInteractiveImports).toBe(0);
});
it("runs quoted positional prompt text", async () => {
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "hello world"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"hello world",
expect.any(Object),
expect.anything(),
);
});
it("rejects unknown root flags before loading runtime modules", async () => {
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--made-up-flag"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining("unknown option '--made-up-flag'"),
);
expect(runtimeMocks.runAgent).not.toHaveBeenCalled();
expect(mockState.runAgentImports).toBe(0);
expect(mockState.runInteractiveImports).toBe(0);
});
it("creates a worktree and runs prompt sessions from it", async () => {
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--worktree", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(worktreeMocks.createTaskWorktree).toHaveBeenCalledWith({
cwd: process.cwd(),
});
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
cwd: "/tmp/cline-worktree",
workspaceRoot: "/tmp/cline-worktree",
}),
expect.anything(),
);
});
it("creates a worktree for default interactive mode", async () => {
process.argv = ["bun", "src/index.ts", "--worktree"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(worktreeMocks.createTaskWorktree).toHaveBeenCalledWith({
cwd: process.cwd(),
});
expect(runtimeMocks.runAgent).not.toHaveBeenCalled();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
cwd: "/tmp/cline-worktree",
workspaceRoot: "/tmp/cline-worktree",
}),
expect.anything(),
undefined,
expect.objectContaining({
initialPrompt: undefined,
}),
);
});
it("rejects interactive --worktree when no terminal is attached", async () => {
Object.defineProperty(process.stdin, "isTTY", {
value: false,
configurable: true,
});
Object.defineProperty(process.stdout, "isTTY", {
value: false,
configurable: true,
});
process.argv = ["bun", "src/index.ts", "--worktree"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(worktreeMocks.createTaskWorktree).not.toHaveBeenCalled();
});
it("allows --worktree with piped stdin", async () => {
Object.defineProperty(process.stdin, "isTTY", {
value: false,
configurable: true,
});
Object.defineProperty(process.stdout, "isTTY", {
value: false,
configurable: true,
});
vi.mocked(fstatSync).mockReturnValue({
isFIFO: () => true,
isFile: () => false,
} as unknown as ReturnType<typeof fstatSync>);
vi.spyOn(process.stdin, Symbol.asyncIterator).mockImplementation(
async function* (): AsyncGenerator<Buffer, undefined> {
yield Buffer.from("from pipe");
return undefined;
},
);
process.argv = ["bun", "src/index.ts", "--worktree"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(worktreeMocks.createTaskWorktree).toHaveBeenCalledWith({
cwd: process.cwd(),
});
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"from pipe",
expect.objectContaining({
cwd: "/tmp/cline-worktree",
workspaceRoot: "/tmp/cline-worktree",
}),
expect.anything(),
);
});
it("validates resumed sessions before creating a worktree", async () => {
sessionMocks.getSessionRow.mockResolvedValueOnce(undefined);
process.argv = ["bun", "src/index.ts", "--worktree", "--id", "missing"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(sessionMocks.getSessionRow).toHaveBeenCalledWith("missing");
expect(worktreeMocks.createTaskWorktree).not.toHaveBeenCalled();
expect(runtimeMocks.runInteractive).not.toHaveBeenCalled();
});
it("does not force chat view for default interactive mode", async () => {
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.any(Object),
expect.anything(),
undefined,
expect.objectContaining({
startupTarget: undefined,
}),
);
});
it("passes the migration notice marker into interactive mode", async () => {
const notice = {
id: "cline-cli-cline-pass-intro",
title: "Try ClinePass",
body: "ClinePass body",
url: "https://app.cline.bot/dashboard/subscription?personal=true",
openLabel: "Open ClinePass",
};
migrationNoticeMocks.getClineCliMigrationNotice.mockReturnValue(notice);
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.any(Object),
expect.anything(),
undefined,
expect.objectContaining({
initialNotice: notice,
onInitialNoticeShown: expect.any(Function),
}),
);
expect(
migrationNoticeMocks.markClineCliMigrationNoticeShown,
).not.toHaveBeenCalled();
const options = runtimeMocks.runInteractive.mock.calls[0]?.[3];
await options?.onInitialNoticeShown?.(notice);
expect(
migrationNoticeMocks.markClineCliMigrationNoticeShown,
).toHaveBeenCalledWith(undefined, notice.id);
});
it("passes the active ClinePass provider into the migration notice gate", async () => {
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue({
provider: "cline-pass",
model: "cline-pass/test-model",
});
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(
migrationNoticeMocks.getClineCliMigrationNotice,
).toHaveBeenCalledWith(undefined, process.env, {
activeProviderId: "cline-pass",
});
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
providerId: "cline-pass",
}),
expect.anything(),
undefined,
expect.objectContaining({
initialNotice: undefined,
}),
);
});
it("does not start OAuth before onboarding in interactive mode", async () => {
authMocks.isOAuthProvider.mockReturnValue(true);
authMocks.normalizeProviderId.mockReturnValue("cline");
authMocks.getPersistedProviderApiKey.mockReturnValue(undefined);
authMocks.ensureOAuthProviderApiKey.mockClear();
process.argv = ["bun", "src/index.ts", "-i"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(authMocks.ensureOAuthProviderApiKey).not.toHaveBeenCalled();
expect(runtimeMocks.runInteractive).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
providerId: "cline",
apiKey: "",
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("loads live catalog models for default interactive model selection", async () => {
llmMocks.resolveProviderConfig.mockResolvedValue({
knownModels: {
"live-only-model": {
id: "live-only-model",
name: "Live Only Model",
},
},
});
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(llmMocks.resolveProviderConfig).toHaveBeenCalledWith(
"cline",
{
loadLatestOnInit: true,
loadPrivateOnAuth: true,
failOnError: false,
},
undefined,
);
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
knownModels: expect.objectContaining({
"live-only-model": expect.objectContaining({
name: "Live Only Model",
}),
}),
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("passes a positional prompt into TUI mode for startup submission", async () => {
process.argv = ["bun", "src/index.ts", "sup", "-i"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(llmMocks.resolveProviderConfig).toHaveBeenCalledWith(
"cline",
{
loadLatestOnInit: true,
loadPrivateOnAuth: true,
failOnError: false,
},
undefined,
);
expect(runtimeMocks.runInteractive).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.any(Object),
expect.anything(),
undefined,
expect.objectContaining({
initialPrompt: "sup",
startupTarget: undefined,
}),
);
});
it("uses the bundled catalog path for single-prompt runs", async () => {
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(llmMocks.resolveProviderConfig).toHaveBeenCalledWith(
"cline",
undefined,
undefined,
);
expect(runtimeMocks.runAgent).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runInteractive).not.toHaveBeenCalled();
});
it("treats dash-prefixed positional text after -- as a prompt", async () => {
forcePromptModeInput();
process.argv = [
"bun",
"src/index.ts",
"--",
"- You are given a PyTorch state dictionary.",
];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"- You are given a PyTorch state dictionary.",
expect.any(Object),
expect.anything(),
);
expect(runtimeMocks.runInteractive).not.toHaveBeenCalled();
});
it("applies --auto-approve as a runtime policy without changing the config default", async () => {
process.argv = ["bun", "src/index.ts", "--auto-approve", "false"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
defaultToolAutoApprove: true,
toolPolicies: {
"*": { autoApprove: false },
},
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
describe("persisted general settings at startup", () => {
function writePersistedSettings(settings: Record<string, unknown>) {
const path = process.env.CLINE_GLOBAL_SETTINGS_PATH;
if (!path) {
throw new Error("CLINE_GLOBAL_SETTINGS_PATH is not set");
}
writeFileSync(path, JSON.stringify(settings));
}
it("restores the persisted plan mode when no mode flag is provided", async () => {
writePersistedSettings({ planActMode: "plan" });
promptMocks.resolveSystemPrompt.mockClear();
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({ mode: "plan" }),
expect.anything(),
undefined,
expect.any(Object),
);
expect(promptMocks.resolveSystemPrompt).toHaveBeenCalledWith(
expect.objectContaining({ mode: "plan" }),
);
});
it("prefers an explicit --act flag over the persisted plan mode", async () => {
writePersistedSettings({ planActMode: "plan" });
promptMocks.resolveSystemPrompt.mockClear();
process.argv = ["bun", "src/index.ts", "--act"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({ mode: "act" }),
expect.anything(),
undefined,
expect.any(Object),
);
expect(promptMocks.resolveSystemPrompt).toHaveBeenCalledWith(
expect.objectContaining({ mode: "act" }),
);
});
it("restores the persisted auto-approve setting as a runtime policy", async () => {
writePersistedSettings({ toolAutoApprove: false });
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
defaultToolAutoApprove: true,
toolPolicies: {
"*": { autoApprove: false },
},
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("prefers an explicit --auto-approve flag over the persisted setting", async () => {
writePersistedSettings({ toolAutoApprove: false });
process.argv = ["bun", "src/index.ts", "--auto-approve", "true"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
toolPolicies: {
"*": { autoApprove: true },
},
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("restores disabled compaction across restarts", async () => {
writePersistedSettings({
compactionEnabled: false,
compactionStrategy: "basic",
});
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
compaction: { enabled: false },
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("restores the persisted compaction strategy across restarts", async () => {
writePersistedSettings({
compactionEnabled: true,
compactionStrategy: "basic",
});
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
compaction: { enabled: true, strategy: "basic" },
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("prefers an explicit --compaction flag over the persisted mode", async () => {
writePersistedSettings({ compactionEnabled: false });
process.argv = ["bun", "src/index.ts", "--compaction", "agentic"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.objectContaining({
compaction: { enabled: true, strategy: "agentic" },
}),
expect.anything(),
undefined,
expect.any(Object),
);
});
it("applies persisted settings to single-prompt runs as well", async () => {
writePersistedSettings({
compactionEnabled: true,
compactionStrategy: "basic",
planActMode: "plan",
});
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
compaction: { enabled: true, strategy: "basic" },
mode: "plan",
}),
expect.anything(),
);
});
});
it("forces chat view when resuming a session", async () => {
process.argv = ["bun", "src/index.ts", "--id", "sess_123"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.any(Object),
expect.anything(),
"sess_123",
expect.objectContaining({
startupTarget: "chat",
}),
);
});
it("opens history inside the interactive TUI for the history picker", async () => {
migrationNoticeMocks.getClineCliMigrationNotice.mockReturnValue({
id: "cline-cli-cline-pass-intro",
title: "Try ClinePass",
body: "ClinePass body",
url: "https://app.cline.bot/dashboard/subscription?personal=true",
openLabel: "Open ClinePass",
});
process.argv = ["bun", "src/index.ts", "history"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(historyMocks.runHistoryList).not.toHaveBeenCalled();
expect(
migrationNoticeMocks.getClineCliMigrationNotice,
).not.toHaveBeenCalled();
expect(runtimeMocks.runInteractive).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.any(Object),
expect.anything(),
undefined,
expect.objectContaining({
initialPrompt: undefined,
startupTarget: "history",
}),
);
});
it("does not pass non-Cline provider settings as Cline account options", async () => {
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue({
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
model: "openai/gpt-5",
});
providerSettingsMocks.getProviderSettings.mockReturnValue({
provider: "openrouter",
baseUrl: "https://openrouter.ai/api/v1",
model: "openai/gpt-5",
});
authMocks.normalizeProviderId.mockImplementation(
(providerId?: string) => providerId ?? "openrouter",
);
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.any(Object),
expect.anything(),
undefined,
expect.objectContaining({
clineApiBaseUrl: undefined,
clineProviderSettings: undefined,
}),
);
});
it("passes Cline provider settings as Cline account options", async () => {
const clineSettings = {
provider: "cline",
baseUrl: "https://api.example.test",
model: "anthropic/claude-sonnet-4.6",
};
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue(
clineSettings,
);
providerSettingsMocks.getProviderSettings.mockReturnValue(clineSettings);
authMocks.normalizeProviderId.mockImplementation(
(providerId?: string) => providerId ?? "cline",
);
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runInteractive).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runInteractive).toHaveBeenCalledWith(
expect.any(Object),
expect.anything(),
undefined,
expect.objectContaining({
clineApiBaseUrl: "https://api.example.test",
clineProviderSettings: clineSettings,
}),
);
});
it("seeds feature flag identity from persisted Cline account id before refreshing flags", async () => {
const clineSettings = {
provider: "cline",
model: "anthropic/claude-sonnet-4.6",
auth: {
accountId: "acct-startup",
accessToken: "workos:token",
refreshToken: "refresh-token",
},
};
providerSettingsMocks.getProviderSettings.mockReturnValue(clineSettings);
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(
featureFlagMocks.setCliFeatureFlagsAccountContext,
).toHaveBeenCalledWith({ id: "acct-startup" });
// The account identity must be seeded before flags are refreshed/used so
// the background refresh resolves flags for the correct account.
expect(
featureFlagMocks.setCliFeatureFlagsAccountContext.mock
.invocationCallOrder[0],
).toBeLessThan(
featureFlagMocks.refreshCliFeatureFlagsInBackground.mock
.invocationCallOrder[0],
);
});
it("identifies saved Cline accountId for telemetry before runtime events", async () => {
// CLINE-2406: when persisted Cline auth includes an accountId, the
// runtime path must call identifyTelemetryAccount(accountContext) so
// subsequent task.* and workspace.* events carry user_id.
const clineSettings = {
provider: "cline",
model: "anthropic/claude-sonnet-4.6",
auth: { accountId: "usr-abc-123", refreshToken: "rt-token" },
};
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue(
clineSettings,
);
providerSettingsMocks.getProviderSettings.mockReturnValue(clineSettings);
authMocks.normalizeProviderId.mockImplementation(
(providerId?: string) => providerId ?? "cline",
);
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(telemetryMocks.identifyTelemetryAccount).toHaveBeenCalledWith(
expect.objectContaining({
id: "usr-abc-123",
provider: "cline",
}),
);
});
it("does not call identifyTelemetryAccount in runtime path when no saved Cline accountId", async () => {
// CLINE-2406: when no persisted accountId is found (anonymous/unauthenticated),
// identifyTelemetryAccount should not be called from the runtime path.
const clineSettings = {
provider: "cline",
model: "anthropic/claude-sonnet-4.6",
// no auth / no accountId
};
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue(
clineSettings,
);
providerSettingsMocks.getProviderSettings.mockReturnValue(clineSettings);
authMocks.normalizeProviderId.mockImplementation(
(providerId?: string) => providerId ?? "cline",
);
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(telemetryMocks.identifyTelemetryAccount).not.toHaveBeenCalled();
});
it("does not call identifyTelemetryAccount from runtime path when provider is not cline", async () => {
// CLINE-2406: identity identification from saved settings only applies
// to Cline-provider sessions; other providers use different auth flows.
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue({
provider: "openrouter",
model: "openai/gpt-5",
});
providerSettingsMocks.getProviderSettings.mockReturnValue({
provider: "openrouter",
model: "openai/gpt-5",
});
authMocks.normalizeProviderId.mockImplementation(
(providerId?: string) => providerId ?? "openrouter",
);
process.argv = ["bun", "src/index.ts"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(telemetryMocks.identifyTelemetryAccount).not.toHaveBeenCalled();
});
it("runs kanban before loading runtime modules", async () => {
process.argv = ["bun", "src/index.ts", "kanban"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(kanbanMocks.launchKanban).toHaveBeenCalledTimes(1);
expect(mockState.runAgentImports).toBe(0);
expect(mockState.runInteractiveImports).toBe(0);
expect(process.exitCode).toBe(0);
});
it("runs dashboard before loading runtime modules", async () => {
process.argv = [
"bun",
"src/index.ts",
"dashboard",
"--config",
"/tmp/cline-config",
"--data-dir",
".cline-dashboard-data",
"--port",
"9090",
"--no-open",
];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(dashboardMocks.runDashboardCommand).toHaveBeenCalledWith(
expect.objectContaining({
configDir: "/tmp/cline-config",
dataDir: ".cline-dashboard-data",
port: "9090",
openBrowser: false,
io: expect.any(Object),
}),
);
expect(mockState.runAgentImports).toBe(0);
expect(mockState.runInteractiveImports).toBe(0);
expect(process.exitCode).toBe(0);
});
it("prints an install hint when kanban is missing", async () => {
const stderrWrite = vi
.spyOn(process.stderr, "write")
.mockImplementation(() => true);
kanbanMocks.launchKanban.mockImplementation(async () => {
process.stderr.write(
'kanban is not installed. Install it with "npm i -g kanban"\n',
);
return 1;
});
process.argv = ["bun", "src/index.ts", "kanban"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(stderrWrite).toHaveBeenCalledWith(
expect.stringContaining(
'kanban is not installed. Install it with "npm i -g kanban"',
),
);
expect(process.exitCode).toBe(1);
});
it("skips hub prewarm for yolo runs", async () => {
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--yolo", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledTimes(1);
expect(hubRuntimeMocks.ensureCliHubServer).not.toHaveBeenCalled();
});
it("rejects yolo runs with a single bare prompt token", async () => {
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--yolo", "hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining("Unknown command or unquoted prompt: hello"),
);
expect(runtimeMocks.runAgent).not.toHaveBeenCalled();
expect(hubRuntimeMocks.ensureCliHubServer).not.toHaveBeenCalled();
});
it("rewrites /team prompts and enables teams in single-prompt mode", async () => {
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "/team find the bug"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledTimes(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
'<user_command slash="team">spawn a team of agents for the following task: find the bug</user_command>',
expect.objectContaining({
enableAgentTeams: true,
teamName: undefined,
}),
expect.anything(),
);
});
it("rejects /team without quoted task text", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
const consoleError = vi
.spyOn(console, "error")
.mockImplementation(() => undefined);
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "/team"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(mockState.runAgentCalls).toBe(0);
expect(consoleError).toHaveBeenCalledWith(
expect.stringContaining("Unknown command or unquoted prompt: /team"),
);
});
it("enables thinking when explicit thinking level is provided", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--thinking", "high", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
thinking: true,
reasoningEffort: "high",
}),
expect.anything(),
);
});
it("leaves thinking unset when --thinking is not provided", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
thinking: undefined,
reasoningEffort: undefined,
}),
expect.anything(),
);
});
it("disables thinking when --thinking none is explicitly provided", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--thinking", "none", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
thinking: false,
reasoningEffort: undefined,
}),
expect.anything(),
);
});
it("maps --thinking to medium effort", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--thinking", "--", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
compaction: {
enabled: true,
},
thinking: true,
reasoningEffort: "medium",
}),
expect.anything(),
);
});
it("uses persisted reasoning effort when --thinking is not provided", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
providerSettingsMocks.getProviderSettings.mockReturnValue({
provider: "cline",
model: "openai/gpt-5",
reasoning: { enabled: true, effort: "high" },
});
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
thinking: true,
reasoningEffort: "high",
}),
expect.anything(),
);
});
it("uses persisted disabled reasoning when --thinking is not provided", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
providerSettingsMocks.getProviderSettings.mockReturnValue({
provider: "cline",
model: "openai/gpt-5",
reasoning: { enabled: false },
});
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
thinking: false,
reasoningEffort: undefined,
}),
expect.anything(),
);
});
it("prefers explicit --thinking over persisted reasoning effort", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
providerSettingsMocks.getProviderSettings.mockReturnValue({
provider: "cline",
model: "openai/gpt-5",
reasoning: { enabled: true, effort: "high" },
});
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--thinking", "low", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
thinking: true,
reasoningEffort: "low",
}),
expect.anything(),
);
});
it("uses Core's agentic compaction default for prompt runs", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
compaction: {
enabled: true,
},
}),
expect.anything(),
);
});
it("supports basic truncation compaction for prompt runs", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = [
"bun",
"src/index.ts",
"--compaction",
"basic",
"say hello",
];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
compaction: {
enabled: true,
strategy: "basic",
},
}),
expect.anything(),
);
});
it("supports agentic compaction for prompt runs", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = [
"bun",
"src/index.ts",
"--compaction",
"agentic",
"say hello",
];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
compaction: {
enabled: true,
strategy: "agentic",
},
}),
expect.anything(),
);
});
it("rejects the removed llm compaction alias", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--compaction", "llm", "hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(runtimeMocks.runAgent).not.toHaveBeenCalled();
});
it("rejects the removed truncation compaction alias", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = [
"bun",
"src/index.ts",
"--compaction",
"truncation",
"hello",
];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(runtimeMocks.runAgent).not.toHaveBeenCalled();
});
it("supports disabling compaction for prompt runs", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--compaction", "off", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
compaction: {
enabled: false,
},
}),
expect.anything(),
);
});
it("rejects invalid compaction modes", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
forcePromptModeInput();
process.argv = [
"bun",
"src/index.ts",
"--compaction",
"aggressive",
"hello",
];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(process.exitCode).toBe(1);
expect(runtimeMocks.runAgent).not.toHaveBeenCalled();
});
it("does not fail fast for headless json mode with an OAuth provider", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
authMocks.isOAuthProvider.mockReturnValue(true);
authMocks.normalizeProviderId.mockReturnValue("cline");
authMocks.getPersistedProviderApiKey.mockReturnValue(undefined);
authMocks.ensureOAuthProviderApiKey.mockClear();
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--json", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(authMocks.ensureOAuthProviderApiKey).not.toHaveBeenCalled();
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
outputMode: "json",
apiKey: "",
providerId: "cline",
}),
expect.anything(),
);
});
it("does not fail fast for headless json mode with a non-OAuth provider", async () => {
mockState.runAgentCalls = 0;
runtimeMocks.runAgent.mockClear();
authMocks.isOAuthProvider.mockReturnValue(false);
authMocks.normalizeProviderId.mockReturnValue("anthropic");
authMocks.getPersistedProviderApiKey.mockReturnValue(undefined);
authMocks.ensureOAuthProviderApiKey.mockClear();
forcePromptModeInput();
process.argv = ["bun", "src/index.ts", "--json", "say hello"];
const { runCli } = await import("./main");
await expect(runCli()).resolves.toBeUndefined();
expect(mockState.runAgentCalls).toBe(1);
expect(authMocks.ensureOAuthProviderApiKey).not.toHaveBeenCalled();
expect(runtimeMocks.runAgent).toHaveBeenCalledWith(
"say hello",
expect.objectContaining({
outputMode: "json",
apiKey: "",
providerId: "anthropic",
}),
expect.anything(),
);
});
});
describe("stdinHasPipedInput", () => {
const originalIsTTY = process.stdin.isTTY;
afterEach(() => {
Object.defineProperty(process.stdin, "isTTY", {
value: originalIsTTY,
configurable: true,
});
vi.mocked(fstatSync).mockRestore();
});
it("returns false when stdin is a TTY", async () => {
Object.defineProperty(process.stdin, "isTTY", {
value: true,
configurable: true,
});
const { stdinHasPipedInput } = await import("./main");
expect(stdinHasPipedInput()).toBe(false);
});
it("returns true when stdin is a FIFO (pipe)", async () => {
Object.defineProperty(process.stdin, "isTTY", {
value: undefined,
configurable: true,
});
vi.mocked(fstatSync).mockReturnValue({
isFIFO: () => true,
isFile: () => false,
} as unknown as ReturnType<typeof fstatSync>);
const { stdinHasPipedInput } = await import("./main");
expect(stdinHasPipedInput()).toBe(true);
});
it("returns true when stdin is a file", async () => {
Object.defineProperty(process.stdin, "isTTY", {
value: undefined,
configurable: true,
});
vi.mocked(fstatSync).mockReturnValue({
isFIFO: () => false,
isFile: () => true,
} as unknown as ReturnType<typeof fstatSync>);
const { stdinHasPipedInput } = await import("./main");
expect(stdinHasPipedInput()).toBe(true);
});
it("returns false in headless/CI (not TTY, not pipe, not file)", async () => {
Object.defineProperty(process.stdin, "isTTY", {
value: undefined,
configurable: true,
});
vi.mocked(fstatSync).mockReturnValue({
isFIFO: () => false,
isFile: () => false,
} as unknown as ReturnType<typeof fstatSync>);
const { stdinHasPipedInput } = await import("./main");
expect(stdinHasPipedInput()).toBe(false);
});
it("returns false when fstatSync throws", async () => {
Object.defineProperty(process.stdin, "isTTY", {
value: undefined,
configurable: true,
});
vi.mocked(fstatSync).mockImplementation(() => {
throw new Error("EBADF");
});
const { stdinHasPipedInput } = await import("./main");
expect(stdinHasPipedInput()).toBe(false);
});
});
describe("resolveConfigDirArg", () => {
it("returns undefined when --config is not present", async () => {
const { resolveConfigDirArg } = await import("./main");
expect(resolveConfigDirArg([])).toBeUndefined();
expect(
resolveConfigDirArg(["auth", "--provider", "openai"]),
).toBeUndefined();
});
it("parses the space-separated form: --config <dir>", async () => {
const { resolveConfigDirArg } = await import("./main");
expect(resolveConfigDirArg(["--config", "./mycfg"])).toBe("./mycfg");
expect(
resolveConfigDirArg([
"auth",
"--config",
"./mycfg",
"--provider",
"openai",
]),
).toBe("./mycfg");
});
it("parses the equals form: --config=<dir>", async () => {
const { resolveConfigDirArg } = await import("./main");
expect(resolveConfigDirArg(["--config=./mycfg"])).toBe("./mycfg");
expect(
resolveConfigDirArg(["auth", "--config=./mycfg", "--provider", "openai"]),
).toBe("./mycfg");
});
it("trims surrounding whitespace from the value", async () => {
const { resolveConfigDirArg } = await import("./main");
expect(resolveConfigDirArg(["--config", " ./mycfg "])).toBe("./mycfg");
expect(resolveConfigDirArg(["--config= ./mycfg "])).toBe("./mycfg");
});
it("returns undefined for empty or whitespace-only values", async () => {
const { resolveConfigDirArg } = await import("./main");
expect(resolveConfigDirArg(["--config", ""])).toBeUndefined();
expect(resolveConfigDirArg(["--config", " "])).toBeUndefined();
expect(resolveConfigDirArg(["--config="])).toBeUndefined();
expect(resolveConfigDirArg(["--config= "])).toBeUndefined();
});
it("returns undefined when --config has no following value (space form)", async () => {
const { resolveConfigDirArg } = await import("./main");
expect(resolveConfigDirArg(["--config"])).toBeUndefined();
});
it("returns the first occurrence when --config appears multiple times", async () => {
const { resolveConfigDirArg } = await import("./main");
expect(
resolveConfigDirArg(["--config", "./first", "--config", "./second"]),
).toBe("./first");
expect(resolveConfigDirArg(["--config=./first", "--config=./second"])).toBe(
"./first",
);
});
it("does not match unrelated flags that share a prefix", async () => {
const { resolveConfigDirArg } = await import("./main");
// e.g. a hypothetical --configure flag must not be picked up.
expect(resolveConfigDirArg(["--configure", "./foo"])).toBeUndefined();
expect(resolveConfigDirArg(["--configure=./foo"])).toBeUndefined();
});
});