Verify external canvas agent delivery through both running applications in CI
creative workspace review checks / verify (pull_request) Has been cancelled
creative workspace review checks / verify (pull_request) Has been cancelled
This commit is contained in:
@@ -6,6 +6,8 @@ on:
|
||||
- 'apps/studio/**'
|
||||
- 'apps/waterfall-wiki/**'
|
||||
- '.gitea/workflows/creative-workspace.yml'
|
||||
- 'compose.creative-review.yml'
|
||||
- 'scripts/integration.sh'
|
||||
concurrency:
|
||||
group: creative-workspace-${{ gitea.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
@@ -27,6 +29,7 @@ jobs:
|
||||
docker build --target runtime -t "creative-wiki-review:${{ gitea.sha }}" .
|
||||
- name: Verify isolated PostgreSQL, S3 and browser integration
|
||||
env:
|
||||
STUDIO_REVIEW_IMAGE: creative-studio-review:${{ gitea.sha }}
|
||||
WATERFALL_WIKI_IMAGE: creative-wiki-review:${{ gitea.sha }}
|
||||
WATERFALL_WIKI_TEST_IMAGE: creative-wiki-tests:${{ gitea.sha }}
|
||||
GITEA_RUN_ID: ${{ gitea.run_id }}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
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();
|
||||
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.
|
||||
const 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:'生成连接凭据',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()}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {verifyCanvasWorkspace} from './canvas-integration';
|
||||
import {verifyCanvasAgentUI} from './canvas-ui-integration';
|
||||
import assert from 'node:assert/strict';
|
||||
import {test} from 'node:test';
|
||||
import {spawn} from 'node:child_process';
|
||||
@@ -34,6 +35,7 @@ test('real PostgreSQL + S3 notes, flat tags, resource sharing and video processi
|
||||
for(const route of ['/','/images','/videos','/audio','/articles','/motion','/library/import','/tools/doodle','/tools/skills','/tools/skills/street-people','/tools/skills/shadow-puppet'])assert.equal((await fetch(base+route)).status,200,route);
|
||||
await verifyEmailAccounts();
|
||||
await verifyCanvasWorkspace(base);
|
||||
if(process.env.VERIFY_CANVAS_UI==='1')await verifyCanvasAgentUI(base);
|
||||
await verifyGuidancePublishing(base);
|
||||
const workspaceEnv:NodeJS.ProcessEnv={...process.env,WORKSPACE_TEST_URL:base};
|
||||
delete workspaceEnv.NODE_TEST_CONTEXT;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Added only by isolated CI integration; no production ports or volumes.
|
||||
services:
|
||||
prompt-studio:
|
||||
image: ${STUDIO_REVIEW_IMAGE:?Build the Studio review image first}
|
||||
init: true
|
||||
environment:
|
||||
HOSTNAME: 0.0.0.0
|
||||
PORT: "3000"
|
||||
STUDIO_WIKI_ORIGIN: http://waterfall-wiki:3000
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
cap_drop: [ALL]
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/studio/api/health').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
|
||||
interval: 5s
|
||||
timeout: 8s
|
||||
retries: 24
|
||||
tests:
|
||||
environment:
|
||||
VERIFY_CANVAS_UI: "1"
|
||||
+10
-3
@@ -6,16 +6,23 @@ export COMPOSE_PROJECT_NAME="prompt-ci-${GITEA_RUN_ID:-$$}"
|
||||
export PG_PORT=0 S3_PORT=0 WEB_PORT=0 WEB_BIND_ADDRESS=127.0.0.1
|
||||
TEST_ENV=$(mktemp)
|
||||
chmod 0600 "$TEST_ENV"
|
||||
compose(){ docker compose -p "$COMPOSE_PROJECT_NAME" --env-file "$TEST_ENV" -f compose.yml "$@"; }
|
||||
cleanup(){ code=$?; if [ "$code" -ne 0 ]; then compose logs --tail 160 waterfall-wiki waterfall-wiki-worker; fi; compose down -v --remove-orphans; rm -f "$TEST_ENV"; }
|
||||
COMPOSE_FILES=(-f compose.yml)
|
||||
STUDIO_SERVICES=()
|
||||
if [[ -n "${STUDIO_REVIEW_IMAGE:-}" ]]; then
|
||||
COMPOSE_FILES+=(-f compose.creative-review.yml)
|
||||
STUDIO_SERVICES+=(prompt-studio)
|
||||
fi
|
||||
compose(){ docker compose -p "$COMPOSE_PROJECT_NAME" --env-file "$TEST_ENV" "${COMPOSE_FILES[@]}" "$@"; }
|
||||
cleanup(){ code=$?; if [ "$code" -ne 0 ]; then compose logs --tail 160 waterfall-wiki waterfall-wiki-worker "${STUDIO_SERVICES[@]}"; fi; compose down -v --remove-orphans; rm -f "$TEST_ENV"; }
|
||||
trap cleanup EXIT
|
||||
# Random credentials exist only for this disposable stack; no host Node required.
|
||||
docker run --rm --entrypoint node "$WATERFALL_WIKI_IMAGE" -e "const c=require('crypto');const hex=()=>c.randomBytes(24).toString('hex');console.log('POSTGRES_PASSWORD='+hex()+'\nS3_ACCESS_KEY='+hex()+'\nS3_SECRET_KEY='+hex())" > "$TEST_ENV"
|
||||
# Explicitly isolate legacy unauthenticated integration coverage; production fails closed.
|
||||
printf '\nAUTH_DISABLED_FOR_TESTS=true\n' >> "$TEST_ENV"
|
||||
if [[ -n "${STUDIO_REVIEW_IMAGE:-}" ]]; then printf '\nACCOUNT_PUBLIC_ORIGIN=http://waterfall-wiki:3000\n' >> "$TEST_ENV"; fi
|
||||
compose up -d --no-build --wait --wait-timeout 120 postgres objects
|
||||
compose run --rm --no-deps migrate
|
||||
compose up -d --no-build --wait --wait-timeout 150 waterfall-wiki waterfall-wiki-worker
|
||||
compose up -d --no-build --wait --wait-timeout 150 waterfall-wiki waterfall-wiki-worker "${STUDIO_SERVICES[@]}"
|
||||
# The test runner calls the actual non-root production web/worker containers.
|
||||
compose run --rm --no-deps tests
|
||||
compose exec -T waterfall-wiki node container-health.mjs
|
||||
|
||||
Reference in New Issue
Block a user