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",
|
command: "task",
|
||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||||
@@ -23,6 +24,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
command: "dashboard",
|
command: "dashboard",
|
||||||
args: ["dashboard"],
|
args: ["dashboard"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||||
@@ -36,6 +38,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
command: "task",
|
command: "task",
|
||||||
args,
|
args,
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "skip-flag" });
|
).toEqual({ launch: false, reason: "skip-flag" });
|
||||||
@@ -47,6 +50,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
command: "task",
|
command: "task",
|
||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: true,
|
centralDbExists: true,
|
||||||
|
projectInitialized: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "central-db-exists" });
|
).toEqual({ launch: false, reason: "central-db-exists" });
|
||||||
@@ -58,6 +62,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
command: "task",
|
command: "task",
|
||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
isTTY: false,
|
isTTY: false,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "non-tty" });
|
).toEqual({ launch: false, reason: "non-tty" });
|
||||||
@@ -69,6 +74,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
command: "serve",
|
command: "serve",
|
||||||
args: ["serve"],
|
args: ["serve"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "command-skip" });
|
).toEqual({ launch: false, reason: "command-skip" });
|
||||||
@@ -78,6 +84,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
command: "daemon",
|
command: "daemon",
|
||||||
args: ["daemon"],
|
args: ["daemon"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "command-skip" });
|
).toEqual({ launch: false, reason: "command-skip" });
|
||||||
@@ -89,6 +96,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
command: "onboard",
|
command: "onboard",
|
||||||
args: ["onboard"],
|
args: ["onboard"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "onboard-command" });
|
).toEqual({ launch: false, reason: "onboard-command" });
|
||||||
@@ -100,6 +108,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
command: "task",
|
command: "task",
|
||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
env: { FUSION_SKIP_ONBOARDING: "1" },
|
env: { FUSION_SKIP_ONBOARDING: "1" },
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
import { getDefaultCentralDbPath } from "@fusion/core";
|
import { getDefaultCentralDbPath } from "@fusion/core";
|
||||||
|
|
||||||
import { isTTYAvailable } from "./dashboard-tui/index.js";
|
import { isTTYAvailable } from "./dashboard-tui/index.js";
|
||||||
@@ -7,6 +8,7 @@ export interface AutoLaunchInput {
|
|||||||
command: string;
|
command: string;
|
||||||
args: string[];
|
args: string[];
|
||||||
centralDbExists: boolean;
|
centralDbExists: boolean;
|
||||||
|
projectInitialized: boolean;
|
||||||
isTTY: boolean;
|
isTTY: boolean;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
}
|
}
|
||||||
@@ -48,6 +50,10 @@ export function shouldAutoLaunchOnboarding(input: AutoLaunchInput): AutoLaunchDe
|
|||||||
return { launch: false, reason: "skip-env" };
|
return { launch: false, reason: "skip-env" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.centralDbExists && input.projectInitialized) {
|
||||||
|
return { launch: false, reason: "central-db-and-project-exist" };
|
||||||
|
}
|
||||||
|
|
||||||
if (input.centralDbExists) {
|
if (input.centralDbExists) {
|
||||||
return { launch: false, reason: "central-db-exists" };
|
return { launch: false, reason: "central-db-exists" };
|
||||||
}
|
}
|
||||||
@@ -74,6 +80,8 @@ export interface MaybeAutoLaunchDeps {
|
|||||||
command: string;
|
command: string;
|
||||||
args: string[];
|
args: string[];
|
||||||
centralDbPath?: string;
|
centralDbPath?: string;
|
||||||
|
projectInitialized?: boolean;
|
||||||
|
cwd?: string;
|
||||||
isTTY?: boolean;
|
isTTY?: boolean;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
runOnboard?: RunOnboard;
|
runOnboard?: RunOnboard;
|
||||||
@@ -85,9 +93,14 @@ export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Prom
|
|||||||
const isTTY = deps.isTTY ?? isTTYAvailable();
|
const isTTY = deps.isTTY ?? isTTYAvailable();
|
||||||
|
|
||||||
let centralDbExists = true;
|
let centralDbExists = true;
|
||||||
|
let projectInitialized = false;
|
||||||
try {
|
try {
|
||||||
|
const pathExists = deps.pathExists ?? existsSync;
|
||||||
const centralDbPath = deps.centralDbPath ?? getDefaultCentralDbPath();
|
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) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
console.error(`[onboard-autolaunch] central DB probe failed; skipping auto-launch: ${message}`);
|
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,
|
command: deps.command,
|
||||||
args: deps.args,
|
args: deps.args,
|
||||||
centralDbExists,
|
centralDbExists,
|
||||||
|
projectInitialized,
|
||||||
isTTY,
|
isTTY,
|
||||||
env,
|
env,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user