openclaw-multi-session-plugins/src/taskState.test.ts

400 lines
11 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";
import {
XWORKMATE_SESSION_EXTENSION_NAMESPACE,
getXWorkmateTaskSnapshot,
recordXWorkmateSessionMapping,
recordXWorkmateTaskRunStarted,
recordXWorkmateTaskRunTerminal,
} from "./taskState.js";
const XWORKMATE_PLUGIN_ID = "openclaw-multi-session-plugins";
function createApiFixture(tasks: Record<string, unknown> = {}, pluginConfig: Record<string, unknown> = {}) {
const sessions = new Map<string, any>();
const api = {
config: {},
pluginConfig,
logger: { warn: () => {} },
runtime: {
agent: {
session: {
getSessionEntry: ({ sessionKey }: { sessionKey: string }) => sessions.get(sessionKey),
listSessionEntries: () =>
[...sessions.entries()].map(([sessionKey, entry]) => ({
sessionKey,
entry,
})),
patchSessionEntry: async ({
sessionKey,
fallbackEntry,
update,
}: {
sessionKey: string;
fallbackEntry?: any;
update: (entry: any) => Partial<any> | null;
}) => {
const current = sessions.get(sessionKey) ?? fallbackEntry;
if (!current) {
return null;
}
const patch = update(current);
if (patch) {
sessions.set(sessionKey, { ...current, ...patch });
}
return sessions.get(sessionKey) ?? null;
},
},
},
tasks: {
runs: {
bindSession: ({ sessionKey }: { sessionKey: string }) => ({
resolve: (token: string) => tasks[`${sessionKey}:${token}`],
get: (token: string) => tasks[`${sessionKey}:${token}`],
findLatest: () => tasks[`${sessionKey}:latest`],
}),
},
},
},
};
return { api: api as any, sessions };
}
async function createWorkspaceFixture() {
return fs.mkdtemp(path.join(os.tmpdir(), "xworkmate-task-state-"));
}
describe("xworkmate task state mapping", () => {
it("requires typed appThreadKey metadata", async () => {
const { api } = createApiFixture();
await expect(
recordXWorkmateSessionMapping({
api,
params: {
schemaVersion: 1,
sessionKey: "draft:legacy",
expectedArtifactDirs: ["artifacts/"],
},
}),
).rejects.toThrow("appThreadKey required");
});
it("writes a durable pluginExtensions mapping without deriving the OpenClaw key", async () => {
const { api, sessions } = createApiFixture();
const mapping = await recordXWorkmateSessionMapping({
api,
params: {
schemaVersion: 1,
appThreadKey: "draft:1780658097668838-1",
openclawSessionKey: "draft:1780658097668838-1",
runId: "run-1",
expectedArtifactDirs: ["assets/images", "reports/"],
createdAt: "2026-06-05T00:00:00.000Z",
},
});
expect(mapping).toMatchObject({
schemaVersion: 1,
appThreadKey: "draft:1780658097668838-1",
openclawSessionKey: "draft:1780658097668838-1",
expectedArtifactDirs: ["assets/images/", "reports/"],
source: "bridge_prepare",
});
expect(
sessions.get("draft:1780658097668838-1").pluginExtensions[XWORKMATE_PLUGIN_ID][
XWORKMATE_SESSION_EXTENSION_NAMESPACE
],
).toMatchObject(mapping);
});
it("fails closed when an existing mapping points to a different app thread", async () => {
const { api } = createApiFixture();
await recordXWorkmateSessionMapping({
api,
params: {
schemaVersion: 1,
appThreadKey: "draft:first",
openclawSessionKey: "draft:first",
runId: "run-1",
},
});
await expect(
recordXWorkmateSessionMapping({
api,
params: {
schemaVersion: 1,
appThreadKey: "draft:second",
openclawSessionKey: "draft:first",
runId: "run-2",
},
}),
).rejects.toThrow("conflict");
});
it("resolves appThreadKey through pluginExtensions before querying native tasks", async () => {
const { api } = createApiFixture({
"draft:1780658097668838-1:run-1": {
taskId: "task-1",
runId: "run-1",
status: "succeeded",
},
});
await recordXWorkmateSessionMapping({
api,
params: {
schemaVersion: 1,
appThreadKey: "draft:1780658097668838-1",
openclawSessionKey: "draft:1780658097668838-1",
runId: "run-1",
expectedArtifactDirs: ["artifacts/"],
},
});
const result = await getXWorkmateTaskSnapshot({
api,
params: {
appThreadKey: "draft:1780658097668838-1",
runId: "run-1",
includeArtifacts: false,
},
});
expect(result).toMatchObject({
success: true,
status: "completed",
openclawSessionKey: "draft:1780658097668838-1",
expectedArtifactDirs: ["artifacts/"],
});
});
it("reports unknown evidence from task artifacts when native task record is unavailable", async () => {
const workspaceDir = await createWorkspaceFixture();
const appThreadKey = "draft:sample-task";
const openclawSessionKey = "agent:main:draft:sample-task";
const runId = "turn-sample";
const artifactDir = path.join(workspaceDir, "tasks", "agent_main_draft_sample-task", runId);
await fs.mkdir(artifactDir, { recursive: true });
await fs.writeFile(path.join(artifactDir, "series.config.json"), "{}\n", "utf8");
const { api } = createApiFixture({}, { workspaceDir });
await recordXWorkmateSessionMapping({
api,
params: {
schemaVersion: 1,
appThreadKey,
openclawSessionKey,
runId,
},
});
const result = await getXWorkmateTaskSnapshot({
api,
params: {
appThreadKey,
openclawSessionKey,
runId,
},
});
expect(result).toMatchObject({
success: false,
status: "unknown",
taskStatus: "unknown",
evidence: "artifacts_present",
openclawSessionKey,
runId,
task: {
source: "artifact_fallback",
status: "unknown",
},
artifacts: [
{
relativePath: "series.config.json",
contentType: "application/json",
},
],
artifactCount: 1,
});
expect((result.warnings as string[]).some((entry) => entry.includes("task status is unknown"))).toBe(true);
});
it("returns no_native_task_record when neither native task record nor task artifacts exist", async () => {
const workspaceDir = await createWorkspaceFixture();
const { api } = createApiFixture({}, { workspaceDir });
await recordXWorkmateSessionMapping({
api,
params: {
schemaVersion: 1,
appThreadKey: "draft:no-task",
openclawSessionKey: "draft:no-task",
runId: "run-1",
},
});
const result = await getXWorkmateTaskSnapshot({
api,
params: {
appThreadKey: "draft:no-task",
runId: "run-1",
},
});
expect(result).toMatchObject({
ok: false,
code: "no_native_task_record",
mapping: {
appThreadKey: "draft:no-task",
openclawSessionKey: "draft:no-task",
},
});
});
it("returns a durable failed agent terminal state when the native task record is absent", async () => {
const workspaceDir = await createWorkspaceFixture();
const { api } = createApiFixture({}, { workspaceDir });
await recordXWorkmateSessionMapping({
api,
params: {
appThreadKey: "draft:failed-run",
openclawSessionKey: "agent:main:draft:failed-run",
runId: "turn-failed",
},
});
await recordXWorkmateTaskRunStarted({
api,
openclawSessionKey: "agent:main:draft:failed-run",
runId: "turn-failed",
});
await recordXWorkmateTaskRunTerminal({
api,
openclawSessionKey: "agent:main:draft:failed-run",
runId: "turn-failed",
success: false,
error: "401 Authentication Fails, api_key=sk-secret-value",
});
await expect(
getXWorkmateTaskSnapshot({
api,
params: {
appThreadKey: "draft:failed-run",
runId: "turn-failed",
},
}),
).resolves.toMatchObject({
success: false,
status: "failed",
taskStatus: "failed",
terminal: true,
terminalSource: "agent_end",
task: {
runId: "turn-failed",
status: "failed",
source: "xworkmate_run_state",
},
error: "401 Authentication Fails, api_key=<redacted>",
});
});
it("returns a recorded running state while the agent turn is still active", async () => {
const { api } = createApiFixture();
await recordXWorkmateSessionMapping({
api,
params: {
appThreadKey: "draft:running-run",
openclawSessionKey: "agent:main:draft:running-run",
runId: "turn-running",
},
});
await recordXWorkmateTaskRunStarted({
api,
openclawSessionKey: "agent:main:draft:running-run",
runId: "turn-running",
});
await expect(
getXWorkmateTaskSnapshot({
api,
params: {
appThreadKey: "draft:running-run",
runId: "turn-running",
includeArtifacts: false,
},
}),
).resolves.toMatchObject({
success: true,
status: "running",
terminal: false,
terminalSource: "session_prepare",
task: {
runId: "turn-running",
status: "running",
source: "xworkmate_run_state",
},
});
});
it("does not accept legacy sessionKey as a task lookup alias", async () => {
const { api } = createApiFixture({
"draft:legacy:run-1": {
taskId: "task-legacy",
runId: "run-1",
status: "succeeded",
},
});
const result = await getXWorkmateTaskSnapshot({
api,
params: {
sessionKey: "draft:legacy",
runId: "run-1",
includeArtifacts: false,
},
});
expect(result).toMatchObject({
ok: false,
code: "invalid_lookup",
});
});
it("can read mapping by appThreadKey from pluginExtensions", async () => {
const { api } = createApiFixture({
"draft:lookup:run-1": {
taskId: "task-1",
runId: "run-1",
status: "succeeded",
},
});
await recordXWorkmateSessionMapping({
api,
params: {
schemaVersion: 1,
appThreadKey: "draft:lookup",
openclawSessionKey: "draft:lookup",
runId: "run-1",
},
});
await expect(
getXWorkmateTaskSnapshot({
api,
params: {
appThreadKey: "draft:lookup",
runId: "run-1",
includeArtifacts: false,
},
}),
).resolves.toMatchObject({
success: true,
appThreadKey: "draft:lookup",
openclawSessionKey: "draft:lookup",
});
});
});