diff --git a/e2e/work-checkpoints.playwright.ts b/e2e/work-checkpoints.playwright.ts new file mode 100644 index 0000000..013b1b7 --- /dev/null +++ b/e2e/work-checkpoints.playwright.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@playwright/test' +import { readFileSync, readlinkSync } from 'node:fs' +import { createHash } from 'node:crypto' + +const manifestPath=process.env.WORK_CHECKPOINT_HTTP_MANIFEST +if(!manifestPath)throw new Error('Explicit task-owned mechanism instance manifest required') +const manifest=JSON.parse(readFileSync(manifestPath,'utf8')) +if(manifest.mode!=='mechanism'||!manifest.dataDir.includes('fast-slow-u2-u3-')||readlinkSync(`/proc/${manifest.pid}/exe`)!==manifest.binary||createHash('sha256').update(readFileSync(manifest.binary)).digest('hex')!==manifest.binarySHA256)throw new Error('Owned mechanism instance verification failed') + +async function setup(request:any){ + const project=await(await request.post(manifest.base+'/api/projects',{data:{name:'browser-'+Date.now()}})).json() + const conv=await(await request.post(manifest.base+`/api/projects/${project.id}/conversations`,{data:{title:'材料整理',profileId:'checkpoint-eval'}})).json() + const ids:string[]=[] + for(let i=0;i<3;i++){ + const res=await request.post(manifest.base+`/api/projects/${project.id}/uploads`,{multipart:{file:{name:`来源${i+1}.txt`,mimeType:'text/plain',buffer:Buffer.from('上线六月。验收条件需要补齐,预算50万元尚未审批。\n\n'+('整理记录说明:保留日期、金额、限制和待确认事项。\n\n').repeat(100))}}}) + expect(res.ok()).toBeTruthy();ids.push((await res.json()).artifactId) + } + const sent=await request.post(manifest.base+`/api/conversations/${conv.id}/messages`,{data:{text:'',attachmentIds:ids,requestId:'browser-'+Date.now()}}) + expect(sent.ok()).toBeTruthy() + const sendBody=await sent.json() + await expect.poll(async()=> (await(await request.get(manifest.base+`/api/runs/${sendBody.run.id}`)).json()).run.status).toBe('completed') + await expect.poll(async()=>{ + const out=await(await request.get(manifest.base+`/api/conversations/${conv.id}/cards`)).json() + return out.cards.find((c:any)=>c.task)?.task.executionState + }).toBe('completed') + return {project,conv,ids} +} +async function open(page:any,seed:any){ + await page.addInitScript(({id}:{id:string})=>{localStorage.setItem('as.conv',id);localStorage.setItem('as.ui',JSON.stringify({theme:'light',rightCollapsed:true,fontSize:14}))},{id:seed.conv.id}) + await page.goto('/') + await expect(page.locator('[data-conversation-work]')).toContainText('材料记载上线六月') +} +test('real HTTP task recovery, source navigation and changed-version limitation',async({page,request})=>{ + const seed=await setup(request);const errors:string[]=[];page.on('pageerror',e=>errors.push(e.message));await open(page,seed) + await expect(page.locator('[data-conversation-work]')).toContainText('验收条件尚需补齐') + await page.reload();await expect(page.locator('[data-conversation-work]')).toContainText('材料记载上线六月') + await page.getByRole('button',{name:'查看依据',exact:true}).first().click() + await page.getByRole('button',{name:/查看来源/}).first().click() + await expect(page.locator('.preview-pane')).toBeVisible();await expect(page.getByText('任务处理时引用的来源版本;下方是本项发现引用的片段')).toBeVisible() + const card=(await(await request.get(manifest.base+`/api/conversations/${seed.conv.id}/cards`)).json()).cards.find((c:any)=>c.task) + const source=card.task.findings[0].sources[0] + const update=await request.put(manifest.base+`/api/artifacts/${source.artifactId}/content`,{data:'上线七月。文件内容已经更新。',headers:{'Content-Type':'text/plain'}});expect(update.ok()).toBeTruthy() + const old=await request.get(manifest.base+`/api/artifacts/${source.artifactId}/content?expectedHash=${source.contentHash}`);expect(old.status()).toBe(409) + await page.reload();await expect(page.locator('[data-conversation-work]')).toContainText('依据文件已更新') + await expect(page.locator('[data-conversation-work] .acard-findings').first()).not.toContainText('材料记载上线六月') + expect(errors).toEqual([]) + if(process.env.WORK_CHECKPOINT_SCREENSHOT)await page.screenshot({path:process.env.WORK_CHECKPOINT_SCREENSHOT,fullPage:true}) +}) +test('background replay does not steal the current file, focus or viewport',async({page,request})=>{ + const seed=await setup(request);await open(page,seed) + await page.getByRole('button',{name:'查看依据',exact:true}).first().click();await page.getByRole('button',{name:/查看来源/}).first().click();await expect(page.locator('.preview-pane')).toBeVisible() + const tabs=await page.locator('.tab-strip').textContent() + const composer=page.locator('textarea').last();await composer.fill('正在记录自己的想法');await composer.focus() + const focus=await composer.evaluate((el:any)=>document.activeElement===el);expect(focus).toBe(true) + const sent=await request.post(manifest.base+`/api/conversations/${seed.conv.id}/messages`,{data:{text:'重点核对预算',attachmentIds:seed.ids,requestId:'another-'+Date.now()}});expect(sent.ok()).toBeTruthy() + await expect.poll(async()=>{ + const cards=(await(await request.get(manifest.base+`/api/conversations/${seed.conv.id}/cards`)).json()).cards + return cards.filter((c:any)=>c.task?.executionState==='completed').length + }).toBe(2) + await expect(composer).toBeFocused();await expect(composer).toHaveValue('正在记录自己的想法') + expect(await page.locator('.tab-strip').textContent()).toBe(tabs) +})