mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:18:37 +00:00
test(core): increase coverage for sessions, auth choice, and model listing
This commit is contained in:
@@ -74,27 +74,29 @@ describe("resolvePermissionRequest", () => {
|
|||||||
expect(prompt).not.toHaveBeenCalled();
|
expect(prompt).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("prompts for fetch even when tool name is known", async () => {
|
it.each([
|
||||||
|
{
|
||||||
|
caseName: "prompts for fetch even when tool name is known",
|
||||||
|
toolCallId: "tool-f",
|
||||||
|
title: "fetch: https://example.com",
|
||||||
|
expectedToolName: "fetch",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: "prompts when tool name contains read/search substrings but isn't a safe kind",
|
||||||
|
toolCallId: "tool-t",
|
||||||
|
title: "thread: reply",
|
||||||
|
expectedToolName: "thread",
|
||||||
|
},
|
||||||
|
])("$caseName", async ({ toolCallId, title, expectedToolName }) => {
|
||||||
const prompt = vi.fn(async () => false);
|
const prompt = vi.fn(async () => false);
|
||||||
const res = await resolvePermissionRequest(
|
const res = await resolvePermissionRequest(
|
||||||
makePermissionRequest({
|
makePermissionRequest({
|
||||||
toolCall: { toolCallId: "tool-f", title: "fetch: https://example.com", status: "pending" },
|
toolCall: { toolCallId, title, status: "pending" },
|
||||||
}),
|
|
||||||
{ prompt, log: () => {} },
|
|
||||||
);
|
|
||||||
expect(prompt).toHaveBeenCalledTimes(1);
|
|
||||||
expect(res).toEqual({ outcome: { outcome: "selected", optionId: "reject" } });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("prompts when tool name contains read/search substrings but isn't a safe kind", async () => {
|
|
||||||
const prompt = vi.fn(async () => false);
|
|
||||||
const res = await resolvePermissionRequest(
|
|
||||||
makePermissionRequest({
|
|
||||||
toolCall: { toolCallId: "tool-t", title: "thread: reply", status: "pending" },
|
|
||||||
}),
|
}),
|
||||||
{ prompt, log: () => {} },
|
{ prompt, log: () => {} },
|
||||||
);
|
);
|
||||||
expect(prompt).toHaveBeenCalledTimes(1);
|
expect(prompt).toHaveBeenCalledTimes(1);
|
||||||
|
expect(prompt).toHaveBeenCalledWith(expectedToolName, title);
|
||||||
expect(res).toEqual({ outcome: { outcome: "selected", optionId: "reject" } });
|
expect(res).toEqual({ outcome: { outcome: "selected", optionId: "reject" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ function createHuggingfacePrompter(params: {
|
|||||||
text: WizardPrompter["text"];
|
text: WizardPrompter["text"];
|
||||||
select: WizardPrompter["select"];
|
select: WizardPrompter["select"];
|
||||||
confirm?: WizardPrompter["confirm"];
|
confirm?: WizardPrompter["confirm"];
|
||||||
|
note?: WizardPrompter["note"];
|
||||||
}): WizardPrompter {
|
}): WizardPrompter {
|
||||||
const overrides: Partial<WizardPrompter> = {
|
const overrides: Partial<WizardPrompter> = {
|
||||||
text: params.text,
|
text: params.text,
|
||||||
@@ -21,6 +22,9 @@ function createHuggingfacePrompter(params: {
|
|||||||
if (params.confirm) {
|
if (params.confirm) {
|
||||||
overrides.confirm = params.confirm;
|
overrides.confirm = params.confirm;
|
||||||
}
|
}
|
||||||
|
if (params.note) {
|
||||||
|
overrides.note = params.note;
|
||||||
|
}
|
||||||
return createWizardPrompter(overrides, { defaultSelect: "" });
|
return createWizardPrompter(overrides, { defaultSelect: "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,9 +99,26 @@ describe("applyAuthChoiceHuggingface", () => {
|
|||||||
expect(parsed.profiles?.["huggingface:default"]?.key).toBe("hf-test-token");
|
expect(parsed.profiles?.["huggingface:default"]?.key).toBe("hf-test-token");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not prompt to reuse env token when opts.token already provided", async () => {
|
it.each([
|
||||||
|
{
|
||||||
|
caseName: "does not prompt to reuse env token when opts.token already provided",
|
||||||
|
tokenProvider: "huggingface",
|
||||||
|
token: "hf-opts-token",
|
||||||
|
envToken: "hf-env-token",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
caseName: "accepts mixed-case tokenProvider from opts without prompting",
|
||||||
|
tokenProvider: " HuGgInGfAcE ",
|
||||||
|
token: "hf-opts-mixed",
|
||||||
|
envToken: undefined,
|
||||||
|
},
|
||||||
|
])("$caseName", async ({ tokenProvider, token, envToken }) => {
|
||||||
const agentDir = await setupTempState();
|
const agentDir = await setupTempState();
|
||||||
process.env.HF_TOKEN = "hf-env-token";
|
if (envToken) {
|
||||||
|
process.env.HF_TOKEN = envToken;
|
||||||
|
} else {
|
||||||
|
delete process.env.HF_TOKEN;
|
||||||
|
}
|
||||||
delete process.env.HUGGINGFACE_HUB_TOKEN;
|
delete process.env.HUGGINGFACE_HUB_TOKEN;
|
||||||
|
|
||||||
const text = vi.fn().mockResolvedValue("hf-text-token");
|
const text = vi.fn().mockResolvedValue("hf-text-token");
|
||||||
@@ -115,8 +136,8 @@ describe("applyAuthChoiceHuggingface", () => {
|
|||||||
runtime,
|
runtime,
|
||||||
setDefaultModel: true,
|
setDefaultModel: true,
|
||||||
opts: {
|
opts: {
|
||||||
tokenProvider: "huggingface",
|
tokenProvider,
|
||||||
token: "hf-opts-token",
|
token,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -125,20 +146,22 @@ describe("applyAuthChoiceHuggingface", () => {
|
|||||||
expect(text).not.toHaveBeenCalled();
|
expect(text).not.toHaveBeenCalled();
|
||||||
|
|
||||||
const parsed = await readAuthProfiles(agentDir);
|
const parsed = await readAuthProfiles(agentDir);
|
||||||
expect(parsed.profiles?.["huggingface:default"]?.key).toBe("hf-opts-token");
|
expect(parsed.profiles?.["huggingface:default"]?.key).toBe(token);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts mixed-case tokenProvider from opts without prompting", async () => {
|
it("notes when selected Hugging Face model uses a locked router policy", async () => {
|
||||||
const agentDir = await setupTempState();
|
await setupTempState();
|
||||||
delete process.env.HF_TOKEN;
|
delete process.env.HF_TOKEN;
|
||||||
delete process.env.HUGGINGFACE_HUB_TOKEN;
|
delete process.env.HUGGINGFACE_HUB_TOKEN;
|
||||||
|
|
||||||
const text = vi.fn().mockResolvedValue("hf-text-token");
|
const text = vi.fn().mockResolvedValue("hf-test-token");
|
||||||
const select: WizardPrompter["select"] = vi.fn(
|
const select: WizardPrompter["select"] = vi.fn(async (params) => {
|
||||||
async (params) => params.options?.[0]?.value as never,
|
const options = (params.options ?? []) as Array<{ value: string }>;
|
||||||
);
|
const cheapest = options.find((option) => option.value.endsWith(":cheapest"));
|
||||||
const confirm = vi.fn(async () => true);
|
return (cheapest?.value ?? options[0]?.value ?? "") as never;
|
||||||
const prompter = createHuggingfacePrompter({ text, select, confirm });
|
});
|
||||||
|
const note: WizardPrompter["note"] = vi.fn(async () => {});
|
||||||
|
const prompter = createHuggingfacePrompter({ text, select, note });
|
||||||
const runtime = createExitThrowingRuntime();
|
const runtime = createExitThrowingRuntime();
|
||||||
|
|
||||||
const result = await applyAuthChoiceHuggingface({
|
const result = await applyAuthChoiceHuggingface({
|
||||||
@@ -147,17 +170,13 @@ describe("applyAuthChoiceHuggingface", () => {
|
|||||||
prompter,
|
prompter,
|
||||||
runtime,
|
runtime,
|
||||||
setDefaultModel: true,
|
setDefaultModel: true,
|
||||||
opts: {
|
|
||||||
tokenProvider: " HuGgInGfAcE ",
|
|
||||||
token: "hf-opts-mixed",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(confirm).not.toHaveBeenCalled();
|
expect(String(result?.config.agents?.defaults?.model?.primary)).toContain(":cheapest");
|
||||||
expect(text).not.toHaveBeenCalled();
|
expect(note).toHaveBeenCalledWith(
|
||||||
|
"Provider locked — router will choose backend by cost or speed.",
|
||||||
const parsed = await readAuthProfiles(agentDir);
|
"Hugging Face",
|
||||||
expect(parsed.profiles?.["huggingface:default"]?.key).toBe("hf-opts-mixed");
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,11 @@ describe("applyAuthChoiceMiniMax", () => {
|
|||||||
}>(agentDir);
|
}>(agentDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetMiniMaxEnv(): void {
|
||||||
|
delete process.env.MINIMAX_API_KEY;
|
||||||
|
delete process.env.MINIMAX_OAUTH_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
await lifecycle.cleanup();
|
await lifecycle.cleanup();
|
||||||
});
|
});
|
||||||
@@ -63,38 +68,60 @@ describe("applyAuthChoiceMiniMax", () => {
|
|||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses opts token for minimax-api without prompt", async () => {
|
it.each([
|
||||||
const agentDir = await setupTempState();
|
{
|
||||||
delete process.env.MINIMAX_API_KEY;
|
caseName: "uses opts token for minimax-api without prompt",
|
||||||
delete process.env.MINIMAX_OAUTH_TOKEN;
|
authChoice: "minimax-api" as const,
|
||||||
|
tokenProvider: "minimax",
|
||||||
const text = vi.fn(async () => "should-not-be-used");
|
token: "mm-opts-token",
|
||||||
const confirm = vi.fn(async () => true);
|
profileId: "minimax:default",
|
||||||
|
|
||||||
const result = await applyAuthChoiceMiniMax({
|
|
||||||
authChoice: "minimax-api",
|
|
||||||
config: {},
|
|
||||||
prompter: createMinimaxPrompter({ text, confirm }),
|
|
||||||
runtime: createExitThrowingRuntime(),
|
|
||||||
setDefaultModel: true,
|
|
||||||
opts: {
|
|
||||||
tokenProvider: "minimax",
|
|
||||||
token: "mm-opts-token",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
|
||||||
expect(result?.config.auth?.profiles?.["minimax:default"]).toMatchObject({
|
|
||||||
provider: "minimax",
|
provider: "minimax",
|
||||||
mode: "api_key",
|
expectedModel: "minimax/MiniMax-M2.5",
|
||||||
});
|
},
|
||||||
expect(result?.config.agents?.defaults?.model?.primary).toBe("minimax/MiniMax-M2.5");
|
{
|
||||||
expect(text).not.toHaveBeenCalled();
|
caseName:
|
||||||
expect(confirm).not.toHaveBeenCalled();
|
"uses opts token for minimax-api-key-cn with trimmed/case-insensitive tokenProvider",
|
||||||
|
authChoice: "minimax-api-key-cn" as const,
|
||||||
|
tokenProvider: " MINIMAX-CN ",
|
||||||
|
token: "mm-cn-opts-token",
|
||||||
|
profileId: "minimax-cn:default",
|
||||||
|
provider: "minimax-cn",
|
||||||
|
expectedModel: "minimax-cn/MiniMax-M2.5",
|
||||||
|
},
|
||||||
|
])(
|
||||||
|
"$caseName",
|
||||||
|
async ({ authChoice, tokenProvider, token, profileId, provider, expectedModel }) => {
|
||||||
|
const agentDir = await setupTempState();
|
||||||
|
resetMiniMaxEnv();
|
||||||
|
|
||||||
const parsed = await readAuthProfiles(agentDir);
|
const text = vi.fn(async () => "should-not-be-used");
|
||||||
expect(parsed.profiles?.["minimax:default"]?.key).toBe("mm-opts-token");
|
const confirm = vi.fn(async () => true);
|
||||||
});
|
|
||||||
|
const result = await applyAuthChoiceMiniMax({
|
||||||
|
authChoice,
|
||||||
|
config: {},
|
||||||
|
prompter: createMinimaxPrompter({ text, confirm }),
|
||||||
|
runtime: createExitThrowingRuntime(),
|
||||||
|
setDefaultModel: true,
|
||||||
|
opts: {
|
||||||
|
tokenProvider,
|
||||||
|
token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).not.toBeNull();
|
||||||
|
expect(result?.config.auth?.profiles?.[profileId]).toMatchObject({
|
||||||
|
provider,
|
||||||
|
mode: "api_key",
|
||||||
|
});
|
||||||
|
expect(result?.config.agents?.defaults?.model?.primary).toBe(expectedModel);
|
||||||
|
expect(text).not.toHaveBeenCalled();
|
||||||
|
expect(confirm).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const parsed = await readAuthProfiles(agentDir);
|
||||||
|
expect(parsed.profiles?.[profileId]?.key).toBe(token);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("uses env token for minimax-api-key-cn when confirmed", async () => {
|
it("uses env token for minimax-api-key-cn when confirmed", async () => {
|
||||||
const agentDir = await setupTempState();
|
const agentDir = await setupTempState();
|
||||||
@@ -125,36 +152,35 @@ describe("applyAuthChoiceMiniMax", () => {
|
|||||||
expect(parsed.profiles?.["minimax-cn:default"]?.key).toBe("mm-env-token");
|
expect(parsed.profiles?.["minimax-cn:default"]?.key).toBe("mm-env-token");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses opts token for minimax-api-key-cn with trimmed/case-insensitive tokenProvider", async () => {
|
it("uses minimax-api-lightning default model", async () => {
|
||||||
const agentDir = await setupTempState();
|
const agentDir = await setupTempState();
|
||||||
delete process.env.MINIMAX_API_KEY;
|
resetMiniMaxEnv();
|
||||||
delete process.env.MINIMAX_OAUTH_TOKEN;
|
|
||||||
|
|
||||||
const text = vi.fn(async () => "should-not-be-used");
|
const text = vi.fn(async () => "should-not-be-used");
|
||||||
const confirm = vi.fn(async () => true);
|
const confirm = vi.fn(async () => true);
|
||||||
|
|
||||||
const result = await applyAuthChoiceMiniMax({
|
const result = await applyAuthChoiceMiniMax({
|
||||||
authChoice: "minimax-api-key-cn",
|
authChoice: "minimax-api-lightning",
|
||||||
config: {},
|
config: {},
|
||||||
prompter: createMinimaxPrompter({ text, confirm }),
|
prompter: createMinimaxPrompter({ text, confirm }),
|
||||||
runtime: createExitThrowingRuntime(),
|
runtime: createExitThrowingRuntime(),
|
||||||
setDefaultModel: true,
|
setDefaultModel: true,
|
||||||
opts: {
|
opts: {
|
||||||
tokenProvider: " MINIMAX-CN ",
|
tokenProvider: "minimax",
|
||||||
token: "mm-cn-opts-token",
|
token: "mm-lightning-token",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result).not.toBeNull();
|
expect(result).not.toBeNull();
|
||||||
expect(result?.config.auth?.profiles?.["minimax-cn:default"]).toMatchObject({
|
expect(result?.config.auth?.profiles?.["minimax:default"]).toMatchObject({
|
||||||
provider: "minimax-cn",
|
provider: "minimax",
|
||||||
mode: "api_key",
|
mode: "api_key",
|
||||||
});
|
});
|
||||||
expect(result?.config.agents?.defaults?.model?.primary).toBe("minimax-cn/MiniMax-M2.5");
|
expect(result?.config.agents?.defaults?.model?.primary).toBe("minimax/MiniMax-M2.5-Lightning");
|
||||||
expect(text).not.toHaveBeenCalled();
|
expect(text).not.toHaveBeenCalled();
|
||||||
expect(confirm).not.toHaveBeenCalled();
|
expect(confirm).not.toHaveBeenCalled();
|
||||||
|
|
||||||
const parsed = await readAuthProfiles(agentDir);
|
const parsed = await readAuthProfiles(agentDir);
|
||||||
expect(parsed.profiles?.["minimax-cn:default"]?.key).toBe("mm-cn-opts-token");
|
expect(parsed.profiles?.["minimax:default"]?.key).toBe("mm-lightning-token");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -260,6 +260,23 @@ describe("models list/status", () => {
|
|||||||
return parseJsonLog(runtime);
|
return parseJsonLog(runtime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GOOGLE_ANTIGRAVITY_OPUS_46_CASES = [
|
||||||
|
{
|
||||||
|
name: "thinking",
|
||||||
|
configuredModelId: "claude-opus-4-6-thinking",
|
||||||
|
templateId: "claude-opus-4-5-thinking",
|
||||||
|
templateName: "Claude Opus 4.5 Thinking",
|
||||||
|
expectedKey: "google-antigravity/claude-opus-4-6-thinking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-thinking",
|
||||||
|
configuredModelId: "claude-opus-4-6",
|
||||||
|
templateId: "claude-opus-4-5",
|
||||||
|
templateName: "Claude Opus 4.5",
|
||||||
|
expectedKey: "google-antigravity/claude-opus-4-6",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
function expectAntigravityModel(
|
function expectAntigravityModel(
|
||||||
payload: Record<string, unknown>,
|
payload: Record<string, unknown>,
|
||||||
params: { key: string; available: boolean; includesTags?: boolean },
|
params: { key: string; available: boolean; includesTags?: boolean },
|
||||||
@@ -329,22 +346,7 @@ describe("models list/status", () => {
|
|||||||
expect(payload.models[0]?.available).toBe(false);
|
expect(payload.models[0]?.available).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each(GOOGLE_ANTIGRAVITY_OPUS_46_CASES)(
|
||||||
{
|
|
||||||
name: "thinking",
|
|
||||||
configuredModelId: "claude-opus-4-6-thinking",
|
|
||||||
templateId: "claude-opus-4-5-thinking",
|
|
||||||
templateName: "Claude Opus 4.5 Thinking",
|
|
||||||
expectedKey: "google-antigravity/claude-opus-4-6-thinking",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-thinking",
|
|
||||||
configuredModelId: "claude-opus-4-6",
|
|
||||||
templateId: "claude-opus-4-5",
|
|
||||||
templateName: "Claude Opus 4.5",
|
|
||||||
expectedKey: "google-antigravity/claude-opus-4-6",
|
|
||||||
},
|
|
||||||
] as const)(
|
|
||||||
"models list resolves antigravity opus 4.6 $name from 4.5 template",
|
"models list resolves antigravity opus 4.6 $name from 4.5 template",
|
||||||
async ({ configuredModelId, templateId, templateName, expectedKey }) => {
|
async ({ configuredModelId, templateId, templateName, expectedKey }) => {
|
||||||
const payload = await runGoogleAntigravityListCase({
|
const payload = await runGoogleAntigravityListCase({
|
||||||
@@ -360,22 +362,7 @@ describe("models list/status", () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
it.each([
|
it.each(GOOGLE_ANTIGRAVITY_OPUS_46_CASES)(
|
||||||
{
|
|
||||||
name: "thinking",
|
|
||||||
configuredModelId: "claude-opus-4-6-thinking",
|
|
||||||
templateId: "claude-opus-4-5-thinking",
|
|
||||||
templateName: "Claude Opus 4.5 Thinking",
|
|
||||||
expectedKey: "google-antigravity/claude-opus-4-6-thinking",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "non-thinking",
|
|
||||||
configuredModelId: "claude-opus-4-6",
|
|
||||||
templateId: "claude-opus-4-5",
|
|
||||||
templateName: "Claude Opus 4.5",
|
|
||||||
expectedKey: "google-antigravity/claude-opus-4-6",
|
|
||||||
},
|
|
||||||
] as const)(
|
|
||||||
"models list marks synthesized antigravity opus 4.6 $name as available when template is available",
|
"models list marks synthesized antigravity opus 4.6 $name as available when template is available",
|
||||||
async ({ configuredModelId, templateId, templateName, expectedKey }) => {
|
async ({ configuredModelId, templateId, templateName, expectedKey }) => {
|
||||||
const payload = await runGoogleAntigravityListCase({
|
const payload = await runGoogleAntigravityListCase({
|
||||||
|
|||||||
@@ -137,28 +137,7 @@ describe("applyModelDefaults", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("propagates provider api to models when model api is missing", () => {
|
it("propagates provider api to models when model api is missing", () => {
|
||||||
const cfg = {
|
const cfg = buildProxyProviderConfig();
|
||||||
models: {
|
|
||||||
providers: {
|
|
||||||
myproxy: {
|
|
||||||
baseUrl: "https://proxy.example/v1",
|
|
||||||
apiKey: "sk-test",
|
|
||||||
api: "openai-completions",
|
|
||||||
models: [
|
|
||||||
{
|
|
||||||
id: "gpt-5.2",
|
|
||||||
name: "GPT-5.2",
|
|
||||||
reasoning: false,
|
|
||||||
input: ["text"],
|
|
||||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
||||||
contextWindow: 200_000,
|
|
||||||
maxTokens: 8192,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies OpenClawConfig;
|
|
||||||
|
|
||||||
const next = applyModelDefaults(cfg);
|
const next = applyModelDefaults(cfg);
|
||||||
const model = next.models?.providers?.myproxy?.models?.[0];
|
const model = next.models?.providers?.myproxy?.models?.[0];
|
||||||
|
|||||||
@@ -37,6 +37,16 @@ describe("sessions", () => {
|
|||||||
const withStateDir = <T>(stateDir: string, fn: () => T): T =>
|
const withStateDir = <T>(stateDir: string, fn: () => T): T =>
|
||||||
withEnv({ OPENCLAW_STATE_DIR: stateDir }, fn);
|
withEnv({ OPENCLAW_STATE_DIR: stateDir }, fn);
|
||||||
|
|
||||||
|
async function createSessionStoreFixture(params: {
|
||||||
|
prefix: string;
|
||||||
|
entries: Record<string, Record<string, unknown>>;
|
||||||
|
}): Promise<{ storePath: string }> {
|
||||||
|
const dir = await createCaseDir(params.prefix);
|
||||||
|
const storePath = path.join(dir, "sessions.json");
|
||||||
|
await fs.writeFile(storePath, JSON.stringify(params.entries, null, 2), "utf-8");
|
||||||
|
return { storePath };
|
||||||
|
}
|
||||||
|
|
||||||
const deriveSessionKeyCases = [
|
const deriveSessionKeyCases = [
|
||||||
{
|
{
|
||||||
name: "returns normalized per-sender key",
|
name: "returns normalized per-sender key",
|
||||||
@@ -307,23 +317,16 @@ describe("sessions", () => {
|
|||||||
|
|
||||||
it("updateSessionStoreEntry preserves existing fields when patching", async () => {
|
it("updateSessionStoreEntry preserves existing fields when patching", async () => {
|
||||||
const sessionKey = "agent:main:main";
|
const sessionKey = "agent:main:main";
|
||||||
const dir = await createCaseDir("updateSessionStoreEntry");
|
const { storePath } = await createSessionStoreFixture({
|
||||||
const storePath = path.join(dir, "sessions.json");
|
prefix: "updateSessionStoreEntry",
|
||||||
await fs.writeFile(
|
entries: {
|
||||||
storePath,
|
[sessionKey]: {
|
||||||
JSON.stringify(
|
sessionId: "sess-1",
|
||||||
{
|
updatedAt: 100,
|
||||||
[sessionKey]: {
|
reasoningLevel: "on",
|
||||||
sessionId: "sess-1",
|
|
||||||
updatedAt: 100,
|
|
||||||
reasoningLevel: "on",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
null,
|
},
|
||||||
2,
|
});
|
||||||
),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
|
|
||||||
await updateSessionStoreEntry({
|
await updateSessionStoreEntry({
|
||||||
storePath,
|
storePath,
|
||||||
@@ -336,6 +339,44 @@ describe("sessions", () => {
|
|||||||
expect(store[sessionKey]?.reasoningLevel).toBe("on");
|
expect(store[sessionKey]?.reasoningLevel).toBe("on");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("updateSessionStoreEntry returns null when session key does not exist", async () => {
|
||||||
|
const { storePath } = await createSessionStoreFixture({
|
||||||
|
prefix: "updateSessionStoreEntry-missing",
|
||||||
|
entries: {},
|
||||||
|
});
|
||||||
|
const update = async () => ({ thinkingLevel: "high" as const });
|
||||||
|
const result = await updateSessionStoreEntry({
|
||||||
|
storePath,
|
||||||
|
sessionKey: "agent:main:missing",
|
||||||
|
update,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updateSessionStoreEntry keeps existing entry when patch callback returns null", async () => {
|
||||||
|
const sessionKey = "agent:main:main";
|
||||||
|
const { storePath } = await createSessionStoreFixture({
|
||||||
|
prefix: "updateSessionStoreEntry-noop",
|
||||||
|
entries: {
|
||||||
|
[sessionKey]: {
|
||||||
|
sessionId: "sess-1",
|
||||||
|
updatedAt: 123,
|
||||||
|
thinkingLevel: "low",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await updateSessionStoreEntry({
|
||||||
|
storePath,
|
||||||
|
sessionKey,
|
||||||
|
update: async () => null,
|
||||||
|
});
|
||||||
|
expect(result).toEqual(expect.objectContaining({ sessionId: "sess-1", thinkingLevel: "low" }));
|
||||||
|
|
||||||
|
const store = loadSessionStore(storePath);
|
||||||
|
expect(store[sessionKey]?.thinkingLevel).toBe("low");
|
||||||
|
});
|
||||||
|
|
||||||
it("updateSessionStore preserves concurrent additions", async () => {
|
it("updateSessionStore preserves concurrent additions", async () => {
|
||||||
const dir = await createCaseDir("updateSessionStore");
|
const dir = await createCaseDir("updateSessionStore");
|
||||||
const storePath = path.join(dir, "sessions.json");
|
const storePath = path.join(dir, "sessions.json");
|
||||||
@@ -534,23 +575,16 @@ describe("sessions", () => {
|
|||||||
|
|
||||||
it("updateSessionStoreEntry merges concurrent patches", async () => {
|
it("updateSessionStoreEntry merges concurrent patches", async () => {
|
||||||
const mainSessionKey = "agent:main:main";
|
const mainSessionKey = "agent:main:main";
|
||||||
const dir = await createCaseDir("updateSessionStoreEntry");
|
const { storePath } = await createSessionStoreFixture({
|
||||||
const storePath = path.join(dir, "sessions.json");
|
prefix: "updateSessionStoreEntry",
|
||||||
await fs.writeFile(
|
entries: {
|
||||||
storePath,
|
[mainSessionKey]: {
|
||||||
JSON.stringify(
|
sessionId: "sess-1",
|
||||||
{
|
updatedAt: 123,
|
||||||
[mainSessionKey]: {
|
thinkingLevel: "low",
|
||||||
sessionId: "sess-1",
|
|
||||||
updatedAt: 123,
|
|
||||||
thinkingLevel: "low",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
null,
|
},
|
||||||
2,
|
});
|
||||||
),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
|
|
||||||
const createDeferred = <T>() => {
|
const createDeferred = <T>() => {
|
||||||
let resolve!: (value: T) => void;
|
let resolve!: (value: T) => void;
|
||||||
@@ -594,23 +628,16 @@ describe("sessions", () => {
|
|||||||
|
|
||||||
it("updateSessionStoreEntry re-reads disk inside lock instead of using stale cache", async () => {
|
it("updateSessionStoreEntry re-reads disk inside lock instead of using stale cache", async () => {
|
||||||
const mainSessionKey = "agent:main:main";
|
const mainSessionKey = "agent:main:main";
|
||||||
const dir = await createCaseDir("updateSessionStoreEntry-cache-bypass");
|
const { storePath } = await createSessionStoreFixture({
|
||||||
const storePath = path.join(dir, "sessions.json");
|
prefix: "updateSessionStoreEntry-cache-bypass",
|
||||||
await fs.writeFile(
|
entries: {
|
||||||
storePath,
|
[mainSessionKey]: {
|
||||||
JSON.stringify(
|
sessionId: "sess-1",
|
||||||
{
|
updatedAt: 123,
|
||||||
[mainSessionKey]: {
|
thinkingLevel: "low",
|
||||||
sessionId: "sess-1",
|
|
||||||
updatedAt: 123,
|
|
||||||
thinkingLevel: "low",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
null,
|
},
|
||||||
2,
|
});
|
||||||
),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prime the in-process cache with the original entry.
|
// Prime the in-process cache with the original entry.
|
||||||
expect(loadSessionStore(storePath)[mainSessionKey]?.thinkingLevel).toBe("low");
|
expect(loadSessionStore(storePath)[mainSessionKey]?.thinkingLevel).toBe("low");
|
||||||
|
|||||||
@@ -84,6 +84,25 @@ function createEnv(
|
|||||||
return env;
|
return env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requireSandbox(sandbox: DockerSetupSandbox | null): DockerSetupSandbox {
|
||||||
|
if (!sandbox) {
|
||||||
|
throw new Error("sandbox missing");
|
||||||
|
}
|
||||||
|
return sandbox;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runDockerSetup(
|
||||||
|
sandbox: DockerSetupSandbox,
|
||||||
|
overrides: Record<string, string | undefined> = {},
|
||||||
|
) {
|
||||||
|
return spawnSync("bash", [sandbox.scriptPath], {
|
||||||
|
cwd: sandbox.rootDir,
|
||||||
|
env: createEnv(sandbox, overrides),
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "ignore", "pipe"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function resolveBashForCompatCheck(): string | null {
|
function resolveBashForCompatCheck(): string | null {
|
||||||
for (const candidate of ["/bin/bash", "bash"]) {
|
for (const candidate of ["/bin/bash", "bash"]) {
|
||||||
const probe = spawnSync(candidate, ["-c", "exit 0"], { encoding: "utf8" });
|
const probe = spawnSync(candidate, ["-c", "exit 0"], { encoding: "utf8" });
|
||||||
@@ -111,44 +130,34 @@ describe("docker-setup.sh", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("handles env defaults, home-volume mounts, and apt build args", async () => {
|
it("handles env defaults, home-volume mounts, and apt build args", async () => {
|
||||||
if (!sandbox) {
|
const activeSandbox = requireSandbox(sandbox);
|
||||||
throw new Error("sandbox missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = spawnSync("bash", [sandbox.scriptPath], {
|
const result = runDockerSetup(activeSandbox, {
|
||||||
cwd: sandbox.rootDir,
|
OPENCLAW_DOCKER_APT_PACKAGES: "ffmpeg build-essential",
|
||||||
env: createEnv(sandbox, {
|
OPENCLAW_EXTRA_MOUNTS: undefined,
|
||||||
OPENCLAW_DOCKER_APT_PACKAGES: "ffmpeg build-essential",
|
OPENCLAW_HOME_VOLUME: "openclaw-home",
|
||||||
OPENCLAW_EXTRA_MOUNTS: undefined,
|
|
||||||
OPENCLAW_HOME_VOLUME: "openclaw-home",
|
|
||||||
}),
|
|
||||||
stdio: ["ignore", "ignore", "pipe"],
|
|
||||||
});
|
});
|
||||||
expect(result.status).toBe(0);
|
expect(result.status).toBe(0);
|
||||||
const envFile = await readFile(join(sandbox.rootDir, ".env"), "utf8");
|
const envFile = await readFile(join(activeSandbox.rootDir, ".env"), "utf8");
|
||||||
expect(envFile).toContain("OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential");
|
expect(envFile).toContain("OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential");
|
||||||
expect(envFile).toContain("OPENCLAW_EXTRA_MOUNTS=");
|
expect(envFile).toContain("OPENCLAW_EXTRA_MOUNTS=");
|
||||||
expect(envFile).toContain("OPENCLAW_HOME_VOLUME=openclaw-home");
|
expect(envFile).toContain("OPENCLAW_HOME_VOLUME=openclaw-home");
|
||||||
const extraCompose = await readFile(join(sandbox.rootDir, "docker-compose.extra.yml"), "utf8");
|
const extraCompose = await readFile(
|
||||||
|
join(activeSandbox.rootDir, "docker-compose.extra.yml"),
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
expect(extraCompose).toContain("openclaw-home:/home/node");
|
expect(extraCompose).toContain("openclaw-home:/home/node");
|
||||||
expect(extraCompose).toContain("volumes:");
|
expect(extraCompose).toContain("volumes:");
|
||||||
expect(extraCompose).toContain("openclaw-home:");
|
expect(extraCompose).toContain("openclaw-home:");
|
||||||
const log = await readFile(sandbox.logPath, "utf8");
|
const log = await readFile(activeSandbox.logPath, "utf8");
|
||||||
expect(log).toContain("--build-arg OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential");
|
expect(log).toContain("--build-arg OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects injected multiline OPENCLAW_EXTRA_MOUNTS values", async () => {
|
it("rejects injected multiline OPENCLAW_EXTRA_MOUNTS values", async () => {
|
||||||
if (!sandbox) {
|
const activeSandbox = requireSandbox(sandbox);
|
||||||
throw new Error("sandbox missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = spawnSync("bash", [sandbox.scriptPath], {
|
const result = runDockerSetup(activeSandbox, {
|
||||||
cwd: sandbox.rootDir,
|
OPENCLAW_EXTRA_MOUNTS: "/tmp:/tmp\n evil-service:\n image: alpine",
|
||||||
env: createEnv(sandbox, {
|
|
||||||
OPENCLAW_EXTRA_MOUNTS: "/tmp:/tmp\n evil-service:\n image: alpine",
|
|
||||||
}),
|
|
||||||
encoding: "utf8",
|
|
||||||
stdio: ["ignore", "ignore", "pipe"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.status).not.toBe(0);
|
expect(result.status).not.toBe(0);
|
||||||
@@ -156,17 +165,10 @@ describe("docker-setup.sh", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("rejects invalid OPENCLAW_EXTRA_MOUNTS mount format", async () => {
|
it("rejects invalid OPENCLAW_EXTRA_MOUNTS mount format", async () => {
|
||||||
if (!sandbox) {
|
const activeSandbox = requireSandbox(sandbox);
|
||||||
throw new Error("sandbox missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = spawnSync("bash", [sandbox.scriptPath], {
|
const result = runDockerSetup(activeSandbox, {
|
||||||
cwd: sandbox.rootDir,
|
OPENCLAW_EXTRA_MOUNTS: "bad mount spec",
|
||||||
env: createEnv(sandbox, {
|
|
||||||
OPENCLAW_EXTRA_MOUNTS: "bad mount spec",
|
|
||||||
}),
|
|
||||||
encoding: "utf8",
|
|
||||||
stdio: ["ignore", "ignore", "pipe"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.status).not.toBe(0);
|
expect(result.status).not.toBe(0);
|
||||||
@@ -174,17 +176,10 @@ describe("docker-setup.sh", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("rejects invalid OPENCLAW_HOME_VOLUME names", async () => {
|
it("rejects invalid OPENCLAW_HOME_VOLUME names", async () => {
|
||||||
if (!sandbox) {
|
const activeSandbox = requireSandbox(sandbox);
|
||||||
throw new Error("sandbox missing");
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = spawnSync("bash", [sandbox.scriptPath], {
|
const result = runDockerSetup(activeSandbox, {
|
||||||
cwd: sandbox.rootDir,
|
OPENCLAW_HOME_VOLUME: "bad name",
|
||||||
env: createEnv(sandbox, {
|
|
||||||
OPENCLAW_HOME_VOLUME: "bad name",
|
|
||||||
}),
|
|
||||||
encoding: "utf8",
|
|
||||||
stdio: ["ignore", "ignore", "pipe"],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.status).not.toBe(0);
|
expect(result.status).not.toBe(0);
|
||||||
|
|||||||
@@ -1,6 +1,46 @@
|
|||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { createBoundDeliveryRouter } from "./bound-delivery-router.js";
|
import { createBoundDeliveryRouter } from "./bound-delivery-router.js";
|
||||||
import { __testing, registerSessionBindingAdapter } from "./session-binding-service.js";
|
import {
|
||||||
|
__testing,
|
||||||
|
registerSessionBindingAdapter,
|
||||||
|
type SessionBindingRecord,
|
||||||
|
} from "./session-binding-service.js";
|
||||||
|
|
||||||
|
const TARGET_SESSION_KEY = "agent:main:subagent:child";
|
||||||
|
|
||||||
|
function createDiscordBinding(
|
||||||
|
targetSessionKey: string,
|
||||||
|
conversationId: string,
|
||||||
|
boundAt: number,
|
||||||
|
parentConversationId?: string,
|
||||||
|
): SessionBindingRecord {
|
||||||
|
return {
|
||||||
|
bindingId: `runtime:${conversationId}`,
|
||||||
|
targetSessionKey,
|
||||||
|
targetKind: "subagent",
|
||||||
|
conversation: {
|
||||||
|
channel: "discord",
|
||||||
|
accountId: "runtime",
|
||||||
|
conversationId,
|
||||||
|
parentConversationId,
|
||||||
|
},
|
||||||
|
status: "active",
|
||||||
|
boundAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerDiscordSessionBindings(
|
||||||
|
targetSessionKey: string,
|
||||||
|
bindings: SessionBindingRecord[],
|
||||||
|
): void {
|
||||||
|
registerSessionBindingAdapter({
|
||||||
|
channel: "discord",
|
||||||
|
accountId: "runtime",
|
||||||
|
listBySession: (requestedSessionKey) =>
|
||||||
|
requestedSessionKey === targetSessionKey ? bindings : [],
|
||||||
|
resolveByConversation: () => null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
describe("bound delivery router", () => {
|
describe("bound delivery router", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -8,33 +48,13 @@ describe("bound delivery router", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("resolves to a bound destination when a single active binding exists", () => {
|
it("resolves to a bound destination when a single active binding exists", () => {
|
||||||
registerSessionBindingAdapter({
|
registerDiscordSessionBindings(TARGET_SESSION_KEY, [
|
||||||
channel: "discord",
|
createDiscordBinding(TARGET_SESSION_KEY, "thread-1", 1, "parent-1"),
|
||||||
accountId: "runtime",
|
]);
|
||||||
listBySession: (targetSessionKey) =>
|
|
||||||
targetSessionKey === "agent:main:subagent:child"
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
bindingId: "runtime:thread-1",
|
|
||||||
targetSessionKey,
|
|
||||||
targetKind: "subagent",
|
|
||||||
conversation: {
|
|
||||||
channel: "discord",
|
|
||||||
accountId: "runtime",
|
|
||||||
conversationId: "thread-1",
|
|
||||||
parentConversationId: "parent-1",
|
|
||||||
},
|
|
||||||
status: "active",
|
|
||||||
boundAt: 1,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
resolveByConversation: () => null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = createBoundDeliveryRouter().resolveDestination({
|
const route = createBoundDeliveryRouter().resolveDestination({
|
||||||
eventKind: "task_completion",
|
eventKind: "task_completion",
|
||||||
targetSessionKey: "agent:main:subagent:child",
|
targetSessionKey: TARGET_SESSION_KEY,
|
||||||
requester: {
|
requester: {
|
||||||
channel: "discord",
|
channel: "discord",
|
||||||
accountId: "runtime",
|
accountId: "runtime",
|
||||||
@@ -67,44 +87,14 @@ describe("bound delivery router", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("fails closed when multiple bindings exist without requester signal", () => {
|
it("fails closed when multiple bindings exist without requester signal", () => {
|
||||||
registerSessionBindingAdapter({
|
registerDiscordSessionBindings(TARGET_SESSION_KEY, [
|
||||||
channel: "discord",
|
createDiscordBinding(TARGET_SESSION_KEY, "thread-1", 1),
|
||||||
accountId: "runtime",
|
createDiscordBinding(TARGET_SESSION_KEY, "thread-2", 2),
|
||||||
listBySession: (targetSessionKey) =>
|
]);
|
||||||
targetSessionKey === "agent:main:subagent:child"
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
bindingId: "runtime:thread-1",
|
|
||||||
targetSessionKey,
|
|
||||||
targetKind: "subagent",
|
|
||||||
conversation: {
|
|
||||||
channel: "discord",
|
|
||||||
accountId: "runtime",
|
|
||||||
conversationId: "thread-1",
|
|
||||||
},
|
|
||||||
status: "active",
|
|
||||||
boundAt: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
bindingId: "runtime:thread-2",
|
|
||||||
targetSessionKey,
|
|
||||||
targetKind: "subagent",
|
|
||||||
conversation: {
|
|
||||||
channel: "discord",
|
|
||||||
accountId: "runtime",
|
|
||||||
conversationId: "thread-2",
|
|
||||||
},
|
|
||||||
status: "active",
|
|
||||||
boundAt: 2,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
resolveByConversation: () => null,
|
|
||||||
});
|
|
||||||
|
|
||||||
const route = createBoundDeliveryRouter().resolveDestination({
|
const route = createBoundDeliveryRouter().resolveDestination({
|
||||||
eventKind: "task_completion",
|
eventKind: "task_completion",
|
||||||
targetSessionKey: "agent:main:subagent:child",
|
targetSessionKey: TARGET_SESSION_KEY,
|
||||||
failClosed: true,
|
failClosed: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -114,4 +104,49 @@ describe("bound delivery router", () => {
|
|||||||
reason: "ambiguous-without-requester",
|
reason: "ambiguous-without-requester",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("selects requester-matching conversation when multiple bindings exist", () => {
|
||||||
|
registerDiscordSessionBindings(TARGET_SESSION_KEY, [
|
||||||
|
createDiscordBinding(TARGET_SESSION_KEY, "thread-1", 1),
|
||||||
|
createDiscordBinding(TARGET_SESSION_KEY, "thread-2", 2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const route = createBoundDeliveryRouter().resolveDestination({
|
||||||
|
eventKind: "task_completion",
|
||||||
|
targetSessionKey: TARGET_SESSION_KEY,
|
||||||
|
requester: {
|
||||||
|
channel: "discord",
|
||||||
|
accountId: "runtime",
|
||||||
|
conversationId: "thread-2",
|
||||||
|
},
|
||||||
|
failClosed: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(route.mode).toBe("bound");
|
||||||
|
expect(route.reason).toBe("requester-match");
|
||||||
|
expect(route.binding?.conversation.conversationId).toBe("thread-2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back for invalid requester conversation values", () => {
|
||||||
|
registerDiscordSessionBindings(TARGET_SESSION_KEY, [
|
||||||
|
createDiscordBinding(TARGET_SESSION_KEY, "thread-1", 1),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const route = createBoundDeliveryRouter().resolveDestination({
|
||||||
|
eventKind: "task_completion",
|
||||||
|
targetSessionKey: TARGET_SESSION_KEY,
|
||||||
|
requester: {
|
||||||
|
channel: "discord",
|
||||||
|
accountId: "runtime",
|
||||||
|
conversationId: " ",
|
||||||
|
},
|
||||||
|
failClosed: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(route).toEqual({
|
||||||
|
binding: null,
|
||||||
|
mode: "fallback",
|
||||||
|
reason: "invalid-requester",
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,6 +34,12 @@ async function writeJsonFixture(root: string, relativePath: string, value: unkno
|
|||||||
await fs.writeFile(filePath, JSON.stringify(value), "utf-8");
|
await fs.writeFile(filePath, JSON.stringify(value), "utf-8");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function expectVersionMetadataToBeMissing(moduleUrl: string) {
|
||||||
|
expect(readVersionFromPackageJsonForModuleUrl(moduleUrl)).toBeNull();
|
||||||
|
expect(readVersionFromBuildInfoForModuleUrl(moduleUrl)).toBeNull();
|
||||||
|
expect(resolveVersionFromModuleUrl(moduleUrl)).toBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
describe("version resolution", () => {
|
describe("version resolution", () => {
|
||||||
it("resolves package version from nested dist/plugin-sdk module URL", async () => {
|
it("resolves package version from nested dist/plugin-sdk module URL", async () => {
|
||||||
await withTempDir(async (root) => {
|
await withTempDir(async (root) => {
|
||||||
@@ -69,9 +75,7 @@ describe("version resolution", () => {
|
|||||||
it("returns null when no version metadata exists", async () => {
|
it("returns null when no version metadata exists", async () => {
|
||||||
await withTempDir(async (root) => {
|
await withTempDir(async (root) => {
|
||||||
const moduleUrl = await ensureModuleFixture(root);
|
const moduleUrl = await ensureModuleFixture(root);
|
||||||
expect(readVersionFromPackageJsonForModuleUrl(moduleUrl)).toBeNull();
|
expectVersionMetadataToBeMissing(moduleUrl);
|
||||||
expect(readVersionFromBuildInfoForModuleUrl(moduleUrl)).toBeNull();
|
|
||||||
expect(resolveVersionFromModuleUrl(moduleUrl)).toBeNull();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -80,9 +84,7 @@ describe("version resolution", () => {
|
|||||||
await writeJsonFixture(root, "package.json", { name: "other-package", version: "9.9.9" });
|
await writeJsonFixture(root, "package.json", { name: "other-package", version: "9.9.9" });
|
||||||
await writeJsonFixture(root, "build-info.json", { version: " " });
|
await writeJsonFixture(root, "build-info.json", { version: " " });
|
||||||
const moduleUrl = await ensureModuleFixture(root);
|
const moduleUrl = await ensureModuleFixture(root);
|
||||||
expect(readVersionFromPackageJsonForModuleUrl(moduleUrl)).toBeNull();
|
expectVersionMetadataToBeMissing(moduleUrl);
|
||||||
expect(readVersionFromBuildInfoForModuleUrl(moduleUrl)).toBeNull();
|
|
||||||
expect(resolveVersionFromModuleUrl(moduleUrl)).toBeNull();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user