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:
gsxdsm
2026-06-01 13:33:56 -07:00
parent 40b491958f
commit e1a35a364b
4 changed files with 164 additions and 1 deletions

View File

@@ -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"),
);
});
});

View File

@@ -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" },
}),

View File

@@ -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,
});