Why: A failed task card can appear before its notification turn, and the second finding must be observed while work is still running. Evidence: The strengthened browser checks first exposed missing fixture notice text and a zero-width second-stage window; after fixture correction all six task-owned HTTP/Playwright scenarios passed. Frontend 265 unit tests and build passed. Impact: Test coverage only; no production UI change. Refs: U3 r0.4 independent review Important 5
111 lines
8.8 KiB
TypeScript
111 lines
8.8 KiB
TypeScript
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
|
|
test.skip(!manifestPath,'Run explicitly with a task-owned mechanism instance manifest')
|
|
const manifest=manifestPath?JSON.parse(readFileSync(manifestPath,'utf8')):null
|
|
if(manifest&&(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')
|
|
|
|
type Scenario='normal'|'slow-stages'|'zero-failure'|'partial-failure'|'choice'
|
|
async function setup(request:any,scenario:Scenario='normal',awaitTask=true){
|
|
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 marker=i===0&&scenario!=='normal'?`[fixture:${scenario}]\n`:''
|
|
const res=await request.post(manifest.base+`/api/projects/${project.id}/uploads`,{multipart:{file:{name:`来源${i+1}.txt`,mimeType:'text/plain',buffer:Buffer.from(marker+'上线六月。验收条件需要补齐,预算50万元尚未审批。交付包含12项需求,其中3项依赖外部接口。第一期先完成8项核心需求。\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')
|
|
if(awaitTask)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(scenario.endsWith('failure')?'failed':'completed')
|
|
return {project,conv,ids,sendRunId:sendBody.run.id}
|
|
}
|
|
async function waitForFailureNotice(request:any,seed:any){
|
|
await expect.poll(async()=>{
|
|
const response=await request.get(manifest.base+`/api/conversations/${seed.conv.id}`)
|
|
if(!response.ok())return false
|
|
const current=await response.json()
|
|
const children=current.runs.filter((r:any)=>r.kind==='subagent')
|
|
const notices=current.messages.filter((m:any)=>m.role==='system'&&m.parts?.some((p:any)=>p.type==='text'&&p.text?.includes('后台子代理「')))
|
|
const noticeRuns=current.runs.filter((r:any)=>r.kind==='conversation'&&r.id!==seed.sendRunId)
|
|
return children.length>0&¬ices.length>=children.length&¬iceRuns.length>=children.length&&
|
|
noticeRuns.every((r:any)=>['completed','failed','interrupted'].includes(r.status))
|
|
}).toBe(true)
|
|
}
|
|
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 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)
|
|
})
|
|
test('two sourced stages appear while the material task is still running',async({page,request})=>{
|
|
const seed=await setup(request,'slow-stages',false)
|
|
await open(page,seed)
|
|
const cards=async()=>(await(await request.get(manifest.base+`/api/conversations/${seed.conv.id}/cards`)).json()).cards
|
|
await expect.poll(async()=>{const card=(await cards()).find((c:any)=>c.task);return card?.task.executionState}).toBe('running')
|
|
await expect(page.locator('[data-conversation-work]')).toContainText('上线六月')
|
|
await expect.poll(async()=>{const card=(await cards()).find((c:any)=>c.task);return card?.task.executionState==='running'&&card.task.checkpointSeq>=2&&card.task.findings?.length>1}).toBe(true)
|
|
await expect(page.locator('[data-conversation-work]')).toContainText('验收条件需要补齐')
|
|
await expect.poll(async()=>{const card=(await cards()).find((c:any)=>c.task);return card?.task.executionState}).toBe('completed')
|
|
})
|
|
for(const scenario of ['zero-failure','partial-failure'] as const){
|
|
test(`${scenario} shows the saved scope without a deliverable choice`,async({page,request})=>{
|
|
const seed=await setup(request,scenario)
|
|
await waitForFailureNotice(request,seed)
|
|
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('/')
|
|
const work=page.locator('[data-conversation-work]')
|
|
await expect(work).toContainText('执行失败')
|
|
if(scenario==='zero-failure')await expect(work).toContainText('暂无阶段结论记录')
|
|
else await expect(work).toContainText('上线六月')
|
|
await expect(page.getByText('后台整理状态已回投工作卡;请按卡片中的结果继续。').last()).toBeVisible()
|
|
await expect(page.locator('.hitl-choice')).toHaveCount(0)
|
|
})
|
|
}
|
|
test('completed sourced work offers only its grounded suggestions',async({page,request})=>{
|
|
const seed=await setup(request,'choice')
|
|
await open(page,seed)
|
|
await expect(page.locator('.hitl-choice')).toHaveCount(3)
|
|
for(const label of ['会议摘要','验收清单','联调计划'])await expect(page.locator('.hitl-choice').filter({hasText:label})).toHaveCount(1)
|
|
await expect(page.locator('.hitl-card')).toContainText('基于整理结果,接下来生成什么?')
|
|
await page.reload()
|
|
await expect(page.locator('.hitl-choice')).toHaveCount(3)
|
|
})
|