diff --git a/integrations/dsh/src/client/env-tab.tsx b/integrations/dsh/src/client/env-tab.tsx index b9359924..18a3efe4 100644 --- a/integrations/dsh/src/client/env-tab.tsx +++ b/integrations/dsh/src/client/env-tab.tsx @@ -992,7 +992,8 @@ export function ReMeEnvironmentTab({ {detection.error ? `: ${detection.error}` : ""}

) : null} - {detected && detected.envs.length === 0 ? ( + {detection.phase === "error" || + (detected && detected.envs.length === 0) ? (
{t("emptyTitle")} {t("emptyHint")} diff --git a/integrations/dsh/src/env-gateway.ts b/integrations/dsh/src/env-gateway.ts index 30fd0def..562fc015 100644 --- a/integrations/dsh/src/env-gateway.ts +++ b/integrations/dsh/src/env-gateway.ts @@ -31,7 +31,11 @@ export class ReMeEnvGateway extends TypertRemoteService { static inject = ["remeEnv"]; constructor(ctx: Context) { - super(ctx, "remeEnv"); + // The service key must differ from the manager registered as `remeEnv`: + // TypertRemoteService registers itself under this key, and two providers + // for one key fail plugin activation. The wire namespace stays `remeEnv`, + // which is the endpoint the client calls. + super(ctx, "remeEnvGateway", { namespace: "remeEnv" }); } /** Detect local Python interpreters; cached briefly unless refreshed. */ diff --git a/integrations/dsh/tests/env-gateway.test.mjs b/integrations/dsh/tests/env-gateway.test.mjs new file mode 100644 index 00000000..ad01f1ca --- /dev/null +++ b/integrations/dsh/tests/env-gateway.test.mjs @@ -0,0 +1,92 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { Context } from "@deepseek-ai/cordis"; + +import { ReMeEnvGateway } from "../dist/env-gateway.js"; + +/** Manager stub: no subprocesses, just enough surface for the gateway. */ +function stubManager() { + return { + detectPython: async () => ({ + platform: "win32", + scannedAt: "", + envs: [], + suggested: "", + condaRoot: "", + errors: [], + }), + checkReme: async (pythonPath) => ({ + installed: false, + version: "", + versionOk: false, + hasCore: false, + cliPath: "", + error: pythonPath ? "" : "Python interpreter path must be absolute", + }), + }; +} + +/** Mount the gateway inside the same isolated realm the patch creates. */ +async function mounted() { + const root = new Context(); + let realm; + await root.plugin((ctx) => { + realm = ctx.isolate({ remeMemory: true }); + }); + await realm.plugin((ctx) => { + ctx.provide("remeMemory", {}); + ctx.provide("remeEnv", stubManager()); + }); + await realm.plugin(ReMeEnvGateway); + return { root, realm }; +} + +test("gateway mounts without a service-key conflict and keeps the wire namespace", async () => { + const { root } = await mounted(); + const receiver = root.reflect.get("remeEnvGateway", false); + assert.ok(receiver, "gateway service is registered under its own key"); + assert.ok(receiver.typertRemote, "gateway exposes a typertRemote binding"); + assert.equal(receiver.typertRemote.serviceKey, "remeEnvGateway"); + assert.equal(receiver.typertRemote.namespace, "remeEnv"); + // The manager must stay reachable under the key the methods inject. + assert.ok(root.reflect.get("remeEnv", false)); +}); + +test("SRC discovery exposes remeEnv/python with one plain input parameter", async () => { + const { root } = await mounted(); + let binding; + for (const [serviceKey, definition] of Object.entries(root.reflect.props)) { + if (definition.type !== "service") continue; + const receiver = root.reflect.get(serviceKey, false); + if (!receiver?.typertRemote) continue; + if (receiver.typertRemote.namespace !== "remeEnv") continue; + binding = { serviceKey, receiver }; + } + assert.ok(binding, "an active service exports the remeEnv namespace"); + assert.equal(binding.serviceKey, "remeEnvGateway"); + + const source = Function.prototype.toString.call( + Object.getPrototypeOf(binding.receiver).python, + ); + const open = source.indexOf("("); + const close = source.indexOf(")", open + 1); + const parameters = source + .slice(open + 1, close) + .split(",") + .map((part) => part.trim()) + .filter(Boolean); + // The Gateway parses this source, so defaults, destructuring, and rest + // parameters would make the endpoint call fail at runtime. + assert.deepEqual(parameters, ["input"]); + + const result = await binding.receiver.python({ refresh: true }); + assert.equal(result.platform, "win32"); +}); + +test("gateway methods remain callable through the manager after remount", async () => { + const { root } = await mounted(); + const receiver = root.reflect.get("remeEnvGateway", false); + const check = await receiver.checkReme({ pythonPath: "" }); + assert.equal(check.installed, false); + assert.match(check.error, /absolute/); +});