85 lines
7.3 KiB
TypeScript
85 lines
7.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import {randomUUID} from 'node:crypto';
|
|
import {chromium} from 'playwright-core';
|
|
import {db} from '../src/lib/db';
|
|
import {newToken,tokenDigest,ACCOUNT_COOKIE} from '../src/lib/account-crypto';
|
|
|
|
export async function verifyCanvasAgentUI(base:string){
|
|
assert.equal(process.env.PROMPT_INTEGRATION_TEST,'1');
|
|
const owner=randomUUID(),session=newToken();
|
|
await db().query("INSERT INTO account_users(id,email,password_hash,verified_at) VALUES($1,$2,'test',now())",[owner,owner+'@example.com']);
|
|
await db().query("INSERT INTO account_tokens(digest,user_id,kind,expires_at) VALUES($1,$2,'session',now()+interval '1 hour')",[tokenDigest(session),owner]);
|
|
const browser=await chromium.launch({executablePath:process.env.CHROMIUM_PATH||'/usr/bin/chromium',headless:true,args:['--no-sandbox','--disable-dev-shm-usage']});
|
|
const context=await browser.newContext({viewport:{width:1440,height:1000}}),errors:string[]=[];
|
|
const ownerHeaders={cookie:ACCOUNT_COOKIE+'='+session};
|
|
try{
|
|
await context.addCookies([{name:ACCOUNT_COOKIE,value:session,url:base,httpOnly:true,sameSite:'Lax'}]);
|
|
const page=await context.newPage();page.setDefaultTimeout(20000);page.on('pageerror',e=>errors.push(e.message));
|
|
await page.goto(base+'/infinite_canvas',{waitUntil:'networkidle'});
|
|
await page.getByRole('button',{name:'从示例开始 ↗',exact:true}).click();
|
|
const shot=page.locator('[data-node-id="shot"] [data-node-content]');
|
|
await shot.click();await shot.click();
|
|
const composer=page.getByRole('region',{name:'节点编辑器',exact:true});
|
|
await composer.getByRole('textbox',{name:'创作要求',exact:true}).fill('根据参考资料写六个分镜');
|
|
await composer.getByRole('button',{name:'交给 Agent',exact:true}).click();
|
|
const panel=page.getByRole('region',{name:'Agent 连接',exact:true});
|
|
await panel.getByText('任务已创建,等待你的 coding agent 领取',{exact:true}).waitFor();
|
|
const list=await fetch(base+'/api/canvases',{headers:ownerHeaders}).then(r=>r.json());assert.equal(list.canvases.length,1);
|
|
const root='/api/canvases/'+list.canvases[0].id;
|
|
const state=await fetch(base+root,{headers:ownerHeaders}).then(r=>r.json());assert.equal(state.tasks.length,1);assert.equal(state.tasks[0].status,'queued');
|
|
await panel.getByRole('button',{name:'生成连接凭据',exact:true}).click();
|
|
const credentials=panel.getByRole('textbox',{name:'Agent 连接凭据',exact:true});await credentials.waitFor();
|
|
await panel.getByRole('button',{name:'复制给 coding agent',exact:true}).click();
|
|
await panel.getByRole('status').filter({hasText:/凭据已复制|已选中凭据/}).waitFor();
|
|
const first=JSON.parse(await credentials.inputValue());assert.equal(first.canvasUrl,base+root);
|
|
const agentHeaders={'Content-Type':'application/json',authorization:first.authorization};
|
|
const task=root+'/tasks/'+state.tasks[0].id;
|
|
const recipe=await fetch(base+task,{headers:agentHeaders}).then(r=>r.json());assert.equal(recipe.request.instruction,'根据参考资料写六个分镜');assert.equal(recipe.request.references[0].id,'reference');
|
|
const patch=async(body:unknown)=>fetch(base+task,{method:'PATCH',headers:agentHeaders,body:JSON.stringify(body)});
|
|
assert.equal((await patch({status:'running'})).status,200);
|
|
await panel.getByText(/Agent 正在处理/).waitFor();
|
|
// This fixture verifies the external-client protocol and UI delivery, not model quality.
|
|
let generated='第一镜:晨光穿过窗边的植物。';
|
|
assert.equal((await patch({status:'succeeded',result:{text:generated}})).status,200);
|
|
await page.locator('[data-node-id="shot"] [data-node-content]').getByText(generated,{exact:true}).waitFor();
|
|
const updated=await fetch(base+root,{headers:ownerHeaders}).then(r=>r.json());
|
|
assert.equal(updated.document.cards.find((n:{id:string})=>n.id==='shot').instruction,'根据参考资料写六个分镜');
|
|
await panel.getByRole('button',{name:'关闭 Agent 连接',exact:true}).click();
|
|
await shot.click();await shot.click();
|
|
await composer.getByRole('textbox',{name:'创作要求',exact:true}).fill('再写一个新镜头');
|
|
const queued=page.waitForResponse(r=>r.request().method()==='POST'&&new URL(r.url()).pathname===root+'/tasks');
|
|
await composer.getByRole('button',{name:'交给 Agent',exact:true}).click();
|
|
const queuedResponse=await queued;assert.equal(queuedResponse.status(),201);const secondTask=await queuedResponse.json();
|
|
await panel.getByRole('button',{name:'关闭 Agent 连接',exact:true}).click();
|
|
const brief=page.locator('[data-node-id="brief"] [data-node-content]');await brief.click();await brief.click();
|
|
const localText=page.getByRole('textbox',{name:'01 / 创意简报 内容',exact:true});await localText.fill('用户尚未同步的本机修改');
|
|
const nextTask=root+'/tasks/'+secondTask.id;
|
|
const nextPatch=(body:unknown)=>fetch(base+nextTask,{method:'PATCH',headers:agentHeaders,body:JSON.stringify(body)});
|
|
assert.equal((await nextPatch({status:'running'})).status,200);
|
|
generated='第二镜:城市在雨后醒来。';
|
|
assert.equal((await nextPatch({status:'succeeded',result:{text:generated}})).status,200);
|
|
await page.getByRole('button',{name:'Agent · 在线画布',exact:true}).click();
|
|
await panel.getByRole('button',{name:'载入在线版本',exact:true}).waitFor();
|
|
assert.equal(await localText.inputValue(),'用户尚未同步的本机修改','Polling must not replace unsaved local work');
|
|
await panel.getByRole('button',{name:'载入在线版本',exact:true}).click();
|
|
await page.locator('[data-node-id="shot"] [data-node-content]').getByText(generated,{exact:true}).waitFor();
|
|
assert.equal(await page.locator('[data-node-id="brief"] [data-node-content]').textContent(),state.document.cards.find((n:{id:string})=>n.id==='brief').text);
|
|
await panel.getByRole('button',{name:'生成连接凭据',exact:true}).click();
|
|
await page.waitForFunction(old=>{const field=document.querySelector<HTMLTextAreaElement>('textarea[aria-label="Agent 连接凭据"]');return !!field&&JSON.parse(field.value).authorization!==old},first.authorization);
|
|
const second=JSON.parse(await credentials.inputValue());assert.equal((await fetch(base+root,{headers:agentHeaders})).status,404);
|
|
await panel.getByRole('button',{name:'撤销连接',exact:true}).click();
|
|
await panel.getByText('Agent 访问已撤销',{exact:true}).waitFor();
|
|
assert.equal((await fetch(base+root,{headers:{authorization:second.authorization}})).status,404);
|
|
await page.setViewportSize({width:390,height:844});
|
|
const bounds=await panel.boundingBox();assert.ok(bounds&&bounds.x>=0&&bounds.x+bounds.width<=390,'Agent panel stays within the mobile viewport');
|
|
await page.reload({waitUntil:'networkidle'});
|
|
await page.getByRole('button',{name:'Agent',exact:true}).click();
|
|
const reopened=page.getByRole('region',{name:'Agent 连接',exact:true});
|
|
await reopened.getByRole('combobox',{name:'打开在线画布',exact:true}).selectOption(list.canvases[0].id);
|
|
await reopened.getByRole('button',{name:'载入在线版本',exact:true}).click();
|
|
await page.locator('[data-node-id="shot"] [data-node-content]').getByText(generated,{exact:true}).waitFor();
|
|
assert.deepEqual(errors,[]);
|
|
console.log('Canvas UI dispatched a real stored task, received an external-client fixture result, rotated/revoked access and restored its online document');
|
|
}finally{await context.close();await browser.close()}
|
|
}
|