Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ export class RestoreAllUseCase {
if (manifest === null) throw new NoManifestError();

const effectiveFiles = interactive ? await this.promptForFiles(projectRoot) : undefined;
if (effectiveFiles !== undefined && effectiveFiles.length === 0) {
return {
totalRestored: 0,
totalKept: 0,
pluginNamesRestored: [],
errors,
unrestorable: [],
nativeOnlyToolIds: [],
};
}
const version = this.resolveVersion(manifest);
const restoreResult = await this.runConfigRestore(
projectRoot,
Expand Down Expand Up @@ -73,12 +83,12 @@ export class RestoreAllUseCase {
.filter((d) => d.status === "modified" || d.status === "deleted")
.map((d) => d.relativePath)
);
if (driftedFiles.length === 0) return [];
if (driftedFiles.length === 0) return undefined;
const selected = await this.prompter.checkbox(
"Select files to restore:",
driftedFiles.map((f) => ({ name: f, value: f }))
);
return selected.length === 0 ? [] : selected;
return selected;
}

private async runConfigRestore(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,34 @@ describe("RestoreAllUseCase β€” the --force flag", () => {
});
});

describe("RestoreAllUseCase β€” an interactive run that ticks nothing", () => {
it("restores nothing: an empty selection is a decision, not the absence of one", async () => {
const deps = await buildUnitDeps(PROJECT_ROOT);
await initAndInstall(deps, PROJECT_ROOT, "claude");
const manifest = await deps.manifestRepo.load();
const tracked = manifest?.getToolFiles("claude") ?? [];
const trackedPath = join(PROJECT_ROOT, tracked[0].relativePath);
await deps.fs.writeFile(trackedPath, "EDITED OUTSIDE THE CLI");
const prompter = new ScriptedPrompter([ScriptedPrompter.answer.checkbox([])]);

const result = await makeRestoreAllUseCase(
deps,
new PluginDistributionReaderAdapter(deps.fs),
prompter
).execute(PROJECT_ROOT, false, true);

expect(deps.fs.getFile(trackedPath)).toBe("EDITED OUTSIDE THE CLI");
expect(result).toStrictEqual({
totalRestored: 0,
totalKept: 0,
pluginNamesRestored: [],
errors: [],
unrestorable: [],
nativeOnlyToolIds: [],
});
});
});

describe("RestoreAllUseCase β€” plugin materialization", () => {
it("restores a corrupted plugin file with exactly one materialization call (translate-mode: claude)", async () => {
const deps = await buildUnitDeps(PROJECT_ROOT);
Expand Down Expand Up @@ -552,7 +580,7 @@ describe("RestoreAllUseCase β€” the interactive file picker", () => {
expect(asked.map((o) => o.files)).toStrictEqual([[KEYBINDINGS]]);
});

it("forwards an empty selection as no file at all", async () => {
it("never delegates when the user ticked nothing: an empty selection is a decision", async () => {
const deps = await vscodeProject();
await deps.fs.writeFile(join(PROJECT_ROOT, KEYBINDINGS), "[]");
const { asked, delegate } = recordingDelegate();
Expand All @@ -563,17 +591,17 @@ describe("RestoreAllUseCase β€” the interactive file picker", () => {
true
);

expect(asked.map((o) => o.files)).toStrictEqual([[]]);
expect(asked).toStrictEqual([]);
});

it("asks nothing and selects nothing when no tracked entry drifted", async () => {
it("asks nothing and delegates with no selection when no tracked entry drifted", async () => {
const deps = await vscodeProject();
const prompter = new CheckboxRecordingPrompter([KEYBINDINGS]);
const { asked, delegate } = recordingDelegate();

await restoreAllDelegatingTo(deps, prompter, delegate).execute(PROJECT_ROOT, false, true);

expect(prompter.asks).toStrictEqual([]);
expect(asked.map((o) => o.files)).toStrictEqual([[]]);
expect(asked.map((o) => o.files)).toStrictEqual([undefined]);
});
});