






















@@ -34,7 +34,10 @@ vi.mock("../cli/prompt.js", () => ({
3434vi.mock("@clack/prompts", () => ({
3535cancel: mocks.clackCancel,
3636isCancel: mocks.clackIsCancel,
37-multiselect: mocks.multiselect,
37+}));
38+39+vi.mock("./migrate/skill-selection-prompt.js", () => ({
40+promptMigrationSkillSelectionValues: mocks.multiselect,
3841}));
39424043vi.mock("../plugins/migration-provider-runtime.js", () => ({
@@ -47,6 +50,11 @@ vi.mock("./backup.js", () => ({
4750backupCreateCommand: mocks.backupCreateCommand,
4851}));
495253+const {
54+MIGRATION_SKILL_SELECTION_SKIP,
55+MIGRATION_SKILL_SELECTION_TOGGLE_ALL_OFF,
56+MIGRATION_SKILL_SELECTION_TOGGLE_ALL_ON,
57+} = await import("./migrate/selection.js");
5058const { migrateApplyCommand, migrateDefaultCommand } = await import("./migrate.js");
51595260function plan(overrides: Partial<MigrationPlan> = {}): MigrationPlan {
@@ -215,10 +223,22 @@ describe("migrateApplyCommand", () => {
215223message: expect.stringContaining("Select Codex skills"),
216224initialValues: ["skill:alpha", "skill:beta"],
217225required: false,
218-options: expect.arrayContaining([
226+options: [
227+expect.objectContaining({
228+value: MIGRATION_SKILL_SELECTION_SKIP,
229+label: "Skip for now",
230+}),
231+expect.objectContaining({
232+value: MIGRATION_SKILL_SELECTION_TOGGLE_ALL_ON,
233+label: "Toggle all on",
234+}),
235+expect.objectContaining({
236+value: MIGRATION_SKILL_SELECTION_TOGGLE_ALL_OFF,
237+label: "Toggle all off",
238+}),
219239expect.objectContaining({ value: "skill:alpha", label: "alpha" }),
220240expect.objectContaining({ value: "skill:beta", label: "beta" }),
221-]),
241+],
222242}),
223243);
224244expect(mocks.promptYesNo).toHaveBeenCalledWith("Apply this migration now?", false);
@@ -237,6 +257,155 @@ describe("migrateApplyCommand", () => {
237257);
238258});
239259260+it("leaves conflicting Codex skills unchecked by default", async () => {
261+Object.defineProperty(process.stdin, "isTTY", {
262+configurable: true,
263+value: true,
264+});
265+const planned = codexSkillPlan({
266+summary: {
267+total: 3,
268+planned: 2,
269+migrated: 0,
270+skipped: 0,
271+conflicts: 1,
272+errors: 0,
273+sensitive: 0,
274+},
275+items: [
276+{
277+id: "skill:alpha",
278+kind: "skill",
279+action: "copy",
280+status: "planned",
281+details: { skillName: "alpha" },
282+},
283+{
284+id: "skill:beta",
285+kind: "skill",
286+action: "copy",
287+status: "conflict",
288+reason: "target exists",
289+details: { skillName: "beta" },
290+},
291+{
292+id: "archive:config.toml",
293+kind: "archive",
294+action: "archive",
295+status: "planned",
296+},
297+],
298+});
299+mocks.provider.plan.mockResolvedValue(planned);
300+mocks.multiselect.mockResolvedValue(["skill:alpha"]);
301+mocks.promptYesNo.mockResolvedValue(false);
302+303+await migrateDefaultCommand(runtime, { provider: "codex" });
304+305+expect(mocks.multiselect).toHaveBeenCalledWith(
306+expect.objectContaining({
307+initialValues: ["skill:alpha"],
308+options: expect.arrayContaining([
309+expect.objectContaining({ value: "skill:beta", label: "beta" }),
310+]),
311+}),
312+);
313+expect(mocks.promptYesNo).toHaveBeenCalledWith("Apply this migration now?", false);
314+expect(mocks.provider.apply).not.toHaveBeenCalled();
315+});
316+317+it("skips interactive Codex skill migration when Skip for now is selected", async () => {
318+Object.defineProperty(process.stdin, "isTTY", {
319+configurable: true,
320+value: true,
321+});
322+const planned = codexSkillPlan();
323+mocks.provider.plan.mockResolvedValue(planned);
324+mocks.multiselect.mockResolvedValue([MIGRATION_SKILL_SELECTION_SKIP]);
325+326+const result = await migrateDefaultCommand(runtime, { provider: "codex" });
327+328+expect(result).toBe(planned);
329+expect(mocks.promptYesNo).not.toHaveBeenCalled();
330+expect(mocks.backupCreateCommand).not.toHaveBeenCalled();
331+expect(mocks.provider.apply).not.toHaveBeenCalled();
332+expect(runtime.log).toHaveBeenCalledWith("Codex skill migration skipped for now.");
333+});
334+335+it("applies Toggle all off before interactive Codex migration apply", async () => {
336+Object.defineProperty(process.stdin, "isTTY", {
337+configurable: true,
338+value: true,
339+});
340+const planned = codexSkillPlan();
341+mocks.provider.plan.mockResolvedValue(planned);
342+mocks.multiselect.mockResolvedValue([MIGRATION_SKILL_SELECTION_TOGGLE_ALL_OFF]);
343+mocks.promptYesNo.mockResolvedValue(true);
344+mocks.provider.apply.mockImplementation(async (_ctx, selectedPlan: MigrationPlan) => ({
345+ ...selectedPlan,
346+summary: { ...selectedPlan.summary, planned: 0, migrated: 1 },
347+items: selectedPlan.items.map((item) =>
348+item.status === "planned" ? { ...item, status: "migrated" as const } : item,
349+),
350+}));
351+352+await migrateDefaultCommand(runtime, { provider: "codex" });
353+354+const appliedPlan = mocks.provider.apply.mock.calls[0]?.[1] as MigrationPlan;
355+expect(appliedPlan.summary).toMatchObject({ planned: 1, skipped: 2, conflicts: 0 });
356+expect(appliedPlan.items).toEqual(
357+expect.arrayContaining([
358+expect.objectContaining({
359+id: "skill:alpha",
360+status: "skipped",
361+reason: "not selected for migration",
362+}),
363+expect.objectContaining({
364+id: "skill:beta",
365+status: "skipped",
366+reason: "not selected for migration",
367+}),
368+expect.objectContaining({ id: "archive:config.toml", status: "planned" }),
369+]),
370+);
371+});
372+373+it("applies Toggle all on unless Toggle all off is also selected", async () => {
374+Object.defineProperty(process.stdin, "isTTY", {
375+configurable: true,
376+value: true,
377+});
378+const planned = codexSkillPlan();
379+mocks.provider.plan.mockResolvedValue(planned);
380+mocks.multiselect.mockResolvedValue([MIGRATION_SKILL_SELECTION_TOGGLE_ALL_ON]);
381+mocks.promptYesNo.mockResolvedValue(true);
382+mocks.provider.apply.mockImplementation(async (_ctx, selectedPlan: MigrationPlan) => ({
383+ ...selectedPlan,
384+summary: { ...selectedPlan.summary, planned: 0, migrated: 3 },
385+items: selectedPlan.items.map((item) =>
386+item.status === "planned" ? { ...item, status: "migrated" as const } : item,
387+),
388+}));
389+390+await migrateDefaultCommand(runtime, { provider: "codex" });
391+392+let appliedPlan = mocks.provider.apply.mock.calls[0]?.[1] as MigrationPlan;
393+expect(appliedPlan.summary).toMatchObject({ planned: 3, skipped: 0, conflicts: 0 });
394+395+mocks.provider.plan.mockResolvedValue(planned);
396+mocks.multiselect.mockResolvedValue([
397+MIGRATION_SKILL_SELECTION_TOGGLE_ALL_ON,
398+MIGRATION_SKILL_SELECTION_TOGGLE_ALL_OFF,
399+]);
400+mocks.promptYesNo.mockResolvedValue(true);
401+mocks.provider.apply.mockClear();
402+403+await migrateDefaultCommand(runtime, { provider: "codex" });
404+405+appliedPlan = mocks.provider.apply.mock.calls[0]?.[1] as MigrationPlan;
406+expect(appliedPlan.summary).toMatchObject({ planned: 1, skipped: 2, conflicts: 0 });
407+});
408+240409it("does not apply when interactive apply confirmation is declined", async () => {
241410Object.defineProperty(process.stdin, "isTTY", {
242411configurable: true,
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。