From bf89a1da2eac87960228ecbcc665d1723bc27701 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 18:10:04 +0800 Subject: [PATCH] docs(tests): clarify integration request counts and fixture lifecycle --- tests/headless/foundry-ai.integration.mjs | 2 ++ tests/headless/foundry.integration.mjs | 3 +++ tests/headless/proxy.integration.mjs | 3 ++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/headless/foundry-ai.integration.mjs b/tests/headless/foundry-ai.integration.mjs index 035799c..a17d367 100644 --- a/tests/headless/foundry-ai.integration.mjs +++ b/tests/headless/foundry-ai.integration.mjs @@ -32,6 +32,7 @@ export async function verifyFoundryAI({open,transport,rootId,imageId,assets,call // 通过受理操作 ID 找回对应 RunRecord,避免假设任务 ID 等于操作 ID。 const byOperation=id=>[...engine.getSnapshot().runtime.runs.values()].find(run=>run.operationId===id); // 记录本场景开始前的调用基数,末尾核对生成次数和图片复制是否引入额外上传。 + // calls 还含前面场景,先捕获基数;末尾 +4 是本函数四次显式受理,不把历史请求误算作重复启动。 const initialStarts=posts('/ai/runs').length,initialUploads=posts('/assets').length; // 服务端已提交受理后丢弃响应:重开必须查询原请求,保持原 body,不能第二次启动生成。 @@ -89,6 +90,7 @@ export async function verifyFoundryAI({open,transport,rootId,imageId,assets,call // 显示句柄是带引用的资源,即使字节或元数据断言失败也必须 release。 try{const bytes=Buffer.from(await original.arrayBuffer());assert.deepEqual([...bytes.subarray(0,8)],[137,80,78,71,13,10,26,10]);assert.equal(createHash('sha256').update(bytes).digest('hex'),metadata.digest);assert.equal(metadata.asset.width,3);assert.equal(metadata.asset.height,2);assert.ok((await readDisplay(display.url)).length>0);}finally{display.release();} // 深复制生成后的图片,等待复制作业完成后检查稳定 ID 映射;复用资源不应增加 RunRecord。 + // 复制只应复用已生成 assetId;既检查 RunRecord 数量,也检查实际受理/上传 POST 次数,防止隐藏的新任务。 const runCount=engine.getSnapshot().runtime.runs.size,copyId=await engine.imports.duplicate([{id:generated.id,incarnation:generated.incarnation}],{viewCanvasId:rootId});await engine.settle(); const copy=engine.imports.getJobs().get(copyId);assert.equal(copy.status,'completed');assert.equal(engine.getSnapshot().document.elements.get(copy.stableIds['element:'+generated.id]).data.assetId,generated.data.assetId); assert.equal(engine.getSnapshot().runtime.runs.size,runCount);assert.equal(posts('/ai/runs').length,initialStarts+4);assert.equal(posts('/assets').length,initialUploads);assert.equal((await control('stats')).executions,3); diff --git a/tests/headless/foundry.integration.mjs b/tests/headless/foundry.integration.mjs index 71b29ab..6f87a43 100644 --- a/tests/headless/foundry.integration.mjs +++ b/tests/headless/foundry.integration.mjs @@ -1,6 +1,7 @@ /** * 由 Foundry CI runner 的隔离 PostgreSQL/HTTP 集成环境调用。文档、迁移和资产写入使用真实服务;本地恢复存储及丢响应是测试替身。 * 执行前必须注入隔离服务身份与测试数据;本文件不启动数据库,也不在没有 fixture 时降级为本地服务。 + * 场景会迁移并写入 fixture,还会创建第二个 material;finally 只关闭引擎,远端测试数据由隔离环境统一清理。 * 依次覆盖迁移、未知回执恢复、分区复制/解散、增量加载期间导入与上传,以及跨项目资源采用;AI 场景由环境开关追加。 */ import assert from 'node:assert/strict'; @@ -53,6 +54,7 @@ assert.equal((await createCanvasMigration({...migrationOptions,createId:()=>asse assert.equal(calls.filter(call=>call.method==='POST'&&call.path.endsWith('/model/migrate')).length,migrationPosts);assert.equal(migrationRecords.size,0); // 直接查询原迁移回执,再以同 ID/body 重试并比较 identity_map,检验真实服务的幂等迁移结果。 const migrated=await transport({method:'GET',path:path+'/model/operations/'+migrationId});assert.equal(migrated.status,200);assert.equal(migrated.body.status,'applied'); + // 这是测试主动追加的一次同键幂等重试,与上面的“自动恢复不得新增 POST”计数断言分开。 const retried=await transport({method:'POST',path:path+'/model/migrate',body:migrationBody,requestId:migrationId});assert.deepEqual(retried.body.identity_map,migrated.body.identity_map); // 迁移完成后完整读取规范模型;后续引擎以此作为初始 ACK 基线。 let initial=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.equal(initial.status,'ready'); @@ -139,6 +141,7 @@ try{ await engine.close();now+=10_000;engine=await open({document:capture.document,versions:sourceVersions,loading});await engine.settle();assert.equal(engine.getSnapshot().runtime.ledger.requests.get(upload.id).phase,'resolved');assert.equal(engine.getSnapshot().runtime.ledger.requests.get(upload.id).body,upload.body);assert.equal(calls.filter(call=>call.method==='POST'&&call.path.endsWith('/assets')).length,uploadCount);assert.ok(calls.some(call=>call.method==='GET'&&call.path.endsWith('/assets/operations/'+upload.id))); live=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.equal(live.document.elements.get(imageId).data.assetId,imageResource.id);assert.equal(live.document.elements.get(copiedImage).data.assetId,imageResource.id); // 继续消费原迭代器直到 ready 和 EOF,再用原 token/版本完成 loading;旧尾部合并不能覆盖期间新增内容。 + // 原流仍代表最初捕获的服务版本,期间本地/远端已推进;complete 必须按捕获 token 合并,不能整份覆盖新编辑。 let terminal;for await(const chunk of stream){preview.update(chunk.value);if(chunk.value.status==='ready')terminal=chunk.value;}assert.ok(terminal,'real source never reached terminal and EOF'); assert.equal((await engine.loading.complete(terminal.document,sourceVersions,{sourceToken})).status,'applied');await engine.settle();live=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.ok(live.document.elements.has(engine.imports.getJobs().get(partialId).stableIds['element:partial-native']));assert.ok(live.document.elements.has(partialFrame));assert.ok(live.document.elements.has(tail.at(-1).id));assert.ok([...live.document.elements.values()].some(element=>element.structure==='connection'&&element.source.elementId===anchor.id&&element.target.elementId===partialFrame)); // 独立读取资源授权元数据及原字节,和本地资源记录、暂存文件比较,不仅检查图片元素的 assetId。 diff --git a/tests/headless/proxy.integration.mjs b/tests/headless/proxy.integration.mjs index 3c83833..d7edb62 100644 --- a/tests/headless/proxy.integration.mjs +++ b/tests/headless/proxy.integration.mjs @@ -70,7 +70,7 @@ try{ const assetReceipt=await fetch(`http://127.0.0.1:${port}/api/foundry/${receiptPath}`);assert.equal(assetReceipt.status,200);assert.deepEqual(await assetReceipt.json(),{id:'fixture'}); const metadata=await fetch(`http://127.0.0.1:${port}/api/foundry/${receiptPath.replace('/operations/','/metadata/')}`,{headers:{'X-Expected-Account':account,Cookie:'foundry_session=fixture-session'}});assert.equal(metadata.status,200);assert.deepEqual(await metadata.json(),{id:'fixture'});assert.match(metadata.headers.get('cache-control'),/no-store/);const metadataRequest=received.find(item=>item.path.includes('/assets/metadata/'));assert.equal(metadataRequest.headers['x-expected-account'],account);assert.equal(metadataRequest.headers.authorization,'Bearer fixture-session'); console.log('PASS asset metadata JSON uses the actual authenticated Next proxy without image-body validation or caching'); - // FormData 自带 boundary,检查代理保留 multipart 字节及 X-Resource-Id,而不自行重新编码文件。 + // FormData 自带 boundary;下面核对文件内容片段、Content-Type 和资源/幂等身份头,未逐字节比较整份 multipart 包。 const multipart=new FormData();multipart.append('image',new Blob(['immutable file'],{type:'image/png'}),'fixture.png'); const assetPath=receiptPath.split('/operations/')[0],resourceId='10000000-0000-4000-8000-000000000006'; const uploaded=await fetch(`http://127.0.0.1:${port}/api/foundry/${assetPath}`,{method:'POST',body:multipart,headers:{'X-Resource-Id':resourceId,'Idempotency-Key':key,'X-Expected-Account':account}});await uploaded.text();assert.equal(uploaded.status,200); @@ -91,6 +91,7 @@ try{ // 上游尾部仍挂起时读取第一块,随后释放并核对余下字节,验证端到端增量转发。 const streamed=await fetch(`http://127.0.0.1:${port}/api/foundry/${path.replace('/operations','')}`,{headers:{Accept:'application/vnd.foundry.canvas-model+ndjson;version=1'}}); assert.equal(streamed.headers.get('content-type'),'application/vnd.foundry.canvas-model+ndjson;version=1'); + // 尾部 gate 尚未放开就等待首块,才能区分增量转发和整条缓冲;随后放开并排空剩余 body。 const reader=streamed.body.getReader(),first=await reader.read();assert.equal(new TextDecoder().decode(first.value),streamPrefix);streamRelease();let rest='';while(true){const next=await reader.read();if(next.done)break;rest+=new TextDecoder().decode(next.value);}assert.equal(rest,streamTail);reader.releaseLock(); console.log('PASS negotiated canonical NDJSON and first chunk cross the actual Next proxy before the gated tail'); // 多余路径段应在代理 allowlist 被拒绝,不能发到上游再依赖上游返回错误。