First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { GatewayResponseError } from "@/lib/gateway/GatewayClient";
|
||||
import type { GatewayClient } from "@/lib/gateway/GatewayClient";
|
||||
import { updateGatewayAgentOverrides } from "@/lib/gateway/agentConfig";
|
||||
|
||||
describe("updateGatewayAgentOverrides", () => {
|
||||
it("writes additive alsoAllow entries for per-agent tools", async () => {
|
||||
const client = {
|
||||
call: vi.fn(async (method: string, params?: unknown) => {
|
||||
if (method === "config.get") {
|
||||
return {
|
||||
exists: true,
|
||||
hash: "cfg-additive-1",
|
||||
config: {
|
||||
agents: {
|
||||
list: [{ id: "agent-1", tools: { profile: "coding" } }],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method === "config.set") {
|
||||
const raw = (params as { raw?: string }).raw ?? "";
|
||||
const parsed = JSON.parse(raw) as {
|
||||
agents?: { list?: Array<{ id?: string; tools?: { profile?: string; alsoAllow?: string[]; deny?: string[] } }> };
|
||||
};
|
||||
const entry = parsed.agents?.list?.find((item) => item.id === "agent-1");
|
||||
expect(entry?.tools).toEqual({
|
||||
profile: "coding",
|
||||
alsoAllow: ["group:web", "group:runtime"],
|
||||
deny: ["group:fs"],
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
throw new Error(`unexpected method ${method}`);
|
||||
}),
|
||||
} as unknown as GatewayClient;
|
||||
|
||||
await updateGatewayAgentOverrides({
|
||||
client,
|
||||
agentId: "agent-1",
|
||||
overrides: {
|
||||
tools: {
|
||||
profile: "coding",
|
||||
alsoAllow: ["group:web", "group:web", " group:runtime "],
|
||||
deny: ["group:fs", "group:fs"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("drops legacy allow when writing additive alsoAllow", async () => {
|
||||
const client = {
|
||||
call: vi.fn(async (method: string, params?: unknown) => {
|
||||
if (method === "config.get") {
|
||||
return {
|
||||
exists: true,
|
||||
hash: "cfg-additive-2",
|
||||
config: {
|
||||
agents: {
|
||||
list: [{ id: "agent-1", tools: { profile: "coding", allow: ["group:web"] } }],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method === "config.set") {
|
||||
const raw = (params as { raw?: string }).raw ?? "";
|
||||
const parsed = JSON.parse(raw) as {
|
||||
agents?: {
|
||||
list?: Array<{
|
||||
id?: string;
|
||||
tools?: {
|
||||
profile?: string;
|
||||
allow?: string[];
|
||||
alsoAllow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
const entry = parsed.agents?.list?.find((item) => item.id === "agent-1");
|
||||
expect(entry?.tools).toEqual({
|
||||
profile: "coding",
|
||||
alsoAllow: ["group:runtime"],
|
||||
deny: ["group:fs"],
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
throw new Error(`unexpected method ${method}`);
|
||||
}),
|
||||
} as unknown as GatewayClient;
|
||||
|
||||
await updateGatewayAgentOverrides({
|
||||
client,
|
||||
agentId: "agent-1",
|
||||
overrides: {
|
||||
tools: {
|
||||
alsoAllow: ["group:runtime"],
|
||||
deny: ["group:fs"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves redacted non-agent fields when writing full config", async () => {
|
||||
const client = {
|
||||
call: vi.fn(async (method: string, params?: unknown) => {
|
||||
if (method === "config.get") {
|
||||
return {
|
||||
exists: true,
|
||||
hash: "cfg-redacted-1",
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
xai: {
|
||||
models: [{ id: "grok", maxTokens: "__OPENCLAW_REDACTED__" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "agent-1" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method === "config.set") {
|
||||
const raw = (params as { raw?: string }).raw ?? "";
|
||||
const parsed = JSON.parse(raw) as Record<string, unknown>;
|
||||
expect(parsed.models).toEqual({
|
||||
providers: {
|
||||
xai: {
|
||||
models: [{ id: "grok", maxTokens: "__OPENCLAW_REDACTED__" }],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(parsed.agents).toEqual({
|
||||
list: [
|
||||
{
|
||||
id: "agent-1",
|
||||
tools: {
|
||||
profile: "coding",
|
||||
alsoAllow: ["group:runtime"],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
throw new Error(`unexpected method ${method}`);
|
||||
}),
|
||||
} as unknown as GatewayClient;
|
||||
|
||||
await updateGatewayAgentOverrides({
|
||||
client,
|
||||
agentId: "agent-1",
|
||||
overrides: {
|
||||
tools: {
|
||||
profile: "coding",
|
||||
alsoAllow: ["group:runtime"],
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves concurrent config changes when config.set retries after stale hash", async () => {
|
||||
let configGetCount = 0;
|
||||
let configSetCount = 0;
|
||||
const callOrder: string[] = [];
|
||||
const client = {
|
||||
call: vi.fn(async (method: string, params?: unknown) => {
|
||||
callOrder.push(method);
|
||||
if (method === "config.get") {
|
||||
configGetCount += 1;
|
||||
if (configGetCount === 1) {
|
||||
return {
|
||||
exists: true,
|
||||
hash: "cfg-retry-1",
|
||||
config: {
|
||||
gateway: {
|
||||
reload: {
|
||||
mode: "hybrid",
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "agent-1" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
exists: true,
|
||||
hash: "cfg-retry-2",
|
||||
config: {
|
||||
gateway: {
|
||||
reload: {
|
||||
mode: "off",
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
list: [{ id: "agent-1" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method === "config.set") {
|
||||
configSetCount += 1;
|
||||
const payload = params as { raw?: string; baseHash?: string };
|
||||
const parsed = JSON.parse(payload.raw ?? "") as {
|
||||
gateway?: { reload?: { mode?: string } };
|
||||
agents?: {
|
||||
list?: Array<{
|
||||
id?: string;
|
||||
tools?: {
|
||||
profile?: string;
|
||||
alsoAllow?: string[];
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
if (configSetCount === 1) {
|
||||
expect(payload.baseHash).toBe("cfg-retry-1");
|
||||
expect(parsed.gateway?.reload?.mode).toBe("hybrid");
|
||||
throw new GatewayResponseError({
|
||||
code: "INVALID_REQUEST",
|
||||
message: "config changed since last load; re-run config.get and retry",
|
||||
});
|
||||
}
|
||||
expect(payload.baseHash).toBe("cfg-retry-2");
|
||||
expect(parsed.gateway?.reload?.mode).toBe("off");
|
||||
expect(parsed.agents?.list?.find((entry) => entry.id === "agent-1")?.tools).toEqual({
|
||||
profile: "coding",
|
||||
alsoAllow: ["group:web"],
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
throw new Error(`unexpected method ${method}`);
|
||||
}),
|
||||
} as unknown as GatewayClient;
|
||||
|
||||
await updateGatewayAgentOverrides({
|
||||
client,
|
||||
agentId: "agent-1",
|
||||
overrides: {
|
||||
tools: {
|
||||
profile: "coding",
|
||||
alsoAllow: ["group:web"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(configGetCount).toBe(2);
|
||||
expect(configSetCount).toBe(2);
|
||||
expect(callOrder).toEqual(["config.get", "config.set", "config.get", "config.set"]);
|
||||
});
|
||||
|
||||
it("omits baseHash when config does not exist yet", async () => {
|
||||
let configSetCount = 0;
|
||||
const client = {
|
||||
call: vi.fn(async (method: string, params?: unknown) => {
|
||||
if (method === "config.get") {
|
||||
return {
|
||||
exists: false,
|
||||
config: {
|
||||
agents: {
|
||||
list: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method === "config.set") {
|
||||
configSetCount += 1;
|
||||
const payload = params as { raw?: string; baseHash?: string };
|
||||
const parsed = JSON.parse(payload.raw ?? "") as {
|
||||
agents?: {
|
||||
list?: Array<{
|
||||
id?: string;
|
||||
tools?: {
|
||||
alsoAllow?: string[];
|
||||
deny?: string[];
|
||||
};
|
||||
}>;
|
||||
};
|
||||
};
|
||||
expect(payload.baseHash).toBeUndefined();
|
||||
expect(parsed.agents?.list?.find((entry) => entry.id === "agent-1")?.tools).toEqual({
|
||||
alsoAllow: ["group:runtime"],
|
||||
deny: ["group:fs"],
|
||||
});
|
||||
return { ok: true };
|
||||
}
|
||||
throw new Error(`unexpected method ${method}`);
|
||||
}),
|
||||
} as unknown as GatewayClient;
|
||||
|
||||
await updateGatewayAgentOverrides({
|
||||
client,
|
||||
agentId: "agent-1",
|
||||
overrides: {
|
||||
tools: {
|
||||
alsoAllow: ["group:runtime"],
|
||||
deny: ["group:fs"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(configSetCount).toBe(1);
|
||||
});
|
||||
|
||||
it("fails after a single stale-hash retry attempt", async () => {
|
||||
let configGetCount = 0;
|
||||
let configSetCount = 0;
|
||||
const client = {
|
||||
call: vi.fn(async (method: string) => {
|
||||
if (method === "config.get") {
|
||||
configGetCount += 1;
|
||||
return {
|
||||
exists: true,
|
||||
hash: `cfg-stale-${configGetCount}`,
|
||||
config: {
|
||||
agents: {
|
||||
list: [{ id: "agent-1" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
if (method === "config.set") {
|
||||
configSetCount += 1;
|
||||
throw new GatewayResponseError({
|
||||
code: "INVALID_REQUEST",
|
||||
message: "config changed since last load; re-run config.get and retry",
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected method ${method}`);
|
||||
}),
|
||||
} as unknown as GatewayClient;
|
||||
|
||||
await expect(
|
||||
updateGatewayAgentOverrides({
|
||||
client,
|
||||
agentId: "agent-1",
|
||||
overrides: {
|
||||
tools: {
|
||||
alsoAllow: ["group:runtime"],
|
||||
},
|
||||
},
|
||||
})
|
||||
).rejects.toBeInstanceOf(GatewayResponseError);
|
||||
|
||||
expect(configGetCount).toBe(2);
|
||||
expect(configSetCount).toBe(2);
|
||||
});
|
||||
|
||||
it("fails fast when both allow and alsoAllow are provided", async () => {
|
||||
const client = {
|
||||
call: vi.fn(),
|
||||
} as unknown as GatewayClient;
|
||||
|
||||
await expect(
|
||||
updateGatewayAgentOverrides({
|
||||
client,
|
||||
agentId: "agent-1",
|
||||
overrides: {
|
||||
tools: {
|
||||
allow: ["group:runtime"],
|
||||
alsoAllow: ["group:web"],
|
||||
},
|
||||
},
|
||||
})
|
||||
).rejects.toThrow("Agent tools overrides cannot set both allow and alsoAllow.");
|
||||
|
||||
expect(client.call).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user