FN-5812: add onboard autolaunch backcompat guard coverage

Protect onboarding autolaunch behavior so legacy projects and agent-run commands are not blocked.

- add end-to-end CLI test coverage for onboarding autolaunch backcompat guard behavior
- document the onboarding autolaunch compatibility behavior in the CLI reference
- add a patch changeset for @runfusion/fusion release tracking

Files changed:
 .changeset/fn-5812-onboard-backcompat-guard.md     |   5 +
 docs/cli-reference.md                              |   6 +
 packages/cli/src/commands/__tests__/onboard-autolaunch-backcompat-e2e.test.ts      | 161 +++++++++++++++++++++
 3 files changed, 172 insertions(+)

Fusion-Task-Id: FN-5812

Fusion-Task-Lineage: d735ef0e-1abc-4c22-bd11-07c2de69ace1
This commit is contained in:
gsxdsm
2026-06-01 16:55:56 -07:00
parent 8376781864
commit 245129e24c
3 changed files with 172 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Add orchestrator-level regression coverage and CLI docs that guarantee onboarding auto-launch never blocks existing projects, non-TTY/headless workflows, or agent-run `fn` commands.

View File

@@ -67,6 +67,12 @@ fn onboard --force
The command is safe to re-run and only updates the settings you confirm during The command is safe to re-run and only updates the settings you confirm during
prompts. prompts.
Backward compatibility guarantee: onboarding auto-launch never interrupts
existing setups and never blocks headless usage. It is skipped when both the
central DB and local project DB already exist, and it never prompts in non-TTY,
`serve`, `daemon`, `--skip-onboarding`, `FUSION_SKIP_ONBOARDING`, piped, or
agent-run invocations.
--- ---
## `fn update` ## `fn update`

View File

@@ -0,0 +1,161 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { maybeAutoLaunchOnboarding } from "../onboard-autolaunch.js";
describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("does not prompt onboarded interactive users with central and project DB present", async () => {
const runOnboard = vi.fn();
const pathExists = vi.fn((path: string) =>
path === "/virtual/central.db" || path.endsWith("/.fusion/fusion.db"),
);
await expect(
maybeAutoLaunchOnboarding({
command: "task",
args: ["task", "list"],
centralDbPath: "/virtual/central.db",
cwd: "/repo/demo",
isTTY: true,
pathExists,
runOnboard,
}),
).resolves.toBeUndefined();
expect(runOnboard).not.toHaveBeenCalled();
});
it("never blocks agent-run non-TTY task commands when nothing is initialized", async () => {
const runOnboard = vi.fn();
await expect(
maybeAutoLaunchOnboarding({
command: "task",
args: ["task", "list"],
centralDbPath: "/virtual/central.db",
isTTY: false,
pathExists: () => false,
runOnboard,
}),
).resolves.toBeUndefined();
expect(runOnboard).not.toHaveBeenCalled();
});
it("skips serve and daemon commands even with missing DB", async () => {
for (const command of ["serve", "daemon"]) {
const runOnboard = vi.fn();
await expect(
maybeAutoLaunchOnboarding({
command,
args: [command],
centralDbPath: "/virtual/central.db",
isTTY: true,
pathExists: () => false,
runOnboard,
}),
).resolves.toBeUndefined();
expect(runOnboard).not.toHaveBeenCalled();
}
});
it("honors --skip-onboarding end-to-end", async () => {
const runOnboard = vi.fn();
await expect(
maybeAutoLaunchOnboarding({
command: "task",
args: ["task", "list", "--skip-onboarding"],
centralDbPath: "/virtual/central.db",
isTTY: true,
pathExists: () => false,
runOnboard,
}),
).resolves.toBeUndefined();
expect(runOnboard).not.toHaveBeenCalled();
});
it("honors FUSION_SKIP_ONBOARDING end-to-end", async () => {
const runOnboard = vi.fn();
await expect(
maybeAutoLaunchOnboarding({
command: "task",
args: ["task", "list"],
centralDbPath: "/virtual/central.db",
isTTY: true,
env: { FUSION_SKIP_ONBOARDING: "true" },
pathExists: () => false,
runOnboard,
}),
).resolves.toBeUndefined();
expect(runOnboard).not.toHaveBeenCalled();
});
it("isolates runOnboard failures as non-fatal with one diagnostic", 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"),
);
});
it("launches onboarding once for legitimate first-run interactive commands", async () => {
const runOnboard = vi.fn();
await expect(
maybeAutoLaunchOnboarding({
command: "task",
args: ["task", "list"],
centralDbPath: "/virtual/central.db",
isTTY: true,
pathExists: () => false,
runOnboard,
}),
).resolves.toBeUndefined();
expect(runOnboard).toHaveBeenCalledTimes(1);
});
it("derives projectInitialized from cwd/pathExists seam without real filesystem", async () => {
const runOnboard = vi.fn();
const pathExists = vi.fn((path: string) =>
path === "/virtual/central.db" || path === "/workspace/demo/.fusion/fusion.db",
);
await expect(
maybeAutoLaunchOnboarding({
command: "task",
args: ["task", "list"],
centralDbPath: "/virtual/central.db",
cwd: "/workspace/demo",
isTTY: true,
pathExists,
runOnboard,
}),
).resolves.toBeUndefined();
expect(pathExists).toHaveBeenCalledWith("/workspace/demo/.fusion/fusion.db");
expect(runOnboard).not.toHaveBeenCalled();
});
});