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:
5
.changeset/fn-5812-onboard-backcompat-guard.md
Normal file
5
.changeset/fn-5812-onboard-backcompat-guard.md
Normal 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.
|
||||||
@@ -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`
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user