ai-chatbot/tests/routes/canvas-agent-project-tools.test.ts

48 lines
3.6 KiB
TypeScript

import { LEGACY_AGENT_TOOL_MAP } from "@/lib/ai/canvas-agent-runtime/legacy-tool-map";
import { expect, test } from "@playwright/test";
import { readFileSync, existsSync } from "node:fs";
import { PROJECT_AGENT_OPERATIONS, projectOperationAuthorized, projectOperationUrl, sanitizeProjectToolOutput } from "@/lib/ai/canvas-agent-runtime/project-operation-catalog";
import { shouldUseCanvasAgentRuntime } from "@/lib/ai/canvas-agent-runtime/routing";
import { getCanvasAgentAccessManifest } from "@/lib/ai/canvas-agent-runtime/capability-access";
test("business grant uses only fixed implemented route/method pairs and no management or secret endpoints", () => {
expect(new Set(PROJECT_AGENT_OPERATIONS.map(item=>item.id)).size).toBe(PROJECT_AGENT_OPERATIONS.length);
for (const op of PROJECT_AGENT_OPERATIONS) {
expect(op.path).not.toMatch(/\/admin\/|\/internal\/|\/jobs\/|\/auth\/|\/payment\/|access-keys|clawd-chat|agent-chat/);
const relative=op.path.slice('/api/'.length)+'/route.ts';
const path=['app/api/','app/(chat)/api/'].map(base=>base+relative).find(existsSync);
if (!path) { expect(op.id).toBe('smart-edit.get'); continue; }
expect(readFileSync(path,'utf8')).toMatch(new RegExp(`export\\s+(?:async\\s+)?function\\s+${op.method}\\b`));
}
});
test("video canvas and storyboard permissions are explicitly paused and never admitted through a forged mode", () => {
expect(getCanvasAgentAccessManifest().filter(item=>item.workspace==='video').every(item=>!item.authorized)).toBe(true);
expect(PROJECT_AGENT_OPERATIONS.filter(item=>item.group==='分镜任务').every(item=>!projectOperationAuthorized(item))).toBe(true);
expect(shouldUseCanvasAgentRuntime({ composerMode:'agent', workspaceVariant:'video', contextPayload:{ source:'canvas',projectId:'p' }})).toBe(false);
const route=readFileSync('app/api/canvas/agent-chat/route.ts','utf8');
expect(route).toContain('isZcodeWorkspaceAuthorized(ownedProject.workspaceType)');
});
test("operation URL substitution rejects path escapes and keeps query values encoded", () => {
const op=PROJECT_AGENT_OPERATIONS.find(item=>item.id==='flow-file.get')!;
expect(()=>projectOperationUrl(op,{id:'../../admin/keys'},{})).toThrow();
expect(()=>projectOperationUrl(op,{id:'a%2Fadmin'},{})).toThrow();
expect(projectOperationUrl(op,{id:'safe-id'},{keyword:'a&x=http://evil'})).toBe('/api/flow-board/files/safe-id?keyword=a%26x%3Dhttp%3A%2F%2Fevil');
});
test("nested model/storage secrets cannot be returned to the native model", () => {
expect(sanitizeProjectToolOutput({ data:[{ id:'m',apiKey:'secret', provider:{ SK:'hidden',AK:'hidden',secretAccessKey:'hidden',baseUrl:'https://safe' } }], headers:{ authorization:'secret' } })).toEqual({data:[{id:'m',provider:{baseUrl:'https://safe'}}]});
});
test("every legacy planner tool has a concrete native counterpart or explicit video pause", async () => {
const names=[...readFileSync('lib/ai/tools-registry.ts','utf8').matchAll(/name:\s*"([^"]+)"/g)].map(match=>match[1]);
expect(names).toHaveLength(26);
for (const name of names) expect(LEGACY_AGENT_TOOL_MAP[name],name).toBeTruthy();
expect(LEGACY_AGENT_TOOL_MAP.generateVideo).toBe('paused:video-authorization');
});
test("image workspace mode menus cannot bypass the paused video authorization", () => {
const manifest=getCanvasAgentAccessManifest();
for (const key of ["newVideoCanvas","workflowNodeLibrary","addVideoInput","chatModeVideoMenu","exportVideoCanvas"])
expect(manifest.find(item=>item.id===`canvas.ui.${key}`)).toMatchObject({workspace:'video',authorized:false});
expect(manifest.find(item=>item.id==='canvas.ui.saveCanvas')).toMatchObject({workspace:'canvas',authorized:true});
});