refactor: share sessions patch test helpers

This commit is contained in:
Vincent Koc
2026-06-01 15:17:55 +02:00
parent cbc5f277bb
commit c2aaf8afec

View File

@@ -9,6 +9,11 @@ import { applySessionsPatchToStore } from "./sessions-patch.js";
const SUBAGENT_MODEL = "synthetic/hf:moonshotai/Kimi-K2.5";
const KIMI_SUBAGENT_KEY = "agent:kimi:subagent:child";
const MAIN_SESSION_KEY = "agent:main:main";
const ANTHROPIC_SONNET_MODEL = "anthropic/claude-sonnet-4-6";
const ANTHROPIC_SONNET_ID = "claude-sonnet-4-6";
const ANTHROPIC_OPUS_ID = "claude-opus-4-6";
const OPENAI_GPT_MODEL = "openai/gpt-5.4";
const OPENAI_GPT_ID = "gpt-5.4";
const EMPTY_CFG = {} as OpenClawConfig;
type ApplySessionsPatchArgs = Parameters<typeof applySessionsPatchToStore>[0];
@@ -52,6 +57,100 @@ function expectPatchError(
expect(result.error.message).toContain(message);
}
function mainStoreEntry(overrides: Partial<SessionEntry>): Record<string, SessionEntry> {
return {
[MAIN_SESSION_KEY]: {
sessionId: "sess",
updatedAt: 1,
...overrides,
} as SessionEntry,
};
}
function mainAuthOverrideStore(overrides: Partial<SessionEntry>): Record<string, SessionEntry> {
return mainStoreEntry({
providerOverride: "anthropic",
modelOverride: ANTHROPIC_OPUS_ID,
authProfileOverrideSource: "user",
...overrides,
});
}
function catalogEntry(ref: string, name?: string) {
const separator = ref.indexOf("/");
if (separator < 0) {
throw new Error(`model ref must include provider: ${ref}`);
}
const id = ref.slice(separator + 1);
return {
provider: ref.slice(0, separator),
id,
name: name ?? id,
};
}
function loadCatalog(...refs: string[]): ApplySessionsPatchArgs["loadGatewayModelCatalog"] {
return async () => refs.map((ref) => catalogEntry(ref));
}
function expectModelSelection(
entry: SessionEntry,
providerOverride: string | undefined,
modelOverride: string | undefined,
) {
expect(entry.providerOverride).toBe(providerOverride);
expect(entry.modelOverride).toBe(modelOverride);
}
async function applyMainModelPatch(params: {
store?: Record<string, SessionEntry>;
cfg?: OpenClawConfig;
model: string | null;
catalogRefs?: string[];
}) {
return expectPatchOk(
await runPatch({
store: params.store,
cfg: params.cfg,
patch: { key: MAIN_SESSION_KEY, model: params.model },
loadGatewayModelCatalog:
params.catalogRefs === undefined ? undefined : loadCatalog(...params.catalogRefs),
}),
);
}
async function expectProviderChangeClearsAuthOverride(store: Record<string, SessionEntry>) {
const entry = await applyMainModelPatch({
store,
model: OPENAI_GPT_MODEL,
catalogRefs: [OPENAI_GPT_MODEL],
});
expectModelSelection(entry, "openai", OPENAI_GPT_ID);
expectAuthOverride(entry, { profile: undefined });
}
function expectAuthOverride(
entry: SessionEntry,
expected: {
profile: string | undefined;
source?: string;
compactionCount?: number;
},
) {
expect(entry.authProfileOverride).toBe(expected.profile);
if (expected.profile === undefined) {
expect(entry.authProfileOverrideSource).toBeUndefined();
expect(entry.authProfileOverrideCompactionCount).toBeUndefined();
return;
}
expect(entry.authProfileOverrideSource).toBe(expected.source ?? "user");
if (expected.compactionCount === undefined) {
expect(entry.authProfileOverrideCompactionCount).toBeUndefined();
} else {
expect(entry.authProfileOverrideCompactionCount).toBe(expected.compactionCount);
}
}
async function applySubagentModelPatch(cfg: OpenClawConfig) {
return expectPatchOk(
await runPatch({
@@ -62,7 +161,7 @@ async function applySubagentModelPatch(cfg: OpenClawConfig) {
model: SUBAGENT_MODEL,
},
loadGatewayModelCatalog: async () => [
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "sonnet" },
{ provider: "anthropic", id: ANTHROPIC_SONNET_ID, name: "sonnet" },
{ provider: "synthetic", id: "hf:moonshotai/Kimi-K2.5", name: "kimi" },
],
}),
@@ -100,9 +199,9 @@ function createAllowlistedAnthropicModelCfg(): OpenClawConfig {
return {
agents: {
defaults: {
model: { primary: "openai/gpt-5.4" },
model: { primary: OPENAI_GPT_MODEL },
models: {
"anthropic/claude-sonnet-4-6": { alias: "sonnet" },
[ANTHROPIC_SONNET_MODEL]: { alias: "sonnet" },
},
},
},
@@ -184,7 +283,7 @@ describe("gateway sessions patch", () => {
sessionFile: "stale.jsonl",
label: "Stale Session",
sendPolicy: "deny",
modelOverride: "gpt-5.4",
modelOverride: OPENAI_GPT_ID,
responseUsage: "tokens",
parentSessionKey: "agent:main:main",
} as SessionEntry,
@@ -202,16 +301,14 @@ describe("gateway sessions patch", () => {
expect(entry.sessionFile).toBeUndefined();
expect(entry.label).toBeUndefined();
expect(entry.sendPolicy).toBe("deny");
expect(entry.modelOverride).toBe("gpt-5.4");
expect(entry.modelOverride).toBe(OPENAI_GPT_ID);
expect(entry.responseUsage).toBe("tokens");
expect(entry.parentSessionKey).toBe("agent:main:main");
expect(entry.fastMode).toBe(true);
});
test("clears fastMode when patch sets null", async () => {
const store: Record<string, SessionEntry> = {
[MAIN_SESSION_KEY]: { fastMode: true } as SessionEntry,
};
const store = mainStoreEntry({ fastMode: true });
const entry = expectPatchOk(
await runPatch({
store,
@@ -276,223 +373,125 @@ describe("gateway sessions patch", () => {
});
test("preserves same-provider auth overrides when model patch changes", async () => {
const store: Record<string, SessionEntry> = {
"agent:main:main": {
sessionId: "sess",
updatedAt: 1,
providerOverride: "anthropic",
modelOverride: "claude-opus-4-6",
authProfileOverride: "anthropic:default",
authProfileOverrideSource: "user",
authProfileOverrideCompactionCount: 3,
} as SessionEntry,
};
const entry = expectPatchOk(
await runPatch({
store,
patch: { key: MAIN_SESSION_KEY, model: "anthropic/claude-sonnet-4-6" },
loadGatewayModelCatalog: async () => [
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "claude-sonnet-4-6" },
],
}),
);
expect(entry.providerOverride).toBe("anthropic");
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
expect(entry.authProfileOverride).toBe("anthropic:default");
expect(entry.authProfileOverrideSource).toBe("user");
expect(entry.authProfileOverrideCompactionCount).toBe(3);
const store = mainAuthOverrideStore({
authProfileOverride: "anthropic:default",
authProfileOverrideCompactionCount: 3,
});
const entry = await applyMainModelPatch({
store,
model: ANTHROPIC_SONNET_MODEL,
catalogRefs: [ANTHROPIC_SONNET_MODEL],
});
expectModelSelection(entry, "anthropic", ANTHROPIC_SONNET_ID);
expectAuthOverride(entry, { profile: "anthropic:default", compactionCount: 3 });
});
test("preserves auth overrides for provider-auth aliases when model patch changes", async () => {
const store: Record<string, SessionEntry> = {
"agent:main:main": {
sessionId: "sess-alias",
updatedAt: 1,
providerOverride: "byteplus",
modelOverride: "seedance-1-0-lite-t2v-250428",
authProfileOverride: "byteplus:work",
authProfileOverrideSource: "user",
authProfileOverrideCompactionCount: 2,
} as SessionEntry,
};
const entry = expectPatchOk(
await runPatch({
store,
patch: { key: MAIN_SESSION_KEY, model: "byteplus-plan/ark-code-latest" },
loadGatewayModelCatalog: async () => [
{ provider: "byteplus-plan", id: "ark-code-latest", name: "ark-code-latest" },
],
}),
);
expect(entry.providerOverride).toBe("byteplus-plan");
expect(entry.modelOverride).toBe("ark-code-latest");
expect(entry.authProfileOverride).toBe("byteplus:work");
expect(entry.authProfileOverrideSource).toBe("user");
expect(entry.authProfileOverrideCompactionCount).toBe(2);
const store = mainStoreEntry({
sessionId: "sess-alias",
providerOverride: "byteplus",
modelOverride: "seedance-1-0-lite-t2v-250428",
authProfileOverride: "byteplus:work",
authProfileOverrideSource: "user",
authProfileOverrideCompactionCount: 2,
});
const entry = await applyMainModelPatch({
store,
model: "byteplus-plan/ark-code-latest",
catalogRefs: ["byteplus-plan/ark-code-latest"],
});
expectModelSelection(entry, "byteplus-plan", "ark-code-latest");
expectAuthOverride(entry, { profile: "byteplus:work", compactionCount: 2 });
});
test("preserves unprefixed auth overrides when existing provider matches model patch", async () => {
const store: Record<string, SessionEntry> = {
"agent:main:main": {
sessionId: "sess-unprefixed-same-provider",
updatedAt: 1,
providerOverride: "anthropic",
modelOverride: "claude-opus-4-6",
authProfileOverride: "work",
authProfileOverrideSource: "user",
authProfileOverrideCompactionCount: 4,
} as SessionEntry,
};
const entry = expectPatchOk(
await runPatch({
store,
patch: { key: MAIN_SESSION_KEY, model: "anthropic/claude-sonnet-4-6" },
loadGatewayModelCatalog: async () => [
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "sonnet" },
],
}),
);
expect(entry.providerOverride).toBe("anthropic");
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
expect(entry.authProfileOverride).toBe("work");
expect(entry.authProfileOverrideSource).toBe("user");
expect(entry.authProfileOverrideCompactionCount).toBe(4);
const store = mainAuthOverrideStore({
sessionId: "sess-unprefixed-same-provider",
authProfileOverride: "work",
authProfileOverrideCompactionCount: 4,
});
const entry = await applyMainModelPatch({
store,
model: ANTHROPIC_SONNET_MODEL,
catalogRefs: [ANTHROPIC_SONNET_MODEL],
});
expectModelSelection(entry, "anthropic", ANTHROPIC_SONNET_ID);
expectAuthOverride(entry, { profile: "work", compactionCount: 4 });
});
test("preserves unprefixed auth overrides when existing provider is the default", async () => {
const store: Record<string, SessionEntry> = {
"agent:main:main": {
sessionId: "sess-unprefixed-default-provider",
updatedAt: 1,
authProfileOverride: "work",
authProfileOverrideSource: "user",
authProfileOverrideCompactionCount: 4,
} as SessionEntry,
};
const entry = expectPatchOk(
await runPatch({
cfg: {
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-6" },
},
const store = mainStoreEntry({
sessionId: "sess-unprefixed-default-provider",
authProfileOverride: "work",
authProfileOverrideSource: "user",
authProfileOverrideCompactionCount: 4,
});
const entry = await applyMainModelPatch({
cfg: {
agents: {
defaults: {
model: { primary: `anthropic/${ANTHROPIC_OPUS_ID}` },
},
} as OpenClawConfig,
store,
patch: { key: MAIN_SESSION_KEY, model: "anthropic/claude-sonnet-4-6" },
loadGatewayModelCatalog: async () => [
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "sonnet" },
],
}),
);
expect(entry.providerOverride).toBe("anthropic");
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
expect(entry.authProfileOverride).toBe("work");
expect(entry.authProfileOverrideSource).toBe("user");
expect(entry.authProfileOverrideCompactionCount).toBe(4);
},
} as OpenClawConfig,
store,
model: ANTHROPIC_SONNET_MODEL,
catalogRefs: [ANTHROPIC_SONNET_MODEL],
});
expectModelSelection(entry, "anthropic", ANTHROPIC_SONNET_ID);
expectAuthOverride(entry, { profile: "work", compactionCount: 4 });
});
test("clears unprefixed auth overrides when model patch changes provider", async () => {
const store: Record<string, SessionEntry> = {
"agent:main:main": {
await expectProviderChangeClearsAuthOverride(
mainAuthOverrideStore({
sessionId: "sess-unprefixed-provider-change",
updatedAt: 1,
providerOverride: "anthropic",
modelOverride: "claude-opus-4-6",
authProfileOverride: "work",
authProfileOverrideSource: "user",
authProfileOverrideCompactionCount: 4,
} as SessionEntry,
};
const entry = expectPatchOk(
await runPatch({
store,
patch: { key: MAIN_SESSION_KEY, model: "openai/gpt-5.4" },
loadGatewayModelCatalog: async () => [
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
],
}),
);
expect(entry.providerOverride).toBe("openai");
expect(entry.modelOverride).toBe("gpt-5.4");
expect(entry.authProfileOverride).toBeUndefined();
expect(entry.authProfileOverrideSource).toBeUndefined();
expect(entry.authProfileOverrideCompactionCount).toBeUndefined();
});
test("clears provider-prefixed auth overrides when model patch changes provider", async () => {
const store: Record<string, SessionEntry> = {
"agent:main:main": {
await expectProviderChangeClearsAuthOverride(
mainAuthOverrideStore({
sessionId: "sess-provider-change",
updatedAt: 1,
providerOverride: "anthropic",
modelOverride: "claude-opus-4-6",
authProfileOverride: "anthropic:default",
authProfileOverrideSource: "user",
authProfileOverrideCompactionCount: 3,
} as SessionEntry,
};
const entry = expectPatchOk(
await runPatch({
store,
patch: { key: MAIN_SESSION_KEY, model: "openai/gpt-5.4" },
loadGatewayModelCatalog: async () => [
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
],
}),
);
expect(entry.providerOverride).toBe("openai");
expect(entry.modelOverride).toBe("gpt-5.4");
expect(entry.authProfileOverride).toBeUndefined();
expect(entry.authProfileOverrideSource).toBeUndefined();
expect(entry.authProfileOverrideCompactionCount).toBeUndefined();
});
test("marks explicit model patches as pending live model switches", async () => {
const store: Record<string, SessionEntry> = {
[MAIN_SESSION_KEY]: {
sessionId: "sess-live",
updatedAt: 1,
providerOverride: "openai",
modelOverride: "gpt-5.4",
} as SessionEntry,
};
const entry = expectPatchOk(
await runPatch({
store,
cfg: createAllowlistedAnthropicModelCfg(),
patch: { key: MAIN_SESSION_KEY, model: "anthropic/claude-sonnet-4-6" },
loadGatewayModelCatalog: async () => [
{ provider: "openai", id: "gpt-5.4", name: "gpt-5.4" },
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "claude-sonnet-4-6" },
],
}),
);
const store = mainStoreEntry({
sessionId: "sess-live",
providerOverride: "openai",
modelOverride: OPENAI_GPT_ID,
});
const entry = await applyMainModelPatch({
store,
cfg: createAllowlistedAnthropicModelCfg(),
model: ANTHROPIC_SONNET_MODEL,
catalogRefs: [OPENAI_GPT_MODEL, ANTHROPIC_SONNET_MODEL],
});
expect(entry.providerOverride).toBe("anthropic");
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
expectModelSelection(entry, "anthropic", ANTHROPIC_SONNET_ID);
expect(entry.liveModelSwitchPending).toBe(true);
});
test("marks model reset patches as pending live model switches", async () => {
const store: Record<string, SessionEntry> = {
[MAIN_SESSION_KEY]: {
sessionId: "sess-live-reset",
updatedAt: 1,
providerOverride: "anthropic",
modelOverride: "claude-sonnet-4-6",
} as SessionEntry,
};
const entry = expectPatchOk(
await runPatch({
store,
cfg: createAllowlistedAnthropicModelCfg(),
patch: { key: MAIN_SESSION_KEY, model: null },
}),
);
const store = mainStoreEntry({
sessionId: "sess-live-reset",
providerOverride: "anthropic",
modelOverride: ANTHROPIC_SONNET_ID,
});
const entry = await applyMainModelPatch({
store,
cfg: createAllowlistedAnthropicModelCfg(),
model: null,
});
expect(entry.providerOverride).toBeUndefined();
expect(entry.modelOverride).toBeUndefined();
expectModelSelection(entry, undefined, undefined);
expect(entry.liveModelSwitchPending).toBe(true);
});
@@ -515,12 +514,11 @@ describe("gateway sessions patch", () => {
const entry = expectPatchOk(
await runPatch({
cfg: createAllowlistedAnthropicModelCfg(),
patch: { key: MAIN_SESSION_KEY, model: "anthropic/claude-sonnet-4-6" },
patch: { key: MAIN_SESSION_KEY, model: ANTHROPIC_SONNET_MODEL },
loadGatewayModelCatalog: async () => catalog,
}),
);
expect(entry.providerOverride).toBe("anthropic");
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
expectModelSelection(entry, "anthropic", ANTHROPIC_SONNET_ID);
});
test("sets spawnDepth for subagent sessions", async () => {
@@ -808,7 +806,7 @@ describe("gateway sessions patch", () => {
test("allows target agent own model for subagent session even when missing from global allowlist", async () => {
const cfg = makeKimiSubagentCfg({
agentPrimaryModel: "synthetic/hf:moonshotai/Kimi-K2.5",
agentPrimaryModel: SUBAGENT_MODEL,
});
const entry = await applySubagentModelPatch(cfg);
@@ -819,13 +817,12 @@ describe("gateway sessions patch", () => {
test("allows target agent subagents.model for subagent session even when missing from global allowlist", async () => {
const cfg = makeKimiSubagentCfg({
agentPrimaryModel: "anthropic/claude-sonnet-4-6",
agentPrimaryModel: ANTHROPIC_SONNET_MODEL,
agentSubagentModel: SUBAGENT_MODEL,
});
const entry = await applySubagentModelPatch(cfg);
expect(entry.providerOverride).toBe("synthetic");
expect(entry.modelOverride).toBe("hf:moonshotai/Kimi-K2.5");
expectModelSelection(entry, "synthetic", "hf:moonshotai/Kimi-K2.5");
});
test("allows global defaults.subagents.model for subagent session even when missing from global allowlist", async () => {
@@ -834,86 +831,57 @@ describe("gateway sessions patch", () => {
});
const entry = await applySubagentModelPatch(cfg);
expect(entry.providerOverride).toBe("synthetic");
expect(entry.modelOverride).toBe("hf:moonshotai/Kimi-K2.5");
expectModelSelection(entry, "synthetic", "hf:moonshotai/Kimi-K2.5");
});
test("persists trailing @profile suffix as authProfileOverride on model patch", async () => {
const entry = expectPatchOk(
await runPatch({
cfg: createAllowlistedAnthropicModelCfg(),
patch: { key: MAIN_SESSION_KEY, model: "anthropic/claude-sonnet-4-6@myprofile" },
loadGatewayModelCatalog: async () => [
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "claude-sonnet-4-6" },
],
}),
);
expect(entry.providerOverride).toBe("anthropic");
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
expect(entry.authProfileOverride).toBe("myprofile");
expect(entry.authProfileOverrideSource).toBe("user");
const entry = await applyMainModelPatch({
cfg: createAllowlistedAnthropicModelCfg(),
model: `${ANTHROPIC_SONNET_MODEL}@myprofile`,
catalogRefs: [ANTHROPIC_SONNET_MODEL],
});
expectModelSelection(entry, "anthropic", ANTHROPIC_SONNET_ID);
expectAuthOverride(entry, { profile: "myprofile" });
expect(entry.liveModelSwitchPending).toBe(true);
});
test("marks same-model @profile patches as pending live model switches", async () => {
const store: Record<string, SessionEntry> = {
[MAIN_SESSION_KEY]: {
sessionId: "sess-live-profile-only",
updatedAt: 1,
providerOverride: "anthropic",
modelOverride: "claude-sonnet-4-6",
authProfileOverride: "oldprofile",
authProfileOverrideSource: "user",
} as SessionEntry,
};
const entry = expectPatchOk(
await runPatch({
store,
cfg: createAllowlistedAnthropicModelCfg(),
patch: { key: MAIN_SESSION_KEY, model: "anthropic/claude-sonnet-4-6@newprofile" },
loadGatewayModelCatalog: async () => [
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "claude-sonnet-4-6" },
],
}),
);
expect(entry.providerOverride).toBe("anthropic");
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
expect(entry.authProfileOverride).toBe("newprofile");
const store = mainStoreEntry({
sessionId: "sess-live-profile-only",
providerOverride: "anthropic",
modelOverride: ANTHROPIC_SONNET_ID,
authProfileOverride: "oldprofile",
authProfileOverrideSource: "user",
});
const entry = await applyMainModelPatch({
store,
cfg: createAllowlistedAnthropicModelCfg(),
model: `${ANTHROPIC_SONNET_MODEL}@newprofile`,
catalogRefs: [ANTHROPIC_SONNET_MODEL],
});
expectModelSelection(entry, "anthropic", ANTHROPIC_SONNET_ID);
expectAuthOverride(entry, { profile: "newprofile" });
expect(entry.liveModelSwitchPending).toBe(true);
});
test("does not set authProfileOverride when profile suffix is missing", async () => {
const entry = expectPatchOk(
await runPatch({
cfg: createAllowlistedAnthropicModelCfg(),
patch: { key: MAIN_SESSION_KEY, model: "anthropic/claude-sonnet-4-6" },
loadGatewayModelCatalog: async () => [
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "claude-sonnet-4-6" },
],
}),
);
expect(entry.providerOverride).toBe("anthropic");
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
expect(entry.authProfileOverride).toBeUndefined();
const entry = await applyMainModelPatch({
cfg: createAllowlistedAnthropicModelCfg(),
model: ANTHROPIC_SONNET_MODEL,
catalogRefs: [ANTHROPIC_SONNET_MODEL],
});
expectModelSelection(entry, "anthropic", ANTHROPIC_SONNET_ID);
expectAuthOverride(entry, { profile: undefined });
});
test("persists full provider:profile authProfileOverride on model patch", async () => {
const entry = expectPatchOk(
await runPatch({
cfg: createAllowlistedAnthropicModelCfg(),
patch: {
key: MAIN_SESSION_KEY,
model: "anthropic/claude-sonnet-4-6@openai:user@example.com",
},
loadGatewayModelCatalog: async () => [
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "claude-sonnet-4-6" },
],
}),
);
expect(entry.providerOverride).toBe("anthropic");
expect(entry.modelOverride).toBe("claude-sonnet-4-6");
expect(entry.authProfileOverride).toBe("openai:user@example.com");
expect(entry.authProfileOverrideSource).toBe("user");
const entry = await applyMainModelPatch({
cfg: createAllowlistedAnthropicModelCfg(),
model: `${ANTHROPIC_SONNET_MODEL}@openai:user@example.com`,
catalogRefs: [ANTHROPIC_SONNET_MODEL],
});
expectModelSelection(entry, "anthropic", ANTHROPIC_SONNET_ID);
expectAuthOverride(entry, { profile: "openai:user@example.com" });
});
test("resolves bare allowlisted model ids before persisting @profile suffix", async () => {