FN-5808: skip onboarding auto-launch for non-interactive and existing setups
Prevent onboarding autolaunch when invocation is non-interactive or an existing central+project setup is detected. - Add a project initialization probe in onboard autolaunch flow and skip when both central DB and project DB already exist. - Preserve skip behavior for serve/daemon and all non-TTY invocations so agent/headless runs never block. - Expand CLI autolaunch coverage with backward-compat and seam tests, and add a changeset for the published package. Files changed: .changeset/fn-5808-onboard-backcompat-guard.md | 5 + .../onboard-autolaunch-backcompat.test.ts | 135 +++++++++++++++++++++ .../commands/__tests__/onboard-autolaunch.test.ts | 9 ++ packages/cli/src/commands/onboard-autolaunch.ts | 16 ++- 4 files changed, 164 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-5808 Fusion-Task-Lineage: b859aa93-44d5-4d93-bc07-8149b95b4042
This commit is contained in:
5
.changeset/fn-5808-onboard-backcompat-guard.md
Normal file
5
.changeset/fn-5808-onboard-backcompat-guard.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
Harden CLI onboarding auto-launch backward compatibility by adding an explicit skip when both the central DB and local project DB already exist. This preserves established agent/headless behavior by ensuring non-TTY, `serve`, and `daemon` invocations continue without onboarding prompts or blocking.
|
||||
@@ -0,0 +1,135 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
maybeAutoLaunchOnboarding,
|
||||
shouldAutoLaunchOnboarding,
|
||||
} from "../onboard-autolaunch.js";
|
||||
|
||||
describe("onboard autolaunch backward-compat guard", () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("skips with explicit reason when central DB and project both exist", () => {
|
||||
expect(
|
||||
shouldAutoLaunchOnboarding({
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbExists: true,
|
||||
projectInitialized: true,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "central-db-and-project-exist" });
|
||||
});
|
||||
|
||||
it("keeps central-db-exists skip when only central DB exists", () => {
|
||||
expect(
|
||||
shouldAutoLaunchOnboarding({
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbExists: true,
|
||||
projectInitialized: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "central-db-exists" });
|
||||
});
|
||||
|
||||
it("still launches when central DB is missing even if project is initialized", () => {
|
||||
expect(
|
||||
shouldAutoLaunchOnboarding({
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: true,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||
});
|
||||
|
||||
it("never launches on non-TTY agent/headless path", () => {
|
||||
expect(
|
||||
shouldAutoLaunchOnboarding({
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
isTTY: false,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "non-tty" });
|
||||
});
|
||||
|
||||
it("never launches for serve/daemon", () => {
|
||||
for (const command of ["serve", "daemon"]) {
|
||||
expect(
|
||||
shouldAutoLaunchOnboarding({
|
||||
command,
|
||||
args: [command],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "command-skip" });
|
||||
}
|
||||
});
|
||||
|
||||
it("agent-run simulation: non-TTY task list resolves and does not prompt", async () => {
|
||||
const runOnboard = vi.fn();
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
|
||||
Object.defineProperty(process.stdout, "isTTY", { value: false, configurable: true });
|
||||
Object.defineProperty(process.stdin, "isTTY", { value: false, configurable: true });
|
||||
|
||||
await expect(
|
||||
maybeAutoLaunchOnboarding({
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbPath: "/virtual/central.db",
|
||||
pathExists: () => false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(runOnboard).not.toHaveBeenCalled();
|
||||
expect(errorSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("derives projectInitialized from cwd/pathExists seam", async () => {
|
||||
const runOnboard = vi.fn();
|
||||
const pathExists = vi.fn((path: string) => path.endsWith(".fusion/fusion.db"));
|
||||
|
||||
await maybeAutoLaunchOnboarding({
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbPath: "/virtual/central.db",
|
||||
cwd: "/workspace/demo",
|
||||
isTTY: true,
|
||||
pathExists,
|
||||
runOnboard,
|
||||
});
|
||||
|
||||
expect(pathExists).toHaveBeenCalledWith("/virtual/central.db");
|
||||
expect(pathExists).toHaveBeenCalledWith("/workspace/demo/.fusion/fusion.db");
|
||||
expect(runOnboard).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("isolates onboard launch failures as non-fatal diagnostics", async () => {
|
||||
const runOnboard = vi.fn().mockRejectedValue(new Error("boom"));
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||
|
||||
await expect(
|
||||
maybeAutoLaunchOnboarding({
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbPath: "/virtual/central.db",
|
||||
isTTY: true,
|
||||
pathExists: () => false,
|
||||
runOnboard,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledTimes(1);
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[onboard-autolaunch] non-fatal onboard launch failure: boom"),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -12,6 +12,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||
@@ -23,6 +24,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
command: "dashboard",
|
||||
args: ["dashboard"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||
@@ -36,6 +38,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
command: "task",
|
||||
args,
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "skip-flag" });
|
||||
@@ -47,6 +50,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbExists: true,
|
||||
projectInitialized: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "central-db-exists" });
|
||||
@@ -58,6 +62,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
isTTY: false,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "non-tty" });
|
||||
@@ -69,6 +74,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
command: "serve",
|
||||
args: ["serve"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "command-skip" });
|
||||
@@ -78,6 +84,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
command: "daemon",
|
||||
args: ["daemon"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "command-skip" });
|
||||
@@ -89,6 +96,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
command: "onboard",
|
||||
args: ["onboard"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
isTTY: true,
|
||||
}),
|
||||
).toEqual({ launch: false, reason: "onboard-command" });
|
||||
@@ -100,6 +108,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
||||
command: "task",
|
||||
args: ["task", "list"],
|
||||
centralDbExists: false,
|
||||
projectInitialized: false,
|
||||
isTTY: true,
|
||||
env: { FUSION_SKIP_ONBOARDING: "1" },
|
||||
}),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { existsSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { getDefaultCentralDbPath } from "@fusion/core";
|
||||
|
||||
import { isTTYAvailable } from "./dashboard-tui/index.js";
|
||||
@@ -7,6 +8,7 @@ export interface AutoLaunchInput {
|
||||
command: string;
|
||||
args: string[];
|
||||
centralDbExists: boolean;
|
||||
projectInitialized: boolean;
|
||||
isTTY: boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}
|
||||
@@ -48,6 +50,10 @@ export function shouldAutoLaunchOnboarding(input: AutoLaunchInput): AutoLaunchDe
|
||||
return { launch: false, reason: "skip-env" };
|
||||
}
|
||||
|
||||
if (input.centralDbExists && input.projectInitialized) {
|
||||
return { launch: false, reason: "central-db-and-project-exist" };
|
||||
}
|
||||
|
||||
if (input.centralDbExists) {
|
||||
return { launch: false, reason: "central-db-exists" };
|
||||
}
|
||||
@@ -74,6 +80,8 @@ export interface MaybeAutoLaunchDeps {
|
||||
command: string;
|
||||
args: string[];
|
||||
centralDbPath?: string;
|
||||
projectInitialized?: boolean;
|
||||
cwd?: string;
|
||||
isTTY?: boolean;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
runOnboard?: RunOnboard;
|
||||
@@ -85,9 +93,14 @@ export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Prom
|
||||
const isTTY = deps.isTTY ?? isTTYAvailable();
|
||||
|
||||
let centralDbExists = true;
|
||||
let projectInitialized = false;
|
||||
try {
|
||||
const pathExists = deps.pathExists ?? existsSync;
|
||||
const centralDbPath = deps.centralDbPath ?? getDefaultCentralDbPath();
|
||||
centralDbExists = (deps.pathExists ?? existsSync)(centralDbPath);
|
||||
const cwd = deps.cwd ?? process.cwd();
|
||||
const projectDbPath = join(cwd, ".fusion", "fusion.db");
|
||||
centralDbExists = pathExists(centralDbPath);
|
||||
projectInitialized = deps.projectInitialized ?? pathExists(projectDbPath);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
console.error(`[onboard-autolaunch] central DB probe failed; skipping auto-launch: ${message}`);
|
||||
@@ -98,6 +111,7 @@ export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Prom
|
||||
command: deps.command,
|
||||
args: deps.args,
|
||||
centralDbExists,
|
||||
projectInitialized,
|
||||
isTTY,
|
||||
env,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user