fix(run): keep temporary run configurations following the toolchain settings (#1167)

Temporary run configurations were kept with what settings derived when they were made (absolute mvn, -s / -Dmaven.repo.local, JAVA_HOME), and never went through the backend again, so after a Maven home / settings file / JDK change they and gutter test runs narrowed from them still ran the old ones. They are now kept without derived values (run_without_derived) and get the current settings applied when the run list loads (run_apply_settings). Maven's settings file and local repository apply to every Maven configuration unless it sets them itself. Earlier stored entries are dropped once.

Refs #822
Цей коміт міститься в:
l0ng-ai authored and GitHub committed 2026-11-04 11:56:03 +08:00
1 джерело 59567f1af0
коміт d3b9a39831
10 files changed
+269 -33

No files matched your search

+4
Переглянути файл
@@ -587,6 +587,10 @@ export const commands = {
runConfigs: () => typedError<RunConfig_Serialize[], IdeError>(__TAURI_INVOKE("run_configs")),
runDotnetResolve: (config: RunConfig_Deserialize, debug: boolean) => typedError<RunConfig_Serialize, IdeError>(__TAURI_INVOKE("run_dotnet_resolve", { config, debug })),
runSaveConfigs: (configs: RunConfig_Deserialize[], removedDetectedIds: string[] | null) => typedError<null, IdeError>(__TAURI_INVOKE("run_save_configs", { configs, removedDetectedIds })),
/** Temporary configurations as they are kept between sessions: without what settings derived. */
runWithoutDerived: (configs: RunConfig_Deserialize[]) => typedError<RunConfig_Serialize[], IdeError>(__TAURI_INVOKE("run_without_derived", { configs })),
/** Kept temporary configurations with the current settings applied. */
runApplySettings: (configs: RunConfig_Deserialize[]) => typedError<RunConfig_Serialize[], IdeError>(__TAURI_INVOKE("run_apply_settings", { configs })),
/** Configuration templates: per-type defaults applied to new configurations. */
runTemplates: () => typedError<RunConfig_Serialize[], IdeError>(__TAURI_INVOKE("run_templates")),
runSaveTemplates: (templates: RunConfig_Deserialize[]) => typedError<null, IdeError>(__TAURI_INVOKE("run_save_templates", { templates })),
+1 -1
Переглянути файл
@@ -391,7 +391,7 @@ describe("temporary configurations", () => {
expect(s.temporary).not.toContain(apps[0]!.id);
expect(s.selectedId).toBe(apps[apps.length - 1]!.id);
expect(s.configs.filter((c) => c.kind === "application")).toHaveLength(MAX_TEMPORARY);
expect(JSON.parse(localStorage.getItem("openidea.run.temporary:/p")!)).toHaveLength(MAX_TEMPORARY);
expect(JSON.parse(localStorage.getItem("openidea.run.temporary.v2:/p")!)).toHaveLength(MAX_TEMPORARY);
// Reusing an existing temporary moves it last instead of duplicating it.
adoptTemporary(apps[3]!);
expect(useRun.getState().temporary.at(-1)).toBe(apps[3]!.id);
+19 -6
Переглянути файл
@@ -198,7 +198,10 @@ export async function loadConfigs(): Promise<void> {
const saved = await call(commands.runConfigs());
if (useProject.getState().project?.root !== root) return;
// Temporary configs survive reloads (and restarts) until saved or evicted; a saved one wins.
const temps = readTemporary(root).filter((t) => !saved.some((c) => c.id === t.id));
// They are kept without what settings derived: today's toolchains apply to them, as to the others.
const kept = readTemporary(root).filter((t) => !saved.some((c) => c.id === t.id));
const temps = kept.length ? await call(commands.runApplySettings(kept)).catch(() => kept) : kept;
if (useProject.getState().project?.root !== root) return;
const configs = [...saved, ...temps];
const stored = useRun.getState().selectedId ?? readSelected(root);
const selectedId =
@@ -312,7 +315,8 @@ export function selectedConfig(): RunConfig | undefined {
// ---------------------------------------------------------------------------
// Temporary configurations (IDEA: created by gutter / context runs, at most 5)
const TEMPORARY_KEY = "openidea.run.temporary:";
// v2: kept without settings-derived values (earlier entries froze the toolchain they were made with).
const TEMPORARY_KEY = "openidea.run.temporary.v2:";
export const MAX_TEMPORARY = 5;
function readTemporary(root: string): RunConfig[] {
@@ -335,14 +339,23 @@ function writeTemporary(root: string, configs: RunConfig[]) {
}
}
let persisting = 0;
function persistTemporary() {
const root = useProject.getState().project?.root;
if (!root) return;
const { configs, temporary } = useRun.getState();
writeTemporary(
root,
temporary.map((id) => configs.find((c) => c.id === id)).filter((c): c is RunConfig => !!c),
);
const list = temporary.map((id) => configs.find((c) => c.id === id)).filter((c): c is RunConfig => !!c);
writeTemporary(root, list);
if (!list.length || !isTauri()) return;
// Then rewritten without what settings derived (the project's Maven, JDK, interpreter…), so a
// later settings change reaches them as it does detected and saved configurations.
const generation = ++persisting;
void call(commands.runWithoutDerived(list))
.then((kept) => {
if (generation === persisting && useProject.getState().project?.root === root) writeTemporary(root, kept);
})
.catch(() => {});
}
/** IDEA's `isConfigurationFromContext` for an Application: the same main class of the same module. */
+80
Переглянути файл
@@ -0,0 +1,80 @@
import { afterEach, beforeEach, expect, it, vi } from "vitest";
const ipc = vi.hoisted(() => ({
runConfigs: vi.fn(),
runWithoutDerived: vi.fn(),
runApplySettings: vi.fn(),
}));
vi.mock("@/platform/ipc", async (importOriginal) => ({
...(await importOriginal<typeof import("@/platform/ipc")>()),
isTauri: () => true,
commands: ipc,
}));
import type { RunConfig } from "@/platform/ipc";
import { useProject } from "@/platform/project";
import { adoptTemporary, loadConfigs, resetRuns, useRun } from "./store";
const KEY = "openidea.run.temporary.v2:/p";
const ok = <T>(data: T) => Promise.resolve({ status: "ok" as const, data });
/** A Maven test run made from the tool window, with the settings of the day baked in. */
const goal: RunConfig = {
id: "maven:goal:pom.xml:test",
name: "app [test]",
kind: "maven",
command: "/old/maven/bin/mvn",
args: ["-s", "/old/settings.xml", "test"],
cwd: null,
env: { JAVA_HOME: "/old/jdk" },
detected: false,
isTest: true,
debugAdapter: null,
};
let mavenHome = "/new/maven";
beforeEach(() => {
resetRuns();
localStorage.clear();
useProject.setState({ project: { root: "/p" } as never });
ipc.runConfigs.mockImplementation(() => ok([]));
// The backend drops what settings derived, and applies the current settings again.
ipc.runWithoutDerived.mockImplementation((configs: RunConfig[]) =>
ok(
configs.map((c) => ({
...c,
command: "mvn",
args: c.args.filter((a, i, all) => a !== "-s" && all[i - 1] !== "-s"),
env: {},
})),
),
);
ipc.runApplySettings.mockImplementation((configs: RunConfig[]) =>
ok(
configs.map((c) => ({
...c,
command: `${mavenHome}/bin/mvn`,
args: ["-s", `${mavenHome}/conf/settings.xml`, ...c.args],
env: { JAVA_HOME: "/new/jdk" },
})),
),
);
});
afterEach(() => useProject.setState({ project: null }));
it("keeps temporary configurations without settings-derived values and applies today's settings on load", async () => {
adoptTemporary(goal);
await vi.waitFor(() => expect(JSON.parse(localStorage.getItem(KEY)!)[0].command).toBe("mvn"));
const kept = JSON.parse(localStorage.getItem(KEY)!)[0] as RunConfig;
expect(kept.args).toEqual(["test"]);
expect(kept.env).toEqual({});
// Settings › Maven changed: the reload after it brings the temporary configuration along.
mavenHome = "/moved/maven";
await loadConfigs();
const loaded = useRun.getState().configs.find((c) => c.id === goal.id)!;
expect(useRun.getState().temporary).toEqual([goal.id]);
expect(loaded.command).toBe("/moved/maven/bin/mvn");
expect(loaded.args).toEqual(["-s", "/moved/maven/conf/settings.xml", "test"]);
expect(loaded.env).toEqual({ JAVA_HOME: "/new/jdk" });
});