fix(config): require doctor for invalid configs (#764 — thanks @mukhtharcm)
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
## 2026.1.12-4
|
## 2026.1.12-4
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Onboarding/Configure: refuse to proceed with invalid configs; run `clawdbot doctor` first to avoid wiping custom fields. (#764 — thanks @mukhtharcm)
|
||||||
- Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake)
|
- Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake)
|
||||||
- Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs.
|
- Discord/Slack: centralize reply-thread planning so auto-thread replies stay in the created thread without parent reply refs.
|
||||||
- Update: run `clawdbot doctor --non-interactive` during updates to avoid TTY hangs. (#781 — thanks @ronyrus)
|
- Update: run `clawdbot doctor --non-interactive` during updates to avoid TTY hangs. (#781 — thanks @ronyrus)
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (
|
|||||||
|
|
||||||
1) **Existing config detection**
|
1) **Existing config detection**
|
||||||
- If `~/.clawdbot/clawdbot.json` exists, choose **Keep / Modify / Reset**.
|
- If `~/.clawdbot/clawdbot.json` exists, choose **Keep / Modify / Reset**.
|
||||||
|
- If the config is invalid or contains legacy keys, the wizard stops and asks
|
||||||
|
you to run `clawdbot doctor` before continuing.
|
||||||
- Reset uses `trash` (never `rm`) and offers scopes:
|
- Reset uses `trash` (never `rm`) and offers scopes:
|
||||||
- Config only
|
- Config only
|
||||||
- Config + credentials + sessions
|
- Config + credentials + sessions
|
||||||
|
|||||||
@@ -586,7 +586,7 @@ export async function runConfigureWizard(
|
|||||||
const prompter = createClackPrompter();
|
const prompter = createClackPrompter();
|
||||||
|
|
||||||
const snapshot = await readConfigFileSnapshot();
|
const snapshot = await readConfigFileSnapshot();
|
||||||
let baseConfig: ClawdbotConfig = snapshot.valid ? snapshot.config : {};
|
const baseConfig: ClawdbotConfig = snapshot.valid ? snapshot.config : {};
|
||||||
|
|
||||||
if (snapshot.exists) {
|
if (snapshot.exists) {
|
||||||
const title = snapshot.valid
|
const title = snapshot.valid
|
||||||
@@ -604,14 +604,11 @@ export async function runConfigureWizard(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!snapshot.valid) {
|
if (!snapshot.valid) {
|
||||||
const reset = guardCancel(
|
outro(
|
||||||
await confirm({
|
"Config invalid. Run `clawdbot doctor` to repair it, then re-run configure.",
|
||||||
message: "Config invalid. Start fresh?",
|
|
||||||
initialValue: true,
|
|
||||||
}),
|
|
||||||
runtime,
|
|
||||||
);
|
);
|
||||||
if (reset) baseConfig = {};
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,6 +128,13 @@ export async function runNonInteractiveOnboarding(
|
|||||||
runtime: RuntimeEnv = defaultRuntime,
|
runtime: RuntimeEnv = defaultRuntime,
|
||||||
) {
|
) {
|
||||||
const snapshot = await readConfigFileSnapshot();
|
const snapshot = await readConfigFileSnapshot();
|
||||||
|
if (snapshot.exists && !snapshot.valid) {
|
||||||
|
runtime.error(
|
||||||
|
"Config invalid. Run `clawdbot doctor` to repair it, then re-run onboarding.",
|
||||||
|
);
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const baseConfig: ClawdbotConfig = snapshot.valid ? snapshot.config : {};
|
const baseConfig: ClawdbotConfig = snapshot.valid ? snapshot.config : {};
|
||||||
const mode = opts.mode ?? "local";
|
const mode = opts.mode ?? "local";
|
||||||
if (mode !== "local" && mode !== "remote") {
|
if (mode !== "local" && mode !== "remote") {
|
||||||
|
|||||||
@@ -287,6 +287,10 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const legacyIssues = findLegacyConfigIssues(resolved);
|
const legacyIssues = findLegacyConfigIssues(resolved);
|
||||||
|
const resolvedConfig =
|
||||||
|
typeof resolved === "object" && resolved !== null
|
||||||
|
? (resolved as ClawdbotConfig)
|
||||||
|
: {};
|
||||||
|
|
||||||
const validated = validateConfigObject(resolved);
|
const validated = validateConfigObject(resolved);
|
||||||
if (!validated.ok) {
|
if (!validated.ok) {
|
||||||
@@ -296,7 +300,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
|||||||
raw,
|
raw,
|
||||||
parsed: parsedRes.parsed,
|
parsed: parsedRes.parsed,
|
||||||
valid: false,
|
valid: false,
|
||||||
config: resolved as ClawdbotConfig,
|
config: resolvedConfig,
|
||||||
issues: validated.issues,
|
issues: validated.issues,
|
||||||
legacyIssues,
|
legacyIssues,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,6 +80,58 @@ vi.mock("../tui/tui.js", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe("runOnboardingWizard", () => {
|
describe("runOnboardingWizard", () => {
|
||||||
|
it("exits when config is invalid", async () => {
|
||||||
|
readConfigFileSnapshot.mockResolvedValueOnce({
|
||||||
|
path: "/tmp/.clawdbot/clawdbot.json",
|
||||||
|
exists: true,
|
||||||
|
raw: "{}",
|
||||||
|
parsed: {},
|
||||||
|
valid: false,
|
||||||
|
config: {},
|
||||||
|
issues: [{ path: "routing.allowFrom", message: "Legacy key" }],
|
||||||
|
legacyIssues: [{ path: "routing.allowFrom", message: "Legacy key" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const select: WizardPrompter["select"] = vi.fn(async () => "quickstart");
|
||||||
|
const prompter: WizardPrompter = {
|
||||||
|
intro: vi.fn(async () => {}),
|
||||||
|
outro: vi.fn(async () => {}),
|
||||||
|
note: vi.fn(async () => {}),
|
||||||
|
select,
|
||||||
|
multiselect: vi.fn(async () => []),
|
||||||
|
text: vi.fn(async () => ""),
|
||||||
|
confirm: vi.fn(async () => false),
|
||||||
|
progress: vi.fn(() => ({ update: vi.fn(), stop: vi.fn() })),
|
||||||
|
};
|
||||||
|
|
||||||
|
const runtime: RuntimeEnv = {
|
||||||
|
log: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
exit: vi.fn((code: number) => {
|
||||||
|
throw new Error(`exit:${code}`);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runOnboardingWizard(
|
||||||
|
{
|
||||||
|
flow: "quickstart",
|
||||||
|
authChoice: "skip",
|
||||||
|
installDaemon: false,
|
||||||
|
skipProviders: true,
|
||||||
|
skipSkills: true,
|
||||||
|
skipHealth: true,
|
||||||
|
skipUi: true,
|
||||||
|
},
|
||||||
|
runtime,
|
||||||
|
prompter,
|
||||||
|
),
|
||||||
|
).rejects.toThrow("exit:1");
|
||||||
|
|
||||||
|
expect(select).not.toHaveBeenCalled();
|
||||||
|
expect(prompter.outro).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("skips prompts and setup steps when flags are set", async () => {
|
it("skips prompts and setup steps when flags are set", async () => {
|
||||||
const select: WizardPrompter["select"] = vi.fn(async () => "quickstart");
|
const select: WizardPrompter["select"] = vi.fn(async () => "quickstart");
|
||||||
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
|
const multiselect: WizardPrompter["multiselect"] = vi.fn(async () => []);
|
||||||
|
|||||||
@@ -96,6 +96,14 @@ export async function runOnboardingWizard(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!snapshot.valid) {
|
||||||
|
await prompter.outro(
|
||||||
|
"Config invalid. Run `clawdbot doctor` to repair it, then re-run onboarding.",
|
||||||
|
);
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const action = (await prompter.select({
|
const action = (await prompter.select({
|
||||||
message: "Config handling",
|
message: "Config handling",
|
||||||
options: [
|
options: [
|
||||||
@@ -124,8 +132,6 @@ export async function runOnboardingWizard(
|
|||||||
})) as ResetScope;
|
})) as ResetScope;
|
||||||
await handleReset(resetScope, resolveUserPath(workspaceDefault), runtime);
|
await handleReset(resetScope, resolveUserPath(workspaceDefault), runtime);
|
||||||
baseConfig = {};
|
baseConfig = {};
|
||||||
} else if (action === "keep" && !snapshot.valid) {
|
|
||||||
baseConfig = {};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user