ReMe/integrations/dsh/tests/runtime.test.mjs
jinliyl 6cb81e7921
refactor(integrations): split TypeScript host plugins (#536)
* refactor(integrations): split TypeScript host plugins

* feat(integrations): refresh host compatibility and status UI

* fix(openclaw): secure status diagnostics
2026-09-11 13:06:02 +08:00

376 lines
10 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import { ReMeRuntime } from "../dist/runtime.js";
const CONFIG = {
autoMemoryEnabled: true,
autoMemoryInterval: 2,
autoDreamEnabled: false,
shutdownTimeoutMs: 50,
dreamIntervalMs: 0,
dreamCron: "0 23 * * *",
dreamHint: "",
timezone: "Asia/Shanghai",
};
test("submits completed turns to auto-memory in background batches", async () => {
const calls = [];
const client = {
async autoMemory(messages, sessionId) {
calls.push({ messages, sessionId });
return { ok: true };
},
};
const runtime = new ReMeRuntime(client, CONFIG, silentLogger());
const session = { id: "session-one" };
completeTurn(runtime, session, 1, 10);
await runtime.stateFor(session).writes;
assert.equal(calls.length, 0);
completeTurn(runtime, session, 2, 20);
await runtime.stateFor(session).writes;
assert.equal(calls.length, 1);
assert.equal(calls[0].messages.length, 4);
assert.match(calls[0].sessionId, /^dsh-[a-f0-9]{24}$/);
const status = runtime.snapshot();
assert.equal(status.autoMemory.queuedTurns, 0);
assert.equal(status.autoMemory.recentTasks[0].phase, "completed");
assert.equal(status.autoMemory.recentTasks[0].turns, 2);
assert.equal(status.autoMemory.recentTasks[0].messages, 4);
});
test("requeues failed auto-memory batches and flushes them on disposal", async () => {
let attempts = 0;
const client = {
async autoMemory() {
attempts += 1;
return { ok: attempts > 1, error: "offline" };
},
};
const runtime = new ReMeRuntime(
client,
{ ...CONFIG, autoMemoryInterval: 1 },
silentLogger(),
);
const session = { id: "retry-session" };
completeTurn(runtime, session, 1, 10);
await runtime.stateFor(session).writes;
assert.equal(runtime.stateFor(session).pendingTurns.length, 1);
await runtime.dispose(session);
assert.equal(attempts, 2);
assert.equal(runtime.states.has(session.id), false);
});
test("retries an in-flight failed batch before disposal completes", async () => {
let attempts = 0;
let markStarted;
let releaseFirst;
const started = new Promise((resolve) => {
markStarted = resolve;
});
const firstRequest = new Promise((resolve) => {
releaseFirst = resolve;
});
const client = {
async autoMemory() {
attempts += 1;
if (attempts === 1) {
markStarted();
await firstRequest;
return { ok: false, error: "offline" };
}
return { ok: true };
},
};
const runtime = new ReMeRuntime(
client,
{ ...CONFIG, autoMemoryInterval: 1 },
silentLogger(),
);
const session = { id: "in-flight-retry-session" };
completeTurn(runtime, session, 1, 10);
await started;
const disposal = runtime.dispose(session);
releaseFirst();
await disposal;
assert.equal(attempts, 2);
assert.equal(runtime.states.has(session.id), false);
});
test("splits auto-memory batches at workspace date boundaries", async () => {
const calls = [];
const client = {
async autoMemory(messages, _sessionId, options) {
calls.push({ messages, options });
return { ok: true };
},
};
const runtime = new ReMeRuntime(
client,
{ ...CONFIG, autoMemoryInterval: 5 },
silentLogger(),
);
const session = { id: "midnight-session" };
completeTurn(runtime, session, 1, 10, Date.parse("2026-08-19T15:59:00Z"));
completeTurn(runtime, session, 2, 20, Date.parse("2026-08-19T16:01:00Z"));
await runtime.stateFor(session).writes;
assert.equal(calls.length, 1);
assert.equal(calls[0].options.date, "2026-08-19");
await runtime.dispose(session);
assert.equal(calls.length, 2);
assert.equal(calls[1].options.date, "2026-08-20");
});
test("retries a failed partial prior-day batch after later activity", async () => {
const calls = [];
const client = {
async autoMemory(messages, _sessionId, options) {
calls.push({ messages, options });
return { ok: calls.length > 1, error: "offline" };
},
};
const runtime = new ReMeRuntime(
client,
{ ...CONFIG, autoMemoryInterval: 5 },
silentLogger(),
);
const session = { id: "midnight-retry-session" };
completeTurn(runtime, session, 1, 10, Date.parse("2026-08-19T15:59:00Z"));
completeTurn(runtime, session, 2, 20, Date.parse("2026-08-19T16:01:00Z"));
await runtime.stateFor(session).writes;
assert.equal(calls.length, 1);
assert.equal(runtime.stateFor(session).pendingTurns.length, 2);
for (let turn = 3; turn <= 7; turn += 1) {
completeTurn(
runtime,
session,
turn,
turn * 10,
Date.parse(`2026-08-20T00:0${turn}:00Z`),
);
}
await runtime.stateFor(session).writes;
assert.equal(calls.length, 3);
assert.equal(calls[1].options.date, "2026-08-19");
assert.equal(calls[1].messages.length, 2);
assert.equal(calls[2].options.date, "2026-08-20");
assert.equal(calls[2].messages.length, 10);
assert.equal(runtime.stateFor(session).pendingTurns.length, 1);
});
test("bounds session disposal and aborts an unresponsive write", async () => {
let observedSignal;
const client = {
async autoMemory(_messages, _sessionId, options) {
observedSignal = options.signal;
return new Promise(() => {});
},
};
const runtime = new ReMeRuntime(
client,
{
...CONFIG,
autoMemoryInterval: 1,
shutdownTimeoutMs: 20,
},
silentLogger(),
);
const session = { id: "stuck-session" };
completeTurn(runtime, session, 1, 10);
const started = Date.now();
await runtime.dispose(session);
assert.equal(observedSignal.aborted, true);
assert.ok(Date.now() - started < 500);
assert.equal(runtime.states.has(session.id), true);
});
test("retains a failed final batch for a later plugin-shutdown retry", async () => {
let attempts = 0;
const client = {
async autoMemory() {
attempts += 1;
return { ok: attempts > 2, error: "offline" };
},
};
const runtime = new ReMeRuntime(
client,
{ ...CONFIG, autoMemoryInterval: 1 },
silentLogger(),
);
const session = { id: "retained-session" };
completeTurn(runtime, session, 1, 10);
await runtime.stateFor(session).writes;
await runtime.dispose(session);
assert.equal(attempts, 2);
assert.equal(runtime.states.has(session.id), true);
await runtime.disposeAll();
assert.equal(attempts, 3);
assert.equal(runtime.states.has(session.id), false);
});
test("runs only one auto-dream task at a time", async () => {
let calls = 0;
let release;
const client = {
async autoDream() {
calls += 1;
await new Promise((resolve) => {
release = resolve;
});
return { ok: true };
},
};
const runtime = new ReMeRuntime(client, CONFIG, silentLogger());
const first = runtime.runDream();
const second = runtime.runDream();
assert.equal(calls, 1);
release();
await Promise.all([first, second]);
assert.equal(calls, 1);
});
test("contains unexpected auto-dream client failures", async () => {
const warnings = [];
const runtime = new ReMeRuntime(
{
async autoDream() {
throw new Error("broken transport");
},
},
CONFIG,
{
debug() {},
warn(event, data) {
warnings.push({ event, data });
},
log() {},
},
);
await runtime.runDream();
assert.equal(warnings.length, 1);
assert.match(warnings[0].data.error, /broken transport/);
assert.equal(runtime.snapshot().autoDream.lastResult, "failed");
assert.match(runtime.snapshot().autoDream.lastError, /broken transport/);
});
test("applies changed batching and dream settings without replacing the runtime", async () => {
const calls = [];
let config = { ...CONFIG, autoMemoryInterval: 5, autoDreamEnabled: false };
const runtime = new ReMeRuntime(
{
async autoMemory(messages) {
calls.push(messages);
return { ok: true };
},
async autoDream() {
return { ok: true };
},
},
() => config,
silentLogger(),
);
const session = { id: "reconfigured-session" };
runtime.start();
completeTurn(runtime, session, 1, 10);
assert.equal(calls.length, 0);
config = {
...config,
autoMemoryInterval: 1,
autoDreamEnabled: true,
dreamIntervalMs: 100000,
};
runtime.reconfigure();
await runtime.stateFor(session).writes;
assert.equal(calls.length, 1);
assert.notEqual(runtime.dreamTimer, null);
assert.equal(runtime.snapshot().autoDream.enabled, true);
assert.ok(runtime.snapshot().autoDream.nextRunAt);
await runtime.disposeAll();
});
test("keeps one auto-dream schedule when reconfigured during a run", async () => {
let calls = 0;
let releaseFirst;
const firstRequest = new Promise((resolve) => {
releaseFirst = resolve;
});
let config = {
...CONFIG,
autoDreamEnabled: true,
dreamIntervalMs: 20,
shutdownTimeoutMs: 100,
};
const runtime = new ReMeRuntime(
{
async autoDream() {
calls += 1;
if (calls === 1) await firstRequest;
return { ok: true };
},
},
() => config,
silentLogger(),
);
runtime.start();
await delay(25);
config = { ...config };
runtime.reconfigure();
releaseFirst();
await delay(55);
assert.ok(calls >= 2 && calls <= 3, `expected one schedule, got ${calls}`);
await runtime.disposeAll();
});
function completeTurn(runtime, session, turn, seq, time) {
runtime.capture(session, { type: "turn/start", data: { turn } });
runtime.capture(session, {
type: "user/message",
seq,
time,
data: {
role: "user",
content: [{ type: "text", text: `question ${turn}` }],
source: { kind: "user" },
},
});
runtime.capture(session, {
type: "assistant/message",
seq: seq + 1,
time: time === undefined ? undefined : time + 1000,
data: {
message: {
role: "assistant",
content: [{ type: "text", text: `answer ${turn}` }],
source: { kind: "model" },
},
},
});
runtime.capture(session, {
type: "turn/end",
data: { turn, reason: { kind: "completed" } },
});
}
function silentLogger() {
return { debug() {}, warn() {}, log() {} };
}
function delay(milliseconds) {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}