

























@@ -197,6 +197,43 @@ function codexPluginPlan(overrides: Partial<MigrationPlan> = {}): MigrationPlan
197197};
198198}
199199200+type MockCallSource = {
201+mock: {
202+calls: ReadonlyArray<ReadonlyArray<unknown>>;
203+};
204+};
205+206+type MigrationSelectionPrompt = {
207+initialValues?: unknown;
208+message?: unknown;
209+options?: Array<{ hint?: unknown; label?: unknown; value?: unknown }>;
210+required?: unknown;
211+};
212+213+function mockCall(source: MockCallSource, callIndex = 0): ReadonlyArray<unknown> {
214+const call = source.mock.calls.at(callIndex);
215+if (!call) {
216+throw new Error(`Expected mock call ${callIndex}`);
217+}
218+return call;
219+}
220+221+function mockArg(source: MockCallSource, callIndex: number, argIndex: number) {
222+return mockCall(source, callIndex)[argIndex];
223+}
224+225+function multiselectPrompt(callIndex = 0): MigrationSelectionPrompt {
226+return mockArg(mocks.multiselect, callIndex, 0) as MigrationSelectionPrompt;
227+}
228+229+function firstApplyContext(): Record<string, unknown> {
230+return mockArg(mocks.provider.apply, 0, 0) as Record<string, unknown>;
231+}
232+233+function firstAppliedPlan(): MigrationPlan {
234+return mockArg(mocks.provider.apply, 0, 1) as MigrationPlan;
235+}
236+200237const runtime: RuntimeEnv = {
201238log: vi.fn(),
202239error: vi.fn(),
@@ -268,8 +305,8 @@ describe("migrateApplyCommand", () => {
268305expect(mocks.provider.plan).toHaveBeenCalledTimes(1);
269306expect(mocks.promptYesNo).toHaveBeenCalledWith("Apply this migration now?", false);
270307expect(mocks.backupCreateCommand).toHaveBeenCalled();
271-expect(typeof mocks.provider.apply.mock.calls.at(0)?.[0]).toBe("object");
272-expect(mocks.provider.apply.mock.calls.at(0)?.[1]).toBe(planned);
308+expect(typeof firstApplyContext()).toBe("object");
309+expect(firstAppliedPlan()).toBe(planned);
273310});
274311275312it("prompts for Codex skills before interactive default apply", async () => {
@@ -291,26 +328,19 @@ describe("migrateApplyCommand", () => {
291328292329await migrateDefaultCommand(runtime, { provider: "codex" });
293330294-const selectionPrompt = mocks.multiselect.mock.calls.at(0)?.[0] as
295-| {
296-initialValues?: unknown;
297-message?: unknown;
298-options?: Array<{ label?: unknown; value?: unknown }>;
299-required?: unknown;
300-}
301-| undefined;
302-expect(String(selectionPrompt?.message)).toContain("Select Codex skills");
303-expect(selectionPrompt?.initialValues).toStrictEqual(["skill:alpha", "skill:beta"]);
304-expect(selectionPrompt?.required).toBe(false);
305-expect(selectionPrompt?.options?.map(({ label, value }) => ({ label, value }))).toStrictEqual([
331+const selectionPrompt = multiselectPrompt();
332+expect(String(selectionPrompt.message)).toContain("Select Codex skills");
333+expect(selectionPrompt.initialValues).toStrictEqual(["skill:alpha", "skill:beta"]);
334+expect(selectionPrompt.required).toBe(false);
335+expect(selectionPrompt.options?.map(({ label, value }) => ({ label, value }))).toStrictEqual([
306336{ value: MIGRATION_SKILL_SELECTION_SKIP, label: "Skip for now" },
307337{ value: MIGRATION_SKILL_SELECTION_TOGGLE_ALL_ON, label: "Toggle all on" },
308338{ value: MIGRATION_SKILL_SELECTION_TOGGLE_ALL_OFF, label: "Toggle all off" },
309339{ value: "skill:alpha", label: "alpha" },
310340{ value: "skill:beta", label: "beta" },
311341]);
312342expect(mocks.promptYesNo).toHaveBeenCalledWith("Apply this migration now?", false);
313-const appliedPlan = mocks.provider.apply.mock.calls.at(0)?.[1] as MigrationPlan;
343+const appliedPlan = firstAppliedPlan();
314344expect(appliedPlan.summary.planned).toBe(2);
315345expect(appliedPlan.summary.skipped).toBe(1);
316346expect(appliedPlan.summary.conflicts).toBe(0);
@@ -356,30 +386,21 @@ describe("migrateApplyCommand", () => {
356386await migrateDefaultCommand(runtime, { provider: "codex" });
357387358388expect(mocks.multiselect).toHaveBeenCalledTimes(2);
359-const skillPrompt = mocks.multiselect.mock.calls.at(0)?.[0] as
360-| { message?: unknown }
361-| undefined;
362-expect(String(skillPrompt?.message)).toContain("Select Codex skills");
363-const pluginPrompt = mocks.multiselect.mock.calls.at(1)?.[0] as
364-| {
365-initialValues?: unknown;
366-message?: unknown;
367-options?: Array<{ label?: unknown; value?: unknown }>;
368-required?: unknown;
369-}
370-| undefined;
371-expect(String(pluginPrompt?.message)).toContain("Select native Codex plugins");
372-expect(pluginPrompt?.initialValues).toStrictEqual(["plugin:google-calendar", "plugin:gmail"]);
373-expect(pluginPrompt?.required).toBe(false);
374-expect(pluginPrompt?.options?.map(({ label, value }) => ({ label, value }))).toStrictEqual([
389+const skillPrompt = multiselectPrompt();
390+expect(String(skillPrompt.message)).toContain("Select Codex skills");
391+const pluginPrompt = multiselectPrompt(1);
392+expect(String(pluginPrompt.message)).toContain("Select native Codex plugins");
393+expect(pluginPrompt.initialValues).toStrictEqual(["plugin:google-calendar", "plugin:gmail"]);
394+expect(pluginPrompt.required).toBe(false);
395+expect(pluginPrompt.options?.map(({ label, value }) => ({ label, value }))).toStrictEqual([
375396{ value: MIGRATION_SKILL_SELECTION_SKIP, label: "Skip for now" },
376397{ value: MIGRATION_SKILL_SELECTION_TOGGLE_ALL_ON, label: "Toggle all on" },
377398{ value: MIGRATION_SKILL_SELECTION_TOGGLE_ALL_OFF, label: "Toggle all off" },
378399{ value: "plugin:google-calendar", label: "google-calendar" },
379400{ value: "plugin:gmail", label: "gmail" },
380401]);
381402expect(mocks.promptYesNo).toHaveBeenCalledWith("Apply this migration now?", false);
382-const appliedPlan = mocks.provider.apply.mock.calls.at(0)?.[1] as MigrationPlan;
403+const appliedPlan = firstAppliedPlan();
383404expect(appliedPlan.summary.planned).toBe(4);
384405expect(appliedPlan.summary.skipped).toBe(2);
385406expect(appliedPlan.summary.conflicts).toBe(0);
@@ -439,12 +460,10 @@ describe("migrateApplyCommand", () => {
439460440461await migrateDefaultCommand(runtime, { provider: "codex" });
441462442-const pluginPrompt = mocks.multiselect.mock.calls.at(1)?.[0] as
443-| { initialValues?: unknown; message?: unknown }
444-| undefined;
445-expect(String(pluginPrompt?.message)).toContain("Select native Codex plugins");
446-expect(pluginPrompt?.initialValues).toStrictEqual(["plugin:google-calendar", "plugin:gmail"]);
447-const appliedPlan = mocks.provider.apply.mock.calls.at(0)?.[1] as MigrationPlan;
463+const pluginPrompt = multiselectPrompt(1);
464+expect(String(pluginPrompt.message)).toContain("Select native Codex plugins");
465+expect(pluginPrompt.initialValues).toStrictEqual(["plugin:google-calendar", "plugin:gmail"]);
466+const appliedPlan = firstAppliedPlan();
448467expect(appliedPlan.summary.planned).toBe(4);
449468expect(appliedPlan.summary.skipped).toBe(2);
450469expect(appliedPlan.summary.conflicts).toBe(0);
@@ -503,22 +522,16 @@ describe("migrateApplyCommand", () => {
503522504523await migrateDefaultCommand(runtime, { provider: "codex" });
505524506-const pluginPrompt = mocks.multiselect.mock.calls.at(0)?.[0] as
507-| {
508-initialValues?: unknown;
509-message?: unknown;
510-options?: Array<{ hint?: unknown; label?: unknown; value?: unknown }>;
511-}
512-| undefined;
513-expect(String(pluginPrompt?.message)).toContain("Select native Codex plugins");
514-expect(pluginPrompt?.initialValues).toStrictEqual(["plugin:gmail"]);
515-const optionsByValue = new Map(pluginPrompt?.options?.map((option) => [option.value, option]));
525+const pluginPrompt = multiselectPrompt();
526+expect(String(pluginPrompt.message)).toContain("Select native Codex plugins");
527+expect(pluginPrompt.initialValues).toStrictEqual(["plugin:gmail"]);
528+const optionsByValue = new Map(pluginPrompt.options?.map((option) => [option.value, option]));
516529expect(optionsByValue.get("plugin:google-calendar")?.label).toBe("google-calendar");
517530expect(String(optionsByValue.get("plugin:google-calendar")?.hint)).toContain(
518531"conflict: plugin exists",
519532);
520533expect(optionsByValue.get("plugin:gmail")?.label).toBe("gmail");
521-const appliedPlan = mocks.provider.apply.mock.calls.at(0)?.[1] as MigrationPlan;
534+const appliedPlan = firstAppliedPlan();
522535expect(appliedPlan.summary.planned).toBe(2);
523536expect(appliedPlan.summary.skipped).toBe(1);
524537expect(appliedPlan.summary.conflicts).toBe(0);
@@ -592,10 +605,8 @@ describe("migrateApplyCommand", () => {
592605593606const result = await migrateDefaultCommand(runtime, { provider: "codex" });
594607595-const pluginPrompt = mocks.multiselect.mock.calls.at(0)?.[0] as
596-| { message?: unknown }
597-| undefined;
598-expect(String(pluginPrompt?.message)).toContain("Select native Codex plugins");
608+const pluginPrompt = multiselectPrompt();
609+expect(String(pluginPrompt.message)).toContain("Select native Codex plugins");
599610expect(mocks.promptYesNo).not.toHaveBeenCalled();
600611expect(mocks.backupCreateCommand).not.toHaveBeenCalled();
601612expect(mocks.provider.apply).not.toHaveBeenCalled();
@@ -634,7 +645,7 @@ describe("migrateApplyCommand", () => {
634645635646expect(mocks.multiselect).not.toHaveBeenCalled();
636647expect(mocks.promptYesNo).toHaveBeenCalledWith("Apply this migration now?", false);
637-const appliedPlan = mocks.provider.apply.mock.calls.at(0)?.[1] as MigrationPlan;
648+const appliedPlan = firstAppliedPlan();
638649expect(appliedPlan.summary.planned).toBe(2);
639650expect(appliedPlan.summary.skipped).toBe(1);
640651expect(appliedPlan.summary.conflicts).toBe(0);
@@ -689,15 +700,10 @@ describe("migrateApplyCommand", () => {
689700690701await migrateDefaultCommand(runtime, { provider: "codex" });
691702692-const skillPrompt = mocks.multiselect.mock.calls.at(0)?.[0] as
693-| {
694-initialValues?: unknown;
695-options?: Array<{ label?: unknown; value?: unknown }>;
696-}
697-| undefined;
698-expect(skillPrompt?.initialValues).toStrictEqual(["skill:alpha"]);
703+const skillPrompt = multiselectPrompt();
704+expect(skillPrompt.initialValues).toStrictEqual(["skill:alpha"]);
699705const skillOptionsByValue = new Map(
700-skillPrompt?.options?.map((option) => [option.value, option]),
706+skillPrompt.options?.map((option) => [option.value, option]),
701707);
702708expect(skillOptionsByValue.get("skill:beta")?.label).toBe("beta");
703709expect(mocks.promptYesNo).toHaveBeenCalledWith("Apply this migration now?", false);
@@ -741,7 +747,7 @@ describe("migrateApplyCommand", () => {
741747expect(mocks.multiselect).toHaveBeenCalledTimes(2);
742748expect(runtime.log).toHaveBeenCalledWith("Codex skill migration skipped for now.");
743749expect(mocks.promptYesNo).toHaveBeenCalledWith("Apply this migration now?", false);
744-const appliedPlan = mocks.provider.apply.mock.calls.at(0)?.[1] as MigrationPlan;
750+const appliedPlan = firstAppliedPlan();
745751expect(appliedPlan.summary.planned).toBe(3);
746752expect(appliedPlan.summary.skipped).toBe(3);
747753expect(appliedPlan.summary.conflicts).toBe(0);
@@ -802,7 +808,7 @@ describe("migrateApplyCommand", () => {
802808803809await migrateDefaultCommand(runtime, { provider: "codex" });
804810805-let appliedPlan = mocks.provider.apply.mock.calls.at(0)?.[1] as MigrationPlan;
811+let appliedPlan = firstAppliedPlan();
806812expect(appliedPlan.summary.planned).toBe(3);
807813expect(appliedPlan.summary.skipped).toBe(0);
808814expect(appliedPlan.summary.conflicts).toBe(0);
@@ -983,7 +989,7 @@ describe("migrateApplyCommand", () => {
983989984990await migrateApplyCommand(runtime, { provider: "codex", yes: true, skills: ["alpha"] });
985991986-const appliedPlan = mocks.provider.apply.mock.calls.at(0)?.[1] as MigrationPlan;
992+const appliedPlan = firstAppliedPlan();
987993expect(appliedPlan.summary.planned).toBe(2);
988994expect(appliedPlan.summary.skipped).toBe(1);
989995expect(appliedPlan.summary.conflicts).toBe(0);
@@ -1007,7 +1013,7 @@ describe("migrateApplyCommand", () => {
1007101310081014await migrateApplyCommand(runtime, { provider: "codex", yes: true, plugins: ["gmail"] });
100910151010-const appliedPlan = mocks.provider.apply.mock.calls.at(0)?.[1] as MigrationPlan;
1016+const appliedPlan = firstAppliedPlan();
10111017expect(appliedPlan.summary.planned).toBe(2);
10121018expect(appliedPlan.summary.skipped).toBe(1);
10131019expect(appliedPlan.summary.conflicts).toBe(0);
@@ -1031,15 +1037,13 @@ describe("migrateApplyCommand", () => {
1031103710321038const result = await migrateApplyCommand(runtime, { provider: "hermes", yes: true });
103310391034-const backupCall = mocks.backupCreateCommand.mock.calls.at(0);
1040+const backupCall = mockCall(mocks.backupCreateCommand);
10351041expect(typeof (backupCall?.[0] as { log?: unknown } | undefined)?.log).toBe("function");
10361042expect(backupCall?.[1]).toStrictEqual({ output: undefined, verify: true });
1037-const applyContext = mocks.provider.apply.mock.calls.at(0)?.[0] as
1038-| { backupPath?: unknown; reportDir?: unknown }
1039-| undefined;
1040-expect(applyContext?.backupPath).toBe("/tmp/openclaw-backup.tgz");
1041-expect(String(applyContext?.reportDir)).toContain("/migration/hermes/");
1042-expect(mocks.provider.apply.mock.calls.at(0)?.[1]).toBe(planned);
1043+const applyContext = firstApplyContext();
1044+expect(applyContext.backupPath).toBe("/tmp/openclaw-backup.tgz");
1045+expect(String(applyContext.reportDir)).toContain("/migration/hermes/");
1046+expect(firstAppliedPlan()).toBe(planned);
10431047expect(result.backupPath).toBe("/tmp/openclaw-backup.tgz");
10441048});
10451049@@ -1150,8 +1154,8 @@ describe("migrateApplyCommand", () => {
11501154await migrateDefaultCommand(runtime, { provider: "hermes", yes: true });
1151115511521156expect(mocks.provider.plan).toHaveBeenCalledTimes(1);
1153-expect(typeof mocks.provider.apply.mock.calls.at(0)?.[0]).toBe("object");
1154-expect(mocks.provider.apply.mock.calls.at(0)?.[1]).toBe(planned);
1157+expect(typeof firstApplyContext()).toBe("object");
1158+expect(firstAppliedPlan()).toBe(planned);
11551159});
1156116011571161it("fails after writing JSON output when apply reports item errors", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。