mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-11-05 07:39:47 +00:00
feat(dsh): restructure the environment tab for scanability
The environment tab rendered as one flat column: five identical headings, every detected interpreter in one long list, and the whole .env form always expanded. That reads like prose and grows without bound on machines with dozens of conda environments. - Lead with a status hero (endpoint, service/Python/ReMe chips, retest and rescan) so the current state is readable at a glance. - Turn the four remaining groups into numbered cards with a status pill in the header, so setup order and per-step state are obvious. - Show only the interpreter in use, then reveal the rest on demand: search box, six rows per page, and a show-all toggle. - Collapse the .env editor behind a summary line (path + model). - Explain the "recommended" marker through a tooltip; the flag now also distinguishes an outdated reme-ai (amber) from a current one. - Expose versionOk on the detection probe so the client can tone the tag without importing host-only modules. - Add a react-test-renderer suite covering the hero, card count, collapsed list, pagination and interpreter selection.
This commit is contained in:
parent
80ce19645e
commit
0d9598e7c8
6 changed files with 1479 additions and 708 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -37,39 +37,86 @@ export const styles = `
|
|||
.reme-settings-tabs button.active{color:var(--dsw-alias-label-primary);font-weight:600}
|
||||
.reme-settings-tabs button.active:after{content:"";position:absolute;right:8px;bottom:-1px;left:8px;height:2px;border-radius:2px;background:var(--dsw-alias-label-primary)}
|
||||
.reme-settings-tabs button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
|
||||
.reme-env{display:flex;flex-direction:column}
|
||||
.reme-env-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:12px 0}
|
||||
.reme-env-row+.reme-settings-error{margin:0}
|
||||
.reme-env-label{flex:none;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500}
|
||||
.reme-env-endpoint{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px}
|
||||
.reme-env-notice{margin:12px 0 0}
|
||||
.reme-env-list{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2)}
|
||||
.reme-env-python{display:flex;align-items:center;gap:10px;padding:10px 0;border-bottom:1px solid var(--dsw-alias-border-l2);cursor:pointer}
|
||||
.reme-env-python input{margin:0;accent-color:var(--dsw-alias-brand-primary)}
|
||||
.reme-env-python-main{min-width:0;flex:1;display:flex;flex-direction:column;gap:2px}
|
||||
.reme-env-python-main strong{font-size:13px;font-weight:600;line-height:1.5}
|
||||
.reme-env-python-main small{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;overflow-wrap:anywhere}
|
||||
.reme-env-hint{display:flex;flex-direction:column;align-items:flex-start;gap:8px;padding:14px 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
||||
.reme-env-hint strong{color:var(--dsw-alias-label-primary)}
|
||||
.reme-env-upcoming{margin:0;padding:4px 0 12px}
|
||||
.reme-env .reme-settings-details{margin-top:8px}
|
||||
.reme-env-checks{display:flex;flex-direction:column;border-top:1px solid var(--dsw-alias-border-l2);margin-top:4px}
|
||||
.reme-env-check{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:9px 0;border-bottom:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);font-size:12px}
|
||||
.reme-env-check>span:first-child{flex:none;font-weight:500;color:var(--dsw-alias-label-primary)}
|
||||
.reme-env-check .reme-status-badge{max-width:75%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.reme-env-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding-top:12px;border-top:1px solid var(--dsw-alias-border-l2)}
|
||||
.reme-env-mirror{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);font-size:12px}
|
||||
.reme-env-mirror select{min-height:28px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);padding:2px 8px;font:inherit;font-size:12px}
|
||||
.reme-env{display:flex;flex-direction:column;gap:12px;padding:16px 0 8px}
|
||||
.reme-env-hero{position:relative;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:14px 18px;overflow:hidden;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,var(--dsw-alias-border-l2));border-radius:16px;background:linear-gradient(128deg,color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,var(--dsw-alias-bg-layer-3)),var(--dsw-alias-bg-layer-3) 58%);padding:17px 19px}
|
||||
.reme-env-hero:after{position:absolute;right:-110px;bottom:-170px;width:230px;height:230px;border-radius:50%;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 7%,transparent);content:"";pointer-events:none}
|
||||
.reme-env-hero-brand{display:flex;min-width:0;align-items:center;gap:12px}
|
||||
.reme-env-hero-mark{display:grid;width:40px;height:40px;flex:none;place-items:center;border-radius:12px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3);font-size:13px;font-weight:750;letter-spacing:-.4px;box-shadow:0 8px 22px color-mix(in srgb,var(--dsw-alias-label-primary) 12%,transparent)}
|
||||
.reme-env-hero-copy{min-width:0}
|
||||
.reme-env-eyebrow{display:block;color:var(--dsw-alias-label-tertiary);font-size:10px;font-weight:650;letter-spacing:.08em;line-height:1.4;text-transform:uppercase}
|
||||
.reme-env-hero-copy h3{margin:2px 0 0;font-size:17px;line-height:1.35}
|
||||
.reme-env-hero-sub{margin:3px 0 0;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.4;text-overflow:ellipsis;white-space:nowrap}
|
||||
.reme-env-hero-chips{position:relative;z-index:1;display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px}
|
||||
.reme-env-hero-actions{grid-column:1 / -1;display:flex;flex-wrap:wrap;align-items:center;gap:8px;border-top:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,var(--dsw-alias-border-l2));padding-top:12px}
|
||||
.reme-env-hero-meta{margin-left:auto;color:var(--dsw-alias-label-tertiary);font-size:11px}
|
||||
.reme-env-hero-error,.reme-env-notice{margin:0}
|
||||
.reme-env-card{border:1px solid var(--dsw-alias-border-l2);border-radius:13px;background:var(--dsw-alias-bg-layer-3)}
|
||||
.reme-env-card>header{display:flex;align-items:flex-start;gap:11px;padding:13px 15px}
|
||||
.reme-env-card.collapsed>header{padding-bottom:14px}
|
||||
.reme-env-step{display:grid;width:22px;height:22px;flex:none;place-items:center;margin-top:-1px;border-radius:50%;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 12%,transparent);color:var(--dsw-alias-brand-primary);font-size:11px;font-weight:700;font-variant-numeric:tabular-nums}
|
||||
.reme-env-card>header>div{min-width:0;flex:1}
|
||||
.reme-env-card>header h4{margin:0;font-size:13px;font-weight:600;line-height:1.5}
|
||||
.reme-env-card>header p{display:-webkit-box;margin:2px 0 0;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:1.5;-webkit-box-orient:vertical;-webkit-line-clamp:2}
|
||||
.reme-env-card>header p.reme-env-summary{display:block;text-overflow:ellipsis;white-space:nowrap}
|
||||
.reme-env-card>header aside{display:flex;flex:none;align-items:center;gap:8px}
|
||||
.reme-env-count{flex:none;border-radius:999px;background:var(--dsw-alias-bg-module-platform);padding:2px 9px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;white-space:nowrap}
|
||||
.reme-env-card-body{display:flex;flex-direction:column;gap:10px;border-top:1px solid var(--dsw-alias-border-l2);padding:12px 15px 14px}
|
||||
.reme-env-picked{display:flex;align-items:center;gap:11px;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 30%,var(--dsw-alias-border-l2));border-radius:11px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 5%,var(--dsw-alias-bg-layer-3));padding:10px 12px}
|
||||
.reme-env-picked-mark{width:16px;height:16px;flex:none;box-sizing:border-box;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 45%,transparent);border-radius:50%;background:var(--dsw-alias-brand-primary);background-clip:content-box;padding:4px}
|
||||
.reme-env-picked-main{display:flex;min-width:0;flex:1;flex-direction:column;gap:3px}
|
||||
.reme-env-title{display:flex;min-width:0;flex-wrap:wrap;align-items:center;gap:6px}
|
||||
.reme-env-title strong{font-size:13px;font-weight:600;line-height:1.5}
|
||||
.reme-env-tag{flex:none;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;padding:1px 7px;color:var(--dsw-alias-label-secondary);font-size:10.5px;line-height:15px;white-space:nowrap}
|
||||
.reme-env-tag.ok{border-color:color-mix(in srgb,#21a366 25%,transparent);background:color-mix(in srgb,#21a366 8%,transparent);color:#168653}
|
||||
.reme-env-tag.brand{border-color:color-mix(in srgb,var(--dsw-alias-brand-primary) 28%,transparent);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 8%,transparent);color:var(--dsw-alias-brand-primary)}
|
||||
.reme-env-tag.warn{border-color:color-mix(in srgb,#d99416 30%,transparent);background:color-mix(in srgb,#d99416 9%,transparent);color:color-mix(in srgb,#d99416 72%,var(--dsw-alias-label-primary))}
|
||||
.reme-env-tag.bad{border-color:color-mix(in srgb,var(--dsw-alias-label-error) 25%,transparent);background:color-mix(in srgb,var(--dsw-alias-label-error) 7%,transparent);color:var(--dsw-alias-label-error)}
|
||||
.reme-env-path{min-width:0;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;line-height:1.5;text-overflow:ellipsis;white-space:nowrap}
|
||||
.reme-env-note{margin:0}
|
||||
.reme-env-picker{display:flex;flex-direction:column;gap:9px}
|
||||
.reme-env-picker-bar{display:flex;flex-wrap:wrap;align-items:center;gap:8px}
|
||||
.reme-env-picker-bar .reme-settings-muted{margin:0}
|
||||
.reme-env-search{box-sizing:border-box;flex:1;min-width:180px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-3);padding:6px 10px;color:var(--dsw-alias-label-primary);font:inherit;font-size:12px;line-height:1.5}
|
||||
.reme-env-search:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:0}
|
||||
.reme-env-options{display:flex;flex-direction:column;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:11px;background:var(--dsw-alias-bg-layer-3)}
|
||||
.reme-env-option{display:flex;align-items:center;gap:8px;padding:8px 11px;cursor:pointer}
|
||||
.reme-env-option+.reme-env-option{border-top:1px solid var(--dsw-alias-border-l2)}
|
||||
.reme-env-option:hover{background:color-mix(in srgb,var(--dsw-alias-label-primary) 3%,transparent)}
|
||||
.reme-env-option input{flex:none;margin:0;accent-color:var(--dsw-alias-brand-primary)}
|
||||
.reme-env-option strong{flex:none;font-size:12px;font-weight:600;line-height:1.5;white-space:nowrap}
|
||||
.reme-env-option em{flex:none;color:var(--dsw-alias-label-tertiary);font-size:11px;font-style:normal;line-height:1.5;white-space:nowrap}
|
||||
.reme-env-option-path{flex:1;min-width:60px;overflow:hidden;text-align:right;color:var(--dsw-alias-label-tertiary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;line-height:1.5;text-overflow:ellipsis;white-space:nowrap}
|
||||
.reme-env-more{align-self:flex-start;color:var(--dsw-alias-brand-primary);font-size:12px}
|
||||
.reme-env-hint{display:flex;flex-direction:column;align-items:flex-start;gap:8px;border:1px dashed var(--dsw-alias-border-l2);border-radius:11px;padding:13px 14px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:1.6}
|
||||
.reme-env-hint strong{color:var(--dsw-alias-label-primary);font-size:13px}
|
||||
.reme-env-factrow{display:flex;align-items:center;gap:9px}
|
||||
.reme-env-factrow .reme-env-path{flex:1}
|
||||
.reme-env-field-label{flex:none;color:var(--dsw-alias-label-secondary);font-size:11.5px;line-height:1.5}
|
||||
.reme-env-metrics{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
|
||||
.reme-env-metric{min-width:0;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:color-mix(in srgb,var(--dsw-alias-bg-module-platform) 45%,var(--dsw-alias-bg-layer-3));padding:9px 11px}
|
||||
.reme-env-metric>span{display:block;color:var(--dsw-alias-label-tertiary);font-size:10.5px;line-height:1.4}
|
||||
.reme-env-metric>div{display:flex;min-width:0;flex-wrap:wrap;align-items:center;gap:5px;margin-top:6px}
|
||||
.reme-env-metric small{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:1.4}
|
||||
.reme-env-metric .reme-status-badge{max-width:100%;white-space:normal;align-items:flex-start;text-align:left}
|
||||
.reme-env-metric .reme-status-badge i{margin-top:5px}
|
||||
.reme-env-metric-path{min-width:0;overflow:hidden;color:var(--dsw-alias-label-primary);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10.5px;line-height:1.5;text-overflow:ellipsis;white-space:nowrap}
|
||||
.reme-env-cta{display:flex;flex-wrap:wrap;align-items:center;gap:8px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:11px}
|
||||
.reme-env-confirm{flex:1 0 100%;display:flex;flex-wrap:wrap;align-items:center;gap:8px;border:1px solid color-mix(in srgb,var(--dsw-alias-brand-primary) 25%,var(--dsw-alias-border-l2));border-radius:9px;background:color-mix(in srgb,var(--dsw-alias-brand-primary) 5%,transparent);padding:9px 12px}
|
||||
.reme-env-confirm p{flex:1 0 100%;margin:0;color:var(--dsw-alias-label-secondary);font-size:12px;line-height:1.5}
|
||||
.reme-env-hint-inline{flex:1 0 100%;margin:0}
|
||||
.reme-env-progress{padding-top:10px}
|
||||
.reme-env-log{max-height:220px;overflow:auto;margin:6px 0 0;border-radius:8px;background:var(--dsw-alias-bg-layer-3);padding:10px 12px;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word}
|
||||
.reme-env-result{padding-top:10px;border-top:1px solid var(--dsw-alias-border-l2)}
|
||||
.reme-env-inline{display:flex;gap:8px;align-items:center}
|
||||
.reme-env-mirror{display:flex;align-items:center;gap:6px;color:var(--dsw-alias-label-secondary);font-size:12px}
|
||||
.reme-env-mirror select{min-height:28px;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);padding:2px 8px;font:inherit;font-size:12px}
|
||||
.reme-env-autostart{margin-left:auto;border:0;padding:0;color:var(--dsw-alias-label-secondary);font-size:12px}
|
||||
.reme-env-progress{display:flex;flex-direction:column;gap:8px}
|
||||
.reme-env-log{max-height:220px;overflow:auto;margin:0;border-radius:8px;background:var(--dsw-alias-bg-module-platform);padding:10px 12px;font-size:11px;line-height:1.5;white-space:pre-wrap;word-break:break-word}
|
||||
.reme-env-result{display:flex;flex-direction:column;align-items:flex-start;gap:8px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:11px}
|
||||
.reme-env-result p{margin:0}
|
||||
.reme-env-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px;border-top:1px solid var(--dsw-alias-border-l2);padding-top:11px}
|
||||
.reme-env-inline{display:flex;align-items:center;gap:8px}
|
||||
.reme-env-inline input{flex:1;min-width:0}
|
||||
.reme-env-config{padding:4px 0 0}
|
||||
.reme-env-group{margin:14px 0 0;color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;letter-spacing:.02em}
|
||||
.reme-env-check-inline{padding:0;border:0}
|
||||
.reme-env-config{padding:2px 0 0}
|
||||
.reme-env-group{margin:12px 0 0;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:650;letter-spacing:.02em}
|
||||
.reme-env .reme-settings-details{margin:0}
|
||||
.reme-env .reme-settings-details pre{margin:8px 0 0}
|
||||
@media(max-width:820px){.reme-env-hero{grid-template-columns:1fr}.reme-env-hero-chips{justify-content:flex-start}.reme-env-metrics{grid-template-columns:1fr}.reme-env-option-path{display:none}.reme-env-autostart{margin-left:0}.reme-env-hero-meta{margin-left:0;width:100%}}
|
||||
@media(max-width:760px){.reme-settings-health,.reme-settings-component-grid,.reme-status-component-grid,.reme-status-capability-grid{grid-template-columns:1fr}.reme-settings-result{display:none}.reme-status-overview-hero{grid-template-columns:1fr}.reme-status-overview-actions{grid-column:1;grid-row:auto;flex-direction:row}.reme-status-overview-actions .reme-status-button{flex:1}.reme-status-file-browser{grid-template-columns:1fr}.reme-status-file-browser>aside{max-height:210px;border-right:0;border-bottom:1px solid var(--dsw-alias-border-l2)}.reme-status-file-browser>article{min-height:300px}.reme-status-task-list summary{grid-template-columns:1fr}}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export interface RemeIO {
|
|||
export interface RemeProbe {
|
||||
installed: boolean;
|
||||
version: string;
|
||||
/** True when the version satisfies {@link MINIMUM_REME}. */
|
||||
versionOk: boolean;
|
||||
}
|
||||
|
||||
export interface RemeCheck extends RemeProbe {
|
||||
|
|
@ -68,11 +70,15 @@ export async function probeReme(
|
|||
timeoutMs: 10_000,
|
||||
});
|
||||
if (result.error || result.code !== 0)
|
||||
return { installed: false, version: "" };
|
||||
return { installed: false, version: "", versionOk: false };
|
||||
const version = parseRemeVersion(result.stdout);
|
||||
return { installed: version !== "", version };
|
||||
return {
|
||||
installed: version !== "",
|
||||
version,
|
||||
versionOk: remeVersionAtLeast(version),
|
||||
};
|
||||
} catch {
|
||||
return { installed: false, version: "" };
|
||||
return { installed: false, version: "", versionOk: false };
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export interface PythonEnv {
|
|||
/** Human-readable discovery channels, e.g. "conda env list; PATH". */
|
||||
source: string;
|
||||
/** reme-ai installation facts, probed for the interpreter recommendation. */
|
||||
reme?: { installed: boolean; version: string };
|
||||
reme?: { installed: boolean; version: string; versionOk?: boolean };
|
||||
}
|
||||
|
||||
export interface PythonDetection {
|
||||
|
|
|
|||
|
|
@ -394,10 +394,26 @@ test("detects, classifies and suggests Windows interpreters end to end", async (
|
|||
assert.equal(store.origin.kind, "windows-store");
|
||||
assert.equal(store.ok, true);
|
||||
|
||||
assert.deepEqual(base.reme, { installed: true, version: "0.4.0.9" });
|
||||
assert.deepEqual(named.reme, { installed: true, version: "0.4.0.9" });
|
||||
assert.deepEqual(pythonOrg.reme, { installed: true, version: "0.4.1.11" });
|
||||
assert.deepEqual(store.reme, { installed: false, version: "" });
|
||||
assert.deepEqual(base.reme, {
|
||||
installed: true,
|
||||
version: "0.4.0.9",
|
||||
versionOk: true,
|
||||
});
|
||||
assert.deepEqual(named.reme, {
|
||||
installed: true,
|
||||
version: "0.4.0.9",
|
||||
versionOk: true,
|
||||
});
|
||||
assert.deepEqual(pythonOrg.reme, {
|
||||
installed: true,
|
||||
version: "0.4.1.11",
|
||||
versionOk: true,
|
||||
});
|
||||
assert.deepEqual(store.reme, {
|
||||
installed: false,
|
||||
version: "",
|
||||
versionOk: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("suggests nothing usable by preferring reme installs, then named conda envs", () => {
|
||||
|
|
|
|||
231
integrations/dsh/tests/env-tab.test.mjs
Normal file
231
integrations/dsh/tests/env-tab.test.mjs
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import React from "react";
|
||||
import { act, create } from "react-test-renderer";
|
||||
|
||||
import { ReMeEnvironmentTab, envZh } from "../dist/client/env-tab.js";
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
const CONDA = "D:\\ProgramFils\\Miniconda";
|
||||
const SUGGESTED = `${CONDA}\\envs\\reme-serve\\python.exe`;
|
||||
|
||||
function env(name, version, reme) {
|
||||
const root = name === "base" ? CONDA : `${CONDA}\\envs\\${name}`;
|
||||
return {
|
||||
exe: `${root}\\python.exe`,
|
||||
version,
|
||||
ok: true,
|
||||
origin: { kind: "conda", envName: name, root },
|
||||
source: "conda env list",
|
||||
...(reme ? { reme: { installed: true, version: reme } } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const ENVS = [
|
||||
env("base", "3.12.3", "0.4.0.9"),
|
||||
env("com_env", "3.12.9"),
|
||||
env("copaw", "3.12.0", "0.3.1.8"),
|
||||
env("drama", "3.12.13"),
|
||||
env("mochi", "3.12.13"),
|
||||
env("mochi_test", "3.12.13"),
|
||||
env("nl_agent", "3.12.0"),
|
||||
env("reme-serve", "3.12.13", "0.4.0.9"),
|
||||
];
|
||||
|
||||
const RESPONSES = {
|
||||
"remeEnv/python": {
|
||||
platform: "win32",
|
||||
scannedAt: new Date().toISOString(),
|
||||
envs: ENVS,
|
||||
suggested: SUGGESTED,
|
||||
condaRoot: CONDA,
|
||||
errors: [],
|
||||
},
|
||||
"remeEnv/checkReme": {
|
||||
installed: true,
|
||||
version: "0.4.0.9",
|
||||
versionOk: true,
|
||||
hasCore: true,
|
||||
cliPath: `${CONDA}\\envs\\reme-serve\\Scripts\\reme.exe`,
|
||||
},
|
||||
"remeEnv/installStatus": null,
|
||||
"remeEnv/readEnv": {
|
||||
exists: true,
|
||||
envPath: "C:\\Users\\test\\.reme\\.env",
|
||||
entries: [
|
||||
{ key: "LLM_BACKEND", value: "dashscope", secret: false, set: true },
|
||||
{
|
||||
key: "LLM_MODEL_NAME",
|
||||
value: "qwen3.7-plus",
|
||||
secret: false,
|
||||
set: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
"remeEnv/serviceStatus": {
|
||||
phase: "running",
|
||||
version: "0.4.0.9",
|
||||
managed: false,
|
||||
pid: 98820,
|
||||
endpoint: "http://127.0.0.1:2333",
|
||||
},
|
||||
};
|
||||
|
||||
const t = (key) => envZh[key];
|
||||
|
||||
function textOf(value) {
|
||||
if (Array.isArray(value)) return value.map(textOf).join("");
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function classesOf(node, found = new Map()) {
|
||||
if (!node || typeof node === "string") return found;
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach((child) => classesOf(child, found));
|
||||
return found;
|
||||
}
|
||||
for (const name of String(node.props.className ?? "").split(" ")) {
|
||||
if (name) found.set(name, (found.get(name) ?? 0) + 1);
|
||||
}
|
||||
(node.children ?? []).forEach((child) => classesOf(child, found));
|
||||
return found;
|
||||
}
|
||||
|
||||
function texts(node, found = []) {
|
||||
if (!node) return found;
|
||||
if (typeof node === "string") {
|
||||
found.push(node);
|
||||
return found;
|
||||
}
|
||||
if (Array.isArray(node)) {
|
||||
node.forEach((child) => texts(child, found));
|
||||
return found;
|
||||
}
|
||||
(node.children ?? []).forEach((child) => texts(child, found));
|
||||
return found;
|
||||
}
|
||||
|
||||
async function mount() {
|
||||
const calls = { mutate: [] };
|
||||
const snapshot = {
|
||||
status: "ready",
|
||||
writable: true,
|
||||
revision: 3,
|
||||
value: {
|
||||
endpoint: "http://127.0.0.1:2333",
|
||||
pythonPath: "",
|
||||
remeWorkspaceDir: "",
|
||||
autoStart: false,
|
||||
requestTimeoutMs: 10000,
|
||||
backgroundTimeoutMs: 3600000,
|
||||
},
|
||||
};
|
||||
const scope = {
|
||||
subscribe: () => () => undefined,
|
||||
getSnapshot: () => snapshot,
|
||||
mutate: async (ops, revision) => {
|
||||
calls.mutate.push({ ops, revision });
|
||||
return true;
|
||||
},
|
||||
};
|
||||
const rpc = {
|
||||
call: async (_path, method) => ({
|
||||
ok: true,
|
||||
value: RESPONSES[method] ?? null,
|
||||
}),
|
||||
};
|
||||
|
||||
let renderer;
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
React.createElement(ReMeEnvironmentTab, { scope, rpc, t }),
|
||||
);
|
||||
});
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 20));
|
||||
});
|
||||
return { renderer, calls };
|
||||
}
|
||||
|
||||
function buttonByText(renderer, label) {
|
||||
const found = renderer.root.findAll(
|
||||
(node) => node.type === "button" && textOf(node.props.children) === label,
|
||||
);
|
||||
assert.ok(found.length > 0, `button not found: ${label}`);
|
||||
return found[0];
|
||||
}
|
||||
|
||||
test("renders the status hero and the four setup steps", async () => {
|
||||
const { renderer } = await mount();
|
||||
const json = renderer.toJSON();
|
||||
const classes = classesOf(json);
|
||||
const copy = texts(json).join(" ");
|
||||
|
||||
assert.equal(classes.get("reme-env-hero"), 1);
|
||||
assert.equal(classes.get("reme-env-card"), 4);
|
||||
assert.equal(classes.get("reme-env-picked"), 1);
|
||||
assert.equal(classes.get("reme-env-metrics"), 1);
|
||||
assert.equal(classes.get("reme-env-autostart"), 1);
|
||||
|
||||
assert.match(copy, /ReMe 本地运行时/);
|
||||
assert.match(copy, /已就绪,记忆保存在本地/);
|
||||
assert.match(copy, /http:\/\/127\.0\.0\.1:2333/);
|
||||
assert.match(copy, /8 个环境/);
|
||||
assert.match(copy, /服务 · ReMe 0\.4\.0\.9/);
|
||||
assert.match(copy, /Python 3\.12\.13/);
|
||||
assert.match(copy, /reme-ai 0\.4\.0\.9/);
|
||||
assert.match(copy, /C:\\Users\\test\\\.reme\\\.env/);
|
||||
});
|
||||
|
||||
test("keeps the interpreter list collapsed until asked, then paginates", async () => {
|
||||
const { renderer } = await mount();
|
||||
assert.equal(classesOf(renderer.toJSON()).get("reme-env-option"), undefined);
|
||||
|
||||
await act(async () => {
|
||||
buttonByText(renderer, envZh.changeInterpreter).props.onClick();
|
||||
});
|
||||
const expanded = classesOf(renderer.toJSON());
|
||||
assert.equal(expanded.get("reme-env-option"), 6);
|
||||
assert.equal(expanded.get("reme-env-search"), 1);
|
||||
assert.equal(expanded.get("reme-env-more"), 1);
|
||||
|
||||
await act(async () => {
|
||||
renderer.root
|
||||
.findAll(
|
||||
(node) =>
|
||||
node.type === "button" && textOf(node.props.children).includes("7"),
|
||||
)[0]
|
||||
.props.onClick();
|
||||
});
|
||||
assert.equal(classesOf(renderer.toJSON()).get("reme-env-option"), 7);
|
||||
});
|
||||
|
||||
test("marks the suggested interpreter and explains why", async () => {
|
||||
const { renderer, calls } = await mount();
|
||||
const json = renderer.toJSON();
|
||||
const recommended = renderer.root.findAll(
|
||||
(node) =>
|
||||
String(node.props.className ?? "").includes("reme-env-tag brand") &&
|
||||
textOf(node.props.children) === envZh.recommended,
|
||||
);
|
||||
assert.ok(recommended.length >= 1);
|
||||
assert.equal(recommended[0].props.title, envZh.recommendWhyReme);
|
||||
assert.ok(texts(json).join(" ").includes(SUGGESTED));
|
||||
|
||||
await act(async () => {
|
||||
buttonByText(renderer, envZh.changeInterpreter).props.onClick();
|
||||
});
|
||||
const radios = renderer.root.findAll(
|
||||
(node) => node.type === "input" && node.props.type === "radio",
|
||||
);
|
||||
const target = radios.find((node) => node.props.disabled === false);
|
||||
assert.ok(target);
|
||||
await act(async () => {
|
||||
target.props.onChange();
|
||||
});
|
||||
assert.deepEqual(calls.mutate.at(-1)?.ops, [
|
||||
{ op: "set", path: ["pythonPath"], value: ENVS[0].exe },
|
||||
]);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue