mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-05 07:39:47 +00:00
fix(dsh): mount the environment gateway under its own service key
TypertRemoteService registers itself under the constructor key, so `super(ctx, remeEnv)` collided with the already-provided `remeEnv` manager: the gateway plugin failed activation (its async rejection was logged only), leaving every /api/remeEnv/* call with HTTP 404. Register the service as `remeEnvGateway` and keep the wire namespace `remeEnv`, which is what the client calls. Add a real-cordis regression test that mounts the gateway inside the same isolated realm and asserts the binding, the namespace, and the SRC-parsed `input` parameter. Also show the interpreter hint on detection failure so an empty list never leaves the tab without guidance.
This commit is contained in:
parent
5d3b4fe31f
commit
80ce19645e
3 changed files with 99 additions and 2 deletions
|
|
@ -992,7 +992,8 @@ export function ReMeEnvironmentTab({
|
|||
{detection.error ? `: ${detection.error}` : ""}
|
||||
</p>
|
||||
) : null}
|
||||
{detected && detected.envs.length === 0 ? (
|
||||
{detection.phase === "error" ||
|
||||
(detected && detected.envs.length === 0) ? (
|
||||
<div className="reme-env-hint">
|
||||
<strong>{t("emptyTitle")}</strong>
|
||||
<span>{t("emptyHint")}</span>
|
||||
|
|
|
|||
|
|
@ -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. */
|
||||
|
|
|
|||
92
integrations/dsh/tests/env-gateway.test.mjs
Normal file
92
integrations/dsh/tests/env-gateway.test.mjs
Normal file
|
|
@ -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/);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue