FN-5847: honor onboarding completion marker in auto-launch
CLI onboarding auto-launch now skips once the persisted completion marker is present. - Load `cliOnboardingCompletedAt` from global settings before launching onboarding when the central DB is missing. - Preserve existing skip precedence for non-TTY, command, flag, environment, and central DB guards. - Add unit and backcompat coverage for completion-marker and skipped-central-DB scenarios. - Document the updated auto-launch behavior and add a patch changeset. Files changed: .changeset/fn-5847-onboard-autolaunch-marker.md | 5 ++ docs/cli-reference.md | 12 +-- .../onboard-autolaunch-backcompat-e2e.test.ts | 7 ++ .../onboard-autolaunch-backcompat.test.ts | 8 ++ .../__tests__/onboard-autolaunch-bypass.test.ts | 7 ++ .../commands/__tests__/onboard-autolaunch.test.ts | 93 ++++++++++++++++++++++ packages/cli/src/commands/onboard-autolaunch.ts | 46 ++++++++++- 7 files changed, 171 insertions(+), 7 deletions(-) Fusion-Task-Id: FN-5847 Fusion-Task-Lineage: db3e474e-e4b8-4299-8087-07b6cd1f4518
This commit is contained in:
5
.changeset/fn-5847-onboard-autolaunch-marker.md
Normal file
5
.changeset/fn-5847-onboard-autolaunch-marker.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
CLI auto-launch now honors the persisted `cliOnboardingCompletedAt` marker so onboarding fires only once, even when the Central DB step was skipped during `fn onboard`.
|
||||||
@@ -72,13 +72,15 @@ On successful completion, Fusion records `cliOnboardingCompletedAt` in global
|
|||||||
settings.
|
settings.
|
||||||
|
|
||||||
Auto-launch behavior: before interactive commands, Fusion auto-launches onboarding
|
Auto-launch behavior: before interactive commands, Fusion auto-launches onboarding
|
||||||
only when the central DB at `getDefaultCentralDbPath()` is missing. Auto-launch
|
only when the central DB at `getDefaultCentralDbPath()` is missing and CLI
|
||||||
is skipped for `serve`, `daemon`, non-TTY runs, `--skip-onboarding`, and
|
onboarding has not already completed. Auto-launch is skipped for `serve`,
|
||||||
`FUSION_SKIP_ONBOARDING`.
|
`daemon`, non-TTY runs, `--skip-onboarding`, `FUSION_SKIP_ONBOARDING`, and once
|
||||||
|
`cliOnboardingCompletedAt` is set.
|
||||||
|
|
||||||
Backward-compatibility guard: existing setups are never blocked — when central DB
|
Backward-compatibility guard: existing setups are never blocked — when central DB
|
||||||
already exists (including the central-DB + registered-project case), onboarding
|
already exists (including the central-DB + registered-project case), or when the
|
||||||
does not auto-launch.
|
CLI onboarding completion marker exists even if the central DB step was skipped,
|
||||||
|
onboarding does not auto-launch.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
|||||||
cwd: "/repo/demo",
|
cwd: "/repo/demo",
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists,
|
pathExists,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
}),
|
}),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
@@ -38,6 +39,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
|||||||
centralDbPath: "/virtual/central.db",
|
centralDbPath: "/virtual/central.db",
|
||||||
isTTY: false,
|
isTTY: false,
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
}),
|
}),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
@@ -74,6 +76,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
|||||||
centralDbPath: "/virtual/central.db",
|
centralDbPath: "/virtual/central.db",
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
}),
|
}),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
@@ -92,6 +95,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
|||||||
isTTY: true,
|
isTTY: true,
|
||||||
env: { FUSION_SKIP_ONBOARDING: "true" },
|
env: { FUSION_SKIP_ONBOARDING: "true" },
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
}),
|
}),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
@@ -110,6 +114,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
|||||||
centralDbPath: "/virtual/central.db",
|
centralDbPath: "/virtual/central.db",
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
}),
|
}),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
@@ -130,6 +135,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
|||||||
centralDbPath: "/virtual/central.db",
|
centralDbPath: "/virtual/central.db",
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
}),
|
}),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
@@ -151,6 +157,7 @@ describe("maybeAutoLaunchOnboarding backward-compat e2e guard", () => {
|
|||||||
cwd: "/workspace/demo",
|
cwd: "/workspace/demo",
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists,
|
pathExists,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
}),
|
}),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
|||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: true,
|
centralDbExists: true,
|
||||||
projectInitialized: true,
|
projectInitialized: true,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "central-db-and-project-exist" });
|
).toEqual({ launch: false, reason: "central-db-and-project-exist" });
|
||||||
@@ -29,6 +30,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
|||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: true,
|
centralDbExists: true,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "central-db-exists" });
|
).toEqual({ launch: false, reason: "central-db-exists" });
|
||||||
@@ -41,6 +43,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
|||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: true,
|
projectInitialized: true,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||||
@@ -53,6 +56,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
|||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: false,
|
isTTY: false,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "non-tty" });
|
).toEqual({ launch: false, reason: "non-tty" });
|
||||||
@@ -66,6 +70,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
|||||||
args: [command],
|
args: [command],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "command-skip" });
|
).toEqual({ launch: false, reason: "command-skip" });
|
||||||
@@ -85,6 +90,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
|||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbPath: "/virtual/central.db",
|
centralDbPath: "/virtual/central.db",
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
}),
|
}),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
@@ -104,6 +110,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
|||||||
cwd: "/workspace/demo",
|
cwd: "/workspace/demo",
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists,
|
pathExists,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -123,6 +130,7 @@ describe("onboard autolaunch backward-compat guard", () => {
|
|||||||
centralDbPath: "/virtual/central.db",
|
centralDbPath: "/virtual/central.db",
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
}),
|
}),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ describe("onboard bypass reasons", () => {
|
|||||||
args: ["task", "list", "--skip-onboarding"],
|
args: ["task", "list", "--skip-onboarding"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "skip-flag" });
|
).toEqual({ launch: false, reason: "skip-flag" });
|
||||||
@@ -30,6 +31,7 @@ describe("onboard bypass reasons", () => {
|
|||||||
args: ["task", "list", "--skip-onboarding"],
|
args: ["task", "list", "--skip-onboarding"],
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
});
|
});
|
||||||
expect(runOnboard).not.toHaveBeenCalled();
|
expect(runOnboard).not.toHaveBeenCalled();
|
||||||
@@ -42,6 +44,7 @@ describe("onboard bypass reasons", () => {
|
|||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
env: { FUSION_SKIP_ONBOARDING: "1" },
|
env: { FUSION_SKIP_ONBOARDING: "1" },
|
||||||
}),
|
}),
|
||||||
@@ -54,6 +57,7 @@ describe("onboard bypass reasons", () => {
|
|||||||
env: { FUSION_SKIP_ONBOARDING: "1" },
|
env: { FUSION_SKIP_ONBOARDING: "1" },
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
});
|
});
|
||||||
expect(runOnboard).not.toHaveBeenCalled();
|
expect(runOnboard).not.toHaveBeenCalled();
|
||||||
@@ -76,6 +80,7 @@ describe("onboard bypass reasons", () => {
|
|||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||||
@@ -122,6 +127,7 @@ describe("extractGlobalProjectFlag", () => {
|
|||||||
skipOnboarding: parsed.skipOnboarding,
|
skipOnboarding: parsed.skipOnboarding,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,6 +138,7 @@ describe("extractGlobalProjectFlag", () => {
|
|||||||
skipOnboarding: parsed.skipOnboarding,
|
skipOnboarding: parsed.skipOnboarding,
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "skip-flag" });
|
).toEqual({ launch: false, reason: "skip-flag" });
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||||
@@ -25,11 +26,53 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
args: ["dashboard"],
|
args: ["dashboard"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: true, reason: "central-db-missing" });
|
).toEqual({ launch: true, reason: "central-db-missing" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("skips when onboarding completion marker is present", () => {
|
||||||
|
expect(
|
||||||
|
shouldAutoLaunchOnboarding({
|
||||||
|
command: "task",
|
||||||
|
args: ["task", "list"],
|
||||||
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: true,
|
||||||
|
isTTY: true,
|
||||||
|
}),
|
||||||
|
).toEqual({ launch: false, reason: "onboarding-complete-marker" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps non-TTY precedence over onboarding completion marker", () => {
|
||||||
|
expect(
|
||||||
|
shouldAutoLaunchOnboarding({
|
||||||
|
command: "task",
|
||||||
|
args: ["task", "list"],
|
||||||
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: true,
|
||||||
|
isTTY: false,
|
||||||
|
}),
|
||||||
|
).toEqual({ launch: false, reason: "non-tty" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps command skip precedence over onboarding completion marker", () => {
|
||||||
|
for (const command of ["serve", "daemon"]) {
|
||||||
|
expect(
|
||||||
|
shouldAutoLaunchOnboarding({
|
||||||
|
command,
|
||||||
|
args: [command],
|
||||||
|
centralDbExists: false,
|
||||||
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: true,
|
||||||
|
isTTY: true,
|
||||||
|
}),
|
||||||
|
).toEqual({ launch: false, reason: "command-skip" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("skips when skip flag is present in args", () => {
|
it("skips when skip flag is present in args", () => {
|
||||||
const args = ["task", "list", "--skip-onboarding"];
|
const args = ["task", "list", "--skip-onboarding"];
|
||||||
expect(args).toContain("--skip-onboarding");
|
expect(args).toContain("--skip-onboarding");
|
||||||
@@ -39,6 +82,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
args,
|
args,
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "skip-flag" });
|
).toEqual({ launch: false, reason: "skip-flag" });
|
||||||
@@ -51,6 +95,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: true,
|
centralDbExists: true,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "central-db-exists" });
|
).toEqual({ launch: false, reason: "central-db-exists" });
|
||||||
@@ -63,6 +108,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: false,
|
isTTY: false,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "non-tty" });
|
).toEqual({ launch: false, reason: "non-tty" });
|
||||||
@@ -75,6 +121,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
args: ["serve"],
|
args: ["serve"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "command-skip" });
|
).toEqual({ launch: false, reason: "command-skip" });
|
||||||
@@ -85,6 +132,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
args: ["daemon"],
|
args: ["daemon"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "command-skip" });
|
).toEqual({ launch: false, reason: "command-skip" });
|
||||||
@@ -97,6 +145,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
args: ["onboard"],
|
args: ["onboard"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
}),
|
}),
|
||||||
).toEqual({ launch: false, reason: "onboard-command" });
|
).toEqual({ launch: false, reason: "onboard-command" });
|
||||||
@@ -109,6 +158,7 @@ describe("shouldAutoLaunchOnboarding", () => {
|
|||||||
args: ["task", "list"],
|
args: ["task", "list"],
|
||||||
centralDbExists: false,
|
centralDbExists: false,
|
||||||
projectInitialized: false,
|
projectInitialized: false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
env: { FUSION_SKIP_ONBOARDING: "1" },
|
env: { FUSION_SKIP_ONBOARDING: "1" },
|
||||||
}),
|
}),
|
||||||
@@ -130,6 +180,7 @@ describe("maybeAutoLaunchOnboarding", () => {
|
|||||||
centralDbPath: "/virtual/fusion-central.db",
|
centralDbPath: "/virtual/fusion-central.db",
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -145,12 +196,53 @@ describe("maybeAutoLaunchOnboarding", () => {
|
|||||||
centralDbPath: "/virtual/fusion-central.db",
|
centralDbPath: "/virtual/fusion-central.db",
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists: () => true,
|
pathExists: () => true,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(runOnboard).not.toHaveBeenCalled();
|
expect(runOnboard).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not invoke runOnboard when injected marker is complete", async () => {
|
||||||
|
const runOnboard = vi.fn();
|
||||||
|
|
||||||
|
await maybeAutoLaunchOnboarding({
|
||||||
|
command: "task",
|
||||||
|
args: ["task", "list"],
|
||||||
|
centralDbPath: "/virtual/fusion-central.db",
|
||||||
|
isTTY: true,
|
||||||
|
pathExists: () => false,
|
||||||
|
loadOnboardingComplete: () => true,
|
||||||
|
runOnboard,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(runOnboard).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("treats marker resolver failure as incomplete and emits diagnostic", async () => {
|
||||||
|
const runOnboard = vi.fn();
|
||||||
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||||
|
|
||||||
|
await maybeAutoLaunchOnboarding({
|
||||||
|
command: "task",
|
||||||
|
args: ["task", "list"],
|
||||||
|
centralDbPath: "/virtual/fusion-central.db",
|
||||||
|
isTTY: true,
|
||||||
|
pathExists: () => false,
|
||||||
|
loadOnboardingComplete: () => {
|
||||||
|
throw new Error("settings unavailable");
|
||||||
|
},
|
||||||
|
runOnboard,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(errorSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining(
|
||||||
|
"[onboard-autolaunch] onboarding marker probe failed; treating as incomplete: settings unavailable",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(runOnboard).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
it("swallows runOnboard errors and emits diagnostic", async () => {
|
it("swallows runOnboard errors and emits diagnostic", async () => {
|
||||||
const runOnboard = vi.fn().mockRejectedValue(new Error("boom"));
|
const runOnboard = vi.fn().mockRejectedValue(new Error("boom"));
|
||||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined);
|
||||||
@@ -162,6 +254,7 @@ describe("maybeAutoLaunchOnboarding", () => {
|
|||||||
centralDbPath: "/virtual/fusion-central.db",
|
centralDbPath: "/virtual/fusion-central.db",
|
||||||
isTTY: true,
|
isTTY: true,
|
||||||
pathExists: () => false,
|
pathExists: () => false,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
runOnboard,
|
runOnboard,
|
||||||
}),
|
}),
|
||||||
).resolves.toBeUndefined();
|
).resolves.toBeUndefined();
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { getDefaultCentralDbPath } from "@fusion/core";
|
import { getDefaultCentralDbPath, GlobalSettingsStore } from "@fusion/core";
|
||||||
|
|
||||||
import { isTTYAvailable } from "./dashboard-tui/index.js";
|
import { isTTYAvailable } from "./dashboard-tui/index.js";
|
||||||
|
import { isCliOnboardingComplete } from "./onboard.js";
|
||||||
|
|
||||||
export interface AutoLaunchInput {
|
export interface AutoLaunchInput {
|
||||||
command: string;
|
command: string;
|
||||||
args: string[];
|
args: string[];
|
||||||
centralDbExists: boolean;
|
centralDbExists: boolean;
|
||||||
projectInitialized: boolean;
|
projectInitialized: boolean;
|
||||||
|
cliOnboardingCompleted: boolean;
|
||||||
isTTY: boolean;
|
isTTY: boolean;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
skipOnboarding?: boolean;
|
skipOnboarding?: boolean;
|
||||||
@@ -51,6 +53,10 @@ export function shouldAutoLaunchOnboarding(input: AutoLaunchInput): AutoLaunchDe
|
|||||||
return { launch: false, reason: "skip-env" };
|
return { launch: false, reason: "skip-env" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.cliOnboardingCompleted) {
|
||||||
|
return { launch: false, reason: "onboarding-complete-marker" };
|
||||||
|
}
|
||||||
|
|
||||||
if (input.centralDbExists && input.projectInitialized) {
|
if (input.centralDbExists && input.projectInitialized) {
|
||||||
return { launch: false, reason: "central-db-and-project-exist" };
|
return { launch: false, reason: "central-db-and-project-exist" };
|
||||||
}
|
}
|
||||||
@@ -88,6 +94,30 @@ export interface MaybeAutoLaunchDeps {
|
|||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
runOnboard?: RunOnboard;
|
runOnboard?: RunOnboard;
|
||||||
pathExists?: (path: string) => boolean;
|
pathExists?: (path: string) => boolean;
|
||||||
|
cliOnboardingCompleted?: boolean;
|
||||||
|
loadOnboardingComplete?: () => Promise<boolean> | boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCliOnboardingComplete(): Promise<boolean> {
|
||||||
|
const globalSettingsStore = new GlobalSettingsStore();
|
||||||
|
await globalSettingsStore.init();
|
||||||
|
const settings = await globalSettingsStore.getSettings();
|
||||||
|
return isCliOnboardingComplete(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveCliOnboardingCompleted(deps: MaybeAutoLaunchDeps): Promise<boolean> {
|
||||||
|
if (deps.cliOnboardingCompleted !== undefined) {
|
||||||
|
return deps.cliOnboardingCompleted;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadOnboardingComplete = deps.loadOnboardingComplete ?? loadCliOnboardingComplete;
|
||||||
|
try {
|
||||||
|
return await loadOnboardingComplete();
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
console.error(`[onboard-autolaunch] onboarding marker probe failed; treating as incomplete: ${message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Promise<void> {
|
export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Promise<void> {
|
||||||
@@ -109,14 +139,26 @@ export async function maybeAutoLaunchOnboarding(deps: MaybeAutoLaunchDeps): Prom
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decision = shouldAutoLaunchOnboarding({
|
const baseDecisionInput = {
|
||||||
command: deps.command,
|
command: deps.command,
|
||||||
args: deps.args,
|
args: deps.args,
|
||||||
centralDbExists,
|
centralDbExists,
|
||||||
projectInitialized,
|
projectInitialized,
|
||||||
|
cliOnboardingCompleted: false,
|
||||||
isTTY,
|
isTTY,
|
||||||
env,
|
env,
|
||||||
skipOnboarding: deps.skipOnboarding,
|
skipOnboarding: deps.skipOnboarding,
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseDecision = shouldAutoLaunchOnboarding(baseDecisionInput);
|
||||||
|
if (!baseDecision.launch) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cliOnboardingCompleted = await resolveCliOnboardingCompleted(deps);
|
||||||
|
const decision = shouldAutoLaunchOnboarding({
|
||||||
|
...baseDecisionInput,
|
||||||
|
cliOnboardingCompleted,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!decision.launch) {
|
if (!decision.launch) {
|
||||||
|
|||||||
Reference in New Issue
Block a user