Файли
openidea/src/features/remote-dev/environment.test.ts
T
l0ng-ai 1f9be73174 feat(remote): Reload the environment and apply devcontainer settings on the remote server (#183)
The remote server read the login shell's PATH once and cached it, had no project-level environment, and changed environment variables only took effect after reinstalling or a manual restart.

- Every openidea-server start re-reads the remote login shell's full environment (`$SHELL -ilc env -0`), with the cached PATH only as a fallback when the shell doesn't answer.
- A project's `.devcontainer/devcontainer.json` (or `.devcontainer.json`) applies on remote hosts: `remoteEnv` (with `${localEnv|containerEnv|env:X[:default]}`, workspace variables and `null` to unset) reaches every child process through one environment overlay (terminals, run configurations, language servers, debuggers, build tools, git), and `postStartCommand` (string, array or parallel object) runs once per server start in the project root as a cancellable background task; a failure raises a notification with the exit code and the last output lines without blocking the project.
- Reload Environment (`remote.reloadEnvironment`, File menu and Find Action) re-resolves the login shell and `remoteEnv` without restarting the server, starting from the process's original environment so prepended PATH entries don't pile up; it lists the changed variables, notes that running processes keep their environment, and offers to restart language servers.
- The JSONC reader moves to `ide_core::jsonc` and is shared with ide-run.

Claude-Session: https://claude.ai/code/session_01Dq6jQQn8ZhRfkkyj3PRutA
2026-10-29 09:28:18 +08:00

67 рядки
3.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import type { EnvReloaded, IdeError } from "@/bindings";
import type { NotificationService } from "@/workbench/contributions";
type Outcome = { status: "ok"; data: EnvReloaded } | { status: "error"; error: IdeError };
let outcome: Outcome;
const envReload = vi.fn(async () => outcome);
const lspServers = vi.fn(async () => ({ status: "ok" as const, data: [{ id: 1 }, { id: 2 }] }));
const lspRestart = vi.fn(async (_id: number) => ({ status: "ok" as const, data: null }));
vi.mock("@/platform/ipc", async (orig) => {
const actual = await orig<typeof import("@/platform/ipc")>();
return { ...actual, commands: { ...actual.commands, envReload, lspServers, lspRestart } };
});
const { reloadEnvironment, reloadSummary } = await import("./environment");
const { provideNotificationService } = await import("@/workbench/contributions");
type Note = Parameters<NotificationService["notify"]>[0];
let notes: Note[];
beforeEach(() => {
notes = [];
provideNotificationService({ notify: (n) => void notes.push(n) });
vi.clearAllMocks();
});
const reloaded = (changed: string[]): EnvReloaded => ({ changed, devcontainer: null, shellRead: true });
describe("Reload Environment", () => {
it("says what changed and that running processes keep their environment", () => {
expect(reloadSummary(reloaded([]))).toBe(
"No variables changed. Terminals and processes already running keep their old environment; new ones get the new one.",
);
expect(reloadSummary(reloaded(["JAVA_HOME", "PATH"]))).toMatch(/^Changed: JAVA_HOME, PATH\. Terminals/);
const many = Array.from({ length: 10 }, (_, i) => `V${i}`);
expect(reloadSummary(reloaded(many))).toMatch(/^Changed: V0, V1, V2, V3, V4, V5, V6, V7 and 2 more\./);
});
it("offers to restart the language servers when something changed", async () => {
outcome = { status: "ok", data: reloaded(["PATH"]) };
await reloadEnvironment();
expect(envReload).toHaveBeenCalledOnce();
expect(notes).toHaveLength(1);
expect(notes[0]).toMatchObject({ level: "info", title: "Environment reloaded" });
const restart = notes[0]!.actions!.find((a) => a.label === "Restart Language Servers")!;
restart.run();
await vi.waitFor(() => expect(lspRestart).toHaveBeenCalledTimes(2));
expect(lspRestart.mock.calls.map((c) => c[0])).toEqual([1, 2]);
});
it("has nothing to restart when nothing changed", async () => {
outcome = { status: "ok", data: reloaded([]) };
await reloadEnvironment();
expect(notes[0]!.actions).toEqual([]);
});
it("reports a failure as an error", async () => {
outcome = {
status: "error",
error: { kind: "unavailable", message: "could not read the environment of the login shell (/bin/zsh)" },
} as Outcome;
await reloadEnvironment();
expect(notes[0]).toMatchObject({ level: "error", title: "Could not reload the environment" });
expect(String(notes[0]!.body)).toContain("login shell");
});
});