mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-01 08:53:36 +00:00
347 lines
9.5 KiB
TypeScript
347 lines
9.5 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import * as jsonFiles from "../infra/json-files.js";
|
|
import {
|
|
cleanupSessionLifecycleArtifacts,
|
|
getSessionEntry,
|
|
listSessionEntries,
|
|
patchSessionEntry,
|
|
readSessionUpdatedAt,
|
|
saveSessionStore,
|
|
updateSessionStore,
|
|
updateSessionStoreEntry,
|
|
upsertSessionEntry,
|
|
} from "./session-store-runtime.js";
|
|
|
|
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
|
|
describe("session-store-runtime compatibility surface", () => {
|
|
let tempDir: string;
|
|
let storePath: string;
|
|
|
|
beforeEach(() => {
|
|
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sdk-session-store-"));
|
|
storePath = path.join(tempDir, "sessions.json");
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("keeps the public session read shape while using accessor-backed exports", async () => {
|
|
const sessionKey = "agent:main:main";
|
|
await upsertSessionEntry({
|
|
sessionKey,
|
|
storePath,
|
|
entry: {
|
|
model: "gpt-5.5",
|
|
sessionId: "session-1",
|
|
updatedAt: 10,
|
|
},
|
|
});
|
|
|
|
expect(getSessionEntry({ sessionKey, storePath })).toMatchObject({
|
|
model: "gpt-5.5",
|
|
sessionId: "session-1",
|
|
updatedAt: 10,
|
|
});
|
|
expect(readSessionUpdatedAt({ sessionKey, storePath })).toEqual(expect.any(Number));
|
|
expect(listSessionEntries({ storePath })).toEqual([
|
|
{
|
|
sessionKey,
|
|
entry: expect.objectContaining({
|
|
model: "gpt-5.5",
|
|
sessionId: "session-1",
|
|
updatedAt: 10,
|
|
}),
|
|
},
|
|
]);
|
|
|
|
await upsertSessionEntry({
|
|
sessionKey,
|
|
storePath,
|
|
entry: {
|
|
sessionId: "session-1",
|
|
updatedAt: 20,
|
|
},
|
|
});
|
|
expect(getSessionEntry({ sessionKey, storePath })?.model).toBeUndefined();
|
|
});
|
|
|
|
it("keeps the public entry mutation signature while delegating to the seam", async () => {
|
|
const sessionKey = "agent:main:main";
|
|
|
|
await expect(
|
|
updateSessionStoreEntry({
|
|
sessionKey,
|
|
storePath,
|
|
update: () => ({ model: "gpt-5.5" }),
|
|
}),
|
|
).resolves.toBeNull();
|
|
|
|
await upsertSessionEntry({
|
|
sessionKey,
|
|
storePath,
|
|
entry: {
|
|
sessionId: "session-1",
|
|
updatedAt: 10,
|
|
},
|
|
});
|
|
|
|
const beforePatch = getSessionEntry({ sessionKey, storePath });
|
|
await expect(
|
|
patchSessionEntry({
|
|
sessionKey,
|
|
storePath,
|
|
preserveActivity: true,
|
|
update: (_entry, context) => ({
|
|
providerOverride: context.existingEntry ? "openai" : "missing",
|
|
updatedAt: 20,
|
|
}),
|
|
}),
|
|
).resolves.toMatchObject({
|
|
providerOverride: "openai",
|
|
sessionId: "session-1",
|
|
updatedAt: beforePatch?.updatedAt,
|
|
});
|
|
|
|
await expect(
|
|
updateSessionStoreEntry({
|
|
sessionKey,
|
|
storePath,
|
|
update: () => ({ model: "gpt-5.5" }),
|
|
}),
|
|
).resolves.toMatchObject({
|
|
model: "gpt-5.5",
|
|
providerOverride: "openai",
|
|
sessionId: "session-1",
|
|
});
|
|
});
|
|
|
|
it("preserves resolved maintenance settings through entry patches", async () => {
|
|
const staleSessionKey = "agent:main:stale";
|
|
const activeSessionKey = "agent:main:active";
|
|
await saveSessionStore(
|
|
storePath,
|
|
{
|
|
[staleSessionKey]: {
|
|
sessionId: "session-stale",
|
|
updatedAt: 10,
|
|
},
|
|
[activeSessionKey]: {
|
|
sessionId: "session-active",
|
|
updatedAt: 20,
|
|
},
|
|
},
|
|
{ skipMaintenance: true },
|
|
);
|
|
|
|
await expect(
|
|
patchSessionEntry({
|
|
sessionKey: activeSessionKey,
|
|
storePath,
|
|
maintenanceConfig: {
|
|
mode: "enforce",
|
|
pruneAfterMs: 7 * DAY_MS,
|
|
modelRunPruneAfterMs: DAY_MS,
|
|
maxEntries: 1,
|
|
resetArchiveRetentionMs: 7 * DAY_MS,
|
|
maxDiskBytes: null,
|
|
highWaterBytes: null,
|
|
},
|
|
update: () => ({ model: "gpt-5.5" }),
|
|
}),
|
|
).resolves.toMatchObject({
|
|
model: "gpt-5.5",
|
|
sessionId: "session-active",
|
|
});
|
|
|
|
expect(getSessionEntry({ sessionKey: activeSessionKey, storePath })).toMatchObject({
|
|
model: "gpt-5.5",
|
|
sessionId: "session-active",
|
|
});
|
|
expect(getSessionEntry({ sessionKey: staleSessionKey, storePath })).toBeUndefined();
|
|
});
|
|
|
|
it("accepts pre-model-run maintenance configs through entry patches", async () => {
|
|
const staleModelRunKey = "agent:main:explicit:model-run-123e4567-e89b-12d3-a456-426614174000";
|
|
const activeSessionKey = "agent:main:active";
|
|
const now = Date.now();
|
|
await saveSessionStore(
|
|
storePath,
|
|
{
|
|
[staleModelRunKey]: {
|
|
sessionId: "session-probe",
|
|
updatedAt: now - 2 * DAY_MS,
|
|
},
|
|
[activeSessionKey]: {
|
|
sessionId: "session-active",
|
|
updatedAt: now,
|
|
},
|
|
},
|
|
{ skipMaintenance: true },
|
|
);
|
|
|
|
const legacyMaintenanceConfig = {
|
|
mode: "enforce" as const,
|
|
pruneAfterMs: 7 * DAY_MS,
|
|
maxEntries: 500,
|
|
resetArchiveRetentionMs: 7 * DAY_MS,
|
|
maxDiskBytes: null,
|
|
highWaterBytes: null,
|
|
};
|
|
|
|
await expect(
|
|
patchSessionEntry({
|
|
sessionKey: activeSessionKey,
|
|
storePath,
|
|
maintenanceConfig: legacyMaintenanceConfig,
|
|
update: () => ({ model: "gpt-5.5" }),
|
|
}),
|
|
).resolves.toMatchObject({
|
|
model: "gpt-5.5",
|
|
sessionId: "session-active",
|
|
});
|
|
|
|
expect(getSessionEntry({ sessionKey: staleModelRunKey, storePath })).toMatchObject({
|
|
sessionId: "session-probe",
|
|
});
|
|
});
|
|
|
|
it("keeps deprecated whole-store mutations grouped as one compatibility operation", async () => {
|
|
const firstSessionKey = "agent:main:first";
|
|
const secondSessionKey = "agent:main:second";
|
|
const deletedSessionKey = "agent:main:deleted";
|
|
await saveSessionStore(
|
|
storePath,
|
|
{
|
|
[firstSessionKey]: {
|
|
sessionId: "session-1",
|
|
updatedAt: 10,
|
|
},
|
|
[secondSessionKey]: {
|
|
sessionId: "session-2",
|
|
updatedAt: 10,
|
|
},
|
|
[deletedSessionKey]: {
|
|
sessionId: "session-3",
|
|
updatedAt: 10,
|
|
},
|
|
},
|
|
{ skipMaintenance: true },
|
|
);
|
|
|
|
await expect(
|
|
updateSessionStore(
|
|
storePath,
|
|
(store) => {
|
|
const first = store[firstSessionKey];
|
|
const second = store[secondSessionKey];
|
|
if (!first || !second) {
|
|
throw new Error("seed session entries missing");
|
|
}
|
|
store[firstSessionKey] = {
|
|
...first,
|
|
model: "gpt-5.5",
|
|
updatedAt: 20,
|
|
};
|
|
store[secondSessionKey] = {
|
|
...second,
|
|
providerOverride: "openai",
|
|
updatedAt: 30,
|
|
};
|
|
delete store[deletedSessionKey];
|
|
return "whole-store-updated";
|
|
},
|
|
{ skipMaintenance: true },
|
|
),
|
|
).resolves.toBe("whole-store-updated");
|
|
|
|
expect(getSessionEntry({ sessionKey: firstSessionKey, storePath })).toMatchObject({
|
|
model: "gpt-5.5",
|
|
sessionId: "session-1",
|
|
updatedAt: 20,
|
|
});
|
|
expect(getSessionEntry({ sessionKey: secondSessionKey, storePath })).toMatchObject({
|
|
providerOverride: "openai",
|
|
sessionId: "session-2",
|
|
updatedAt: 30,
|
|
});
|
|
expect(getSessionEntry({ sessionKey: deletedSessionKey, storePath })).toBeUndefined();
|
|
});
|
|
|
|
it("preserves requireWriteSuccess for critical session entry updates", async () => {
|
|
const sessionKey = "agent:main:main";
|
|
await upsertSessionEntry({
|
|
sessionKey,
|
|
storePath,
|
|
entry: {
|
|
sessionId: "session-1",
|
|
updatedAt: 10,
|
|
},
|
|
});
|
|
const writeError = Object.assign(new Error("write failed"), { code: "ENOENT" });
|
|
const writeSpy = vi.spyOn(jsonFiles, "writeTextAtomic").mockRejectedValue(writeError);
|
|
|
|
try {
|
|
await expect(
|
|
updateSessionStoreEntry({
|
|
sessionKey,
|
|
storePath,
|
|
requireWriteSuccess: true,
|
|
update: () => ({ model: "gpt-5.5" }),
|
|
}),
|
|
).rejects.toBe(writeError);
|
|
} finally {
|
|
writeSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
it("cleans lifecycle artifacts through the accessor-backed SDK wrapper", async () => {
|
|
const sessionKey = "agent:main:lifecycle-owned-old";
|
|
const transcriptPath = path.join(tempDir, "lifecycle-owned-old.jsonl");
|
|
await saveSessionStore(
|
|
storePath,
|
|
{
|
|
[sessionKey]: {
|
|
sessionFile: transcriptPath,
|
|
sessionId: "lifecycle-owned-old",
|
|
updatedAt: 10,
|
|
},
|
|
"agent:main:regular": {
|
|
sessionId: "regular",
|
|
updatedAt: 20,
|
|
},
|
|
},
|
|
{ skipMaintenance: true },
|
|
);
|
|
fs.writeFileSync(transcriptPath, '{"runId":"lifecycle-owned-old"}\n', "utf-8");
|
|
const oldDate = new Date(Date.now() - 600_000);
|
|
fs.utimesSync(transcriptPath, oldDate, oldDate);
|
|
|
|
await expect(
|
|
cleanupSessionLifecycleArtifacts({
|
|
storePath,
|
|
sessionKeySegmentPrefix: "lifecycle-owned-",
|
|
transcriptContentMarker: '"runId":"lifecycle-owned-',
|
|
orphanTranscriptMinAgeMs: 300_000,
|
|
}),
|
|
).resolves.toEqual({
|
|
archivedTranscriptArtifacts: 1,
|
|
removedEntries: 1,
|
|
});
|
|
|
|
expect(getSessionEntry({ sessionKey, storePath })).toBeUndefined();
|
|
expect(getSessionEntry({ sessionKey: "agent:main:regular", storePath })).toMatchObject({
|
|
sessionId: "regular",
|
|
});
|
|
expect(
|
|
fs
|
|
.readdirSync(tempDir)
|
|
.filter((file) => file.startsWith("lifecycle-owned-old.jsonl.deleted.")),
|
|
).toHaveLength(1);
|
|
});
|
|
});
|