mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-13 08:55:14 +00:00
- read the workspace .env as a masked projection: non-secret values are editable, API keys return only •••• plus their last four characters - merge edits into the existing file, preserving comments, ordering and unknown variables; validate the effective file so an untouched real key still satisfies the LLM requirement - require a non-empty EMBEDDING_API_KEY (EMPTY placeholder fills in on first save), because ReMe 0.4.0.x aborts startup without one - test the configured LLM endpoint from inside the workspace with the selected interpreter, classifying 401/404/network failures - add the remeWorkspaceDir setting (empty means ~/.reme) consumed by the .env editor and later service management Real-machine check: masked read leaks no secret, temp-workspace write is idempotent and rejects an empty embedding key, and the LLM probe against the local ~/.reme/.env returned HTTP 200.
174 lines
5.4 KiB
JavaScript
174 lines
5.4 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import {
|
|
envSummary,
|
|
maskSecret,
|
|
mergeEnvFile,
|
|
parseEnvFile,
|
|
parseLlmTest,
|
|
resolveWorkspaceDir,
|
|
testLlm,
|
|
validateEnvUpdate,
|
|
} from "../dist/pyenv/workspace.js";
|
|
|
|
test("resolves the workspace directory with ~/.reme defaults", () => {
|
|
const home = process.platform === "win32" ? "C:\\Users\\u" : "/home/u";
|
|
assert.equal(resolveWorkspaceDir("", home), join(home, ".reme"));
|
|
assert.equal(resolveWorkspaceDir("~/.reme", home), join(home, ".reme"));
|
|
assert.equal(resolveWorkspaceDir("~", home), home);
|
|
assert.throws(() => resolveWorkspaceDir("relative/dir", home));
|
|
});
|
|
|
|
test("parses KEY=VALUE lines tolerating comments, blanks and quotes", () => {
|
|
const parsed = parseEnvFile(
|
|
[
|
|
"# service credentials",
|
|
"",
|
|
"LLM_BACKEND=openai",
|
|
'LLM_MODEL_NAME="qwen3.7-plus"',
|
|
"LLM_API_KEY='sk-test'",
|
|
"UNKNOWN_KEY=kept",
|
|
"BROKEN LINE",
|
|
].join("\n"),
|
|
);
|
|
assert.deepEqual(parsed, {
|
|
LLM_BACKEND: "openai",
|
|
LLM_MODEL_NAME: "qwen3.7-plus",
|
|
LLM_API_KEY: "sk-test",
|
|
UNKNOWN_KEY: "kept",
|
|
});
|
|
});
|
|
|
|
test("merges updates while preserving comments, order and unknown keys", () => {
|
|
const existing = [
|
|
"# managed by hand",
|
|
"LLM_BACKEND=openai",
|
|
"LLM_API_KEY=old-key",
|
|
"",
|
|
"EMBEDDING_API_KEY=EMPTY",
|
|
"# keep me",
|
|
].join("\n");
|
|
const merged = mergeEnvFile(existing, {
|
|
LLM_API_KEY: "new-key",
|
|
LLM_BASE_URL: "https://example.com/v1",
|
|
});
|
|
assert.match(merged, /^# managed by hand$/m);
|
|
assert.match(merged, /^LLM_API_KEY=new-key$/m);
|
|
assert.doesNotMatch(merged, /old-key/);
|
|
assert.match(merged, /^EMBEDDING_API_KEY=EMPTY$/m);
|
|
assert.match(merged, /^# keep me$/m);
|
|
assert.match(merged, /^LLM_BASE_URL=https:\/\/example\.com\/v1$/m);
|
|
assert.ok(merged.endsWith("\n"));
|
|
// Idempotent updates do not duplicate keys.
|
|
assert.equal(
|
|
(
|
|
mergeEnvFile(merged, { LLM_API_KEY: "new-key" }).match(
|
|
/^LLM_API_KEY=/gm,
|
|
) ?? []
|
|
).length,
|
|
1,
|
|
);
|
|
});
|
|
|
|
test("masks secrets down to the last four characters", () => {
|
|
assert.equal(maskSecret("sk-abcdefgh"), "••••efgh");
|
|
assert.equal(maskSecret("abc"), "••••");
|
|
assert.equal(maskSecret(""), "");
|
|
});
|
|
|
|
test("summaries expose masked secrets and plain non-secret values", (t) => {
|
|
const directory = mkdtempSync(join(tmpdir(), "reme-env-"));
|
|
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
const path = join(directory, ".env");
|
|
writeFileSync(
|
|
path,
|
|
"LLM_MODEL_NAME=qwen3.7-plus\nLLM_API_KEY=sk-super-secret\n",
|
|
);
|
|
const summary = envSummary(path, directory);
|
|
assert.equal(summary.exists, true);
|
|
assert.equal(summary.envPath, path);
|
|
const model = summary.entries.find((entry) => entry.key === "LLM_MODEL_NAME");
|
|
assert.equal(model.value, "qwen3.7-plus");
|
|
assert.equal(model.secret, false);
|
|
const key = summary.entries.find((entry) => entry.key === "LLM_API_KEY");
|
|
assert.equal(key.set, true);
|
|
assert.equal(key.secret, true);
|
|
assert.equal(key.value, "");
|
|
assert.equal(key.masked, "••••cret");
|
|
assert.doesNotMatch(JSON.stringify(summary), /super-secret/);
|
|
});
|
|
|
|
test("validates the embedding key that aborts ReMe 0.4.0.x startup", () => {
|
|
assert.throws(() =>
|
|
validateEnvUpdate({ LLM_API_KEY: "sk", EMBEDDING_API_KEY: "" }),
|
|
);
|
|
assert.throws(() =>
|
|
validateEnvUpdate({ LLM_API_KEY: "", EMBEDDING_API_KEY: "EMPTY" }),
|
|
);
|
|
validateEnvUpdate({ LLM_API_KEY: "sk", EMBEDDING_API_KEY: "EMPTY" });
|
|
validateEnvUpdate({ LLM_API_KEY: "sk", EMBEDDING_API_KEY: "real-key" });
|
|
});
|
|
|
|
test("parses LLM test results from noisy python output", () => {
|
|
assert.deepEqual(
|
|
parseLlmTest(
|
|
'warn\n{"ok": true, "status": 200, "latencyMs": 321, "error": ""}\n',
|
|
),
|
|
{ ok: true, status: 200, latencyMs: 321, error: "" },
|
|
);
|
|
assert.deepEqual(parseLlmTest("no json"), {
|
|
ok: false,
|
|
status: 0,
|
|
latencyMs: 0,
|
|
error: "LLM test produced no result",
|
|
});
|
|
});
|
|
|
|
test("runs the LLM probe inside the workspace and classifies failures", async (t) => {
|
|
const directory = mkdtempSync(join(tmpdir(), "reme-llm-"));
|
|
t.after(() => rmSync(directory, { recursive: true, force: true }));
|
|
const calls = [];
|
|
const io = {
|
|
run: async (exe, args, options) => {
|
|
calls.push({ exe, args, options });
|
|
const stdout =
|
|
calls.length === 1
|
|
? '{"ok": true, "status": 200, "latencyMs": 120, "error": ""}\n'
|
|
: '{"ok": false, "status": 401, "latencyMs": 40, "error": "HTTP 401"}\n';
|
|
return { code: 0, stdout, stderr: "" };
|
|
},
|
|
};
|
|
const ok = await testLlm(io, {
|
|
workspaceDir: directory,
|
|
pythonPath: "C:\\env\\python.exe",
|
|
});
|
|
assert.equal(ok.ok, true);
|
|
assert.equal(ok.latencyMs, 120);
|
|
assert.equal(calls[0].options.cwd, directory);
|
|
assert.equal(calls[0].exe, "C:\\env\\python.exe");
|
|
|
|
const denied = await testLlm(io, {
|
|
workspaceDir: directory,
|
|
pythonPath: "C:\\env\\python.exe",
|
|
});
|
|
assert.equal(denied.ok, false);
|
|
assert.match(denied.error, /LLM_API_KEY/);
|
|
|
|
const broken = await testLlm(
|
|
{
|
|
run: async () => ({
|
|
code: null,
|
|
stdout: "",
|
|
stderr: "",
|
|
error: "spawn ENOENT",
|
|
}),
|
|
},
|
|
{ workspaceDir: directory, pythonPath: "C:\\missing\\python.exe" },
|
|
);
|
|
assert.equal(broken.ok, false);
|
|
assert.match(broken.error, /ENOENT/);
|
|
});
|