import { describe, expect, it } from "vitest" ;
import type { SessionEntry } from "../config/sessions.js" ;
import {
clearAllCliSessions,
clearCliSession,
getCliSessionBinding,
hashCliSessionText,
resolveCliSessionReuse,
setCliSessionBinding,
} from "./cli-session.js" ;
describe("cli-session helpers" , () => {
it("persists binding metadata alongside legacy session ids" , () => {
const entry: SessionEntry = {
sessionId: "openclaw-session" ,
updatedAt: Date.now(),
};
setCliSessionBinding(entry, "claude-cli" , {
sessionId: "cli-session-1" ,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-hash" ,
mcpConfigHash: "mcp-hash" ,
mcpResumeHash: "mcp-resume-hash" ,
});
expect(entry.cliSessionIds?.["claude-cli" ]).toBe("cli-session-1" );
expect(entry.claudeCliSessionId).toBe("cli-session-1" );
expect(getCliSessionBinding(entry, "claude-cli" )).toEqual({
sessionId: "cli-session-1" ,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-hash" ,
mcpConfigHash: "mcp-hash" ,
mcpResumeHash: "mcp-resume-hash" ,
});
});
it("keeps legacy bindings reusable until richer metadata is persisted" , () => {
const entry: SessionEntry = {
sessionId: "openclaw-session" ,
updatedAt: Date.now(),
cliSessionIds: { "claude-cli" : "legacy-session" },
claudeCliSessionId: "legacy-session" ,
};
expect(
resolveCliSessionReuse({
binding: getCliSessionBinding(entry, "claude-cli" ),
authEpochVersion: 2 ,
}),
).toEqual({ sessionId: "legacy-session" });
});
it("invalidates legacy bindings when auth, prompt, or MCP state changes" , () => {
const entry: SessionEntry = {
sessionId: "openclaw-session" ,
updatedAt: Date.now(),
cliSessionIds: { "claude-cli" : "legacy-session" },
claudeCliSessionId: "legacy-session" ,
};
const binding = getCliSessionBinding(entry, "claude-cli" );
expect(
resolveCliSessionReuse({
binding,
authEpochVersion: 2 ,
authProfileId: "anthropic:work" ,
}),
).toEqual({ invalidatedReason: "auth-profile" });
expect(
resolveCliSessionReuse({
binding,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-hash" ,
}),
).toEqual({ invalidatedReason: "system-prompt" });
expect(
resolveCliSessionReuse({
binding,
authEpochVersion: 2 ,
mcpConfigHash: "mcp-hash" ,
}),
).toEqual({ invalidatedReason: "mcp" });
});
it("invalidates reuse when stored auth profile or prompt shape changes" , () => {
const binding = {
sessionId: "cli-session-1" ,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-a" ,
};
expect(
resolveCliSessionReuse({
binding,
authProfileId: "anthropic:personal" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-a" ,
}),
).toEqual({ invalidatedReason: "auth-profile" });
expect(
resolveCliSessionReuse({
binding,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-b" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-a" ,
}),
).toEqual({ invalidatedReason: "auth-epoch" });
expect(
resolveCliSessionReuse({
binding,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-b" ,
mcpConfigHash: "mcp-a" ,
}),
).toEqual({ invalidatedReason: "system-prompt" });
expect(
resolveCliSessionReuse({
binding,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-b" ,
}),
).toEqual({ invalidatedReason: "mcp" });
});
it("accepts unversioned auth epochs for binding upgrades" , () => {
const binding = {
sessionId: "cli-session-1" ,
authProfileId: "anthropic:work" ,
authEpoch: "previous-auth-epoch" ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-a" ,
};
expect(
resolveCliSessionReuse({
binding,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-a" ,
}),
).toEqual({ sessionId: "cli-session-1" });
});
it("accepts older auth epoch versions for binding upgrades" , () => {
const binding = {
sessionId: "cli-session-1" ,
authProfileId: "anthropic:work" ,
authEpoch: "refresh-token-auth-epoch" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-a" ,
};
expect(
resolveCliSessionReuse({
binding,
authProfileId: "anthropic:work" ,
authEpoch: "identity-auth-epoch" ,
authEpochVersion: 3 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-a" ,
}),
).toEqual({ sessionId: "cli-session-1" });
});
it("does not treat model changes as a session mismatch" , () => {
const binding = {
sessionId: "cli-session-1" ,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-a" ,
};
expect(
resolveCliSessionReuse({
binding,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-a" ,
}),
).toEqual({ sessionId: "cli-session-1" });
});
it("prefers the stable MCP resume hash over the raw MCP config hash" , () => {
const binding = {
sessionId: "cli-session-1" ,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-config-a" ,
mcpResumeHash: "mcp-resume-a" ,
};
expect(
resolveCliSessionReuse({
binding,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-config-b" ,
mcpResumeHash: "mcp-resume-a" ,
}),
).toEqual({ sessionId: "cli-session-1" });
expect(
resolveCliSessionReuse({
binding,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-config-a" ,
mcpResumeHash: "mcp-resume-b" ,
}),
).toEqual({ invalidatedReason: "mcp" });
});
it("falls back to legacy MCP config hashes when stored resume hashes are absent" , () => {
const binding = {
sessionId: "cli-session-1" ,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-config-a" ,
};
expect(
resolveCliSessionReuse({
binding,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-config-a" ,
mcpResumeHash: "mcp-resume-a" ,
}),
).toEqual({ sessionId: "cli-session-1" });
expect(
resolveCliSessionReuse({
binding,
authProfileId: "anthropic:work" ,
authEpoch: "auth-epoch-a" ,
authEpochVersion: 2 ,
extraSystemPromptHash: "prompt-a" ,
mcpConfigHash: "mcp-config-b" ,
mcpResumeHash: "mcp-resume-a" ,
}),
).toEqual({ invalidatedReason: "mcp" });
});
it("clears provider-scoped and global CLI session state" , () => {
const entry: SessionEntry = {
sessionId: "openclaw-session" ,
updatedAt: Date.now(),
};
setCliSessionBinding(entry, "claude-cli" , { sessionId: "claude-session" });
setCliSessionBinding(entry, "codex-cli" , { sessionId: "codex-session" });
clearCliSession(entry, "codex-cli" );
expect(getCliSessionBinding(entry, "codex-cli" )).toBeUndefined();
expect(getCliSessionBinding(entry, "claude-cli" )?.sessionId).toBe("claude-session" );
clearAllCliSessions(entry);
expect(entry.cliSessionBindings).toBeUndefined();
expect(entry.cliSessionIds).toBeUndefined();
expect(entry.claudeCliSessionId).toBeUndefined();
});
it("hashes trimmed extra system prompts consistently" , () => {
expect(hashCliSessionText(" keep this " )).toBe(hashCliSessionText("keep this" ));
expect(hashCliSessionText("" )).toBeUndefined();
});
});
Messung V0.5 in Prozent C=100 H=97 G=98
¤ Dauer der Verarbeitung: 0.11 Sekunden
(vorverarbeitet am 2026-05-26)
¤
*© Formatika GbR, Deutschland