test(handoff): P3 content asset_use 真后端 e2e + global-setup #14

handoff-content.spec(48080+真 PG+V28 真表):正路 bind-precheck(content)→
createHandoff(content/asset_use)→asset-use-precheck(后端 verify+consume)→asset-uses
落使用事实(单表转 consumed、assetUseId=precheckId);负路伪造 token 被后端 verify 拒(0 写)。
global-setup #14 每轮清 work4 asset_use precheck(asset_use 不绑实体、无需 seed fixture)。

验证:handoff-content 2 passed、全量 e2e 50→52(1 flaky knowledge-disable-restore 单跑绿、非回归)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-22 23:48:40 -07:00
parent 7bb15a5cf4
commit 26425b1755
2 changed files with 111 additions and 1 deletions

View File

@ -306,7 +306,11 @@ async function globalSetup(): Promise<void> {
[HANDOFF_SLOT_KEY]
);
console.log(`[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal/binding/meta-schema/block/kb-status/installed-kb/handoff-bind/handoff-agent);清理 e2e 累积 agent ${agentCleanup.rowCount}`);
// 14) handoff-content(P3):清理 e2e 正路兑现产生的 asset_use precheck(work4),供兑现闭环幂等重跑。
// asset_use 不绑目标实体(单表 precheck 即使用凭证),物理删释放 (tenant_id,command_id) uk;asset 1 + work4 无需额外 seed。
await client.query('DELETE FROM muse_content_work_asset_use_precheck WHERE tenant_id=1 AND work_id=4');
console.log(`[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal/binding/meta-schema/block/kb-status/installed-kb/handoff-bind/handoff-agent/handoff-content);清理 e2e 累积 agent ${agentCleanup.rowCount}`);
} finally {
await client.end();
}

View File

@ -0,0 +1,106 @@
import { expect, test } from '@playwright/test';
/**
* handoff content(asset_use) e2e(,绿)
*
* 前置:真实 muse-server 48080( P3 asset_use + V28 )muse_slice_live;global-setup #14 work4 asset_use precheck
*
* (API ):bind-precheck(targetOwner=content) createHandoff(content/asset_use) token
* asset-use-precheck(market_asset + token): verify(targetOwner=content)+consume asset-uses 使( consumed)
* P3 token 线( PG + token);asset_use seed fixture(sourceAssetId= asset)
* (API ): handoffToken asset-use-precheck verify (code0)线 content
*/
const API = 'http://localhost:48080/app-api/muse';
const AUTH = { Authorization: 'Bearer test1', 'tenant-id': '1', 'X-API-Version': '1' };
const WORK_ID = 4;
test('正路:市场资产 asset_use 兑现到作品(真实后端)', async ({ request }) => {
// 前提:asset 1 已授权(幂等 best-effort)
await request
.post(`${API}/marketplace/assets/1/purchase`, { headers: AUTH, data: { commandId: `e2e-acq-${Date.now()}` } })
.catch(() => undefined);
// 1. 来源侧 bind-precheck(targetOwner=content) → authorizationSummaryId
const bp = await request.post(`${API}/marketplace/assets/1/bind-precheck`, {
headers: AUTH,
data: { commandId: `e2e-bp-${Date.now()}`, targetOwner: 'content', targetAction: 'asset_use', targetWorkId: WORK_ID },
});
const bpBody = await bp.json();
expect(bpBody.code, `bind-precheck: ${JSON.stringify(bpBody)}`).toBe(0);
const authorizationSummaryId = bpBody.data.authorizationSummaryId;
const authorizationSnapshot = bpBody.data.authorizationSnapshot;
// 2. createHandoff(targetOwner=content/asset_use)签一次性 token
const ho = await request.post(`${API}/marketplace/handoffs`, {
headers: AUTH,
data: {
commandId: `e2e-ho-${Date.now()}`,
assetId: '1',
targetOwner: 'content',
targetAction: 'asset_use',
targetWorkId: WORK_ID,
authorizationSummaryId,
returnUrl: 'http://localhost/market',
},
});
const hoBody = await ho.json();
expect(hoBody.code, `createHandoff: ${JSON.stringify(hoBody)}`).toBe(0);
const token = hoBody.data?.handoffToken;
expect(token).toBeTruthy();
// 3. asset-use-precheck(market_asset + token):后端 verify(content/asset_use)+consume
const pc = await request.post(`${API}/works/${WORK_ID}/asset-use-prechecks`, {
headers: AUTH,
data: {
commandId: `e2e-pc-${Date.now()}`,
sourceType: 'market_asset',
handoffToken: token,
sourceId: '1',
sourceVersion: 1,
sourceStatus: 'available',
authorizationSummaryId: String(authorizationSummaryId),
authorizationSnapshotId: authorizationSnapshot ?? 'snap-1',
purposes: ['reference'],
},
});
const pcBody = await pc.json();
expect(pcBody.code, `asset-use-precheck: ${JSON.stringify(pcBody)}`).toBe(0);
const assetUsePrecheckId = pcBody.data?.assetUsePrecheckId;
expect(assetUsePrecheckId).toBeTruthy();
// 4. 读作品 revision(create 段乐观锁)
const workResp = await request.get(`${API}/works/${WORK_ID}`, { headers: AUTH });
const workBody = await workResp.json();
const expectedWorkRevision = typeof workBody.data?.revision === 'number' ? workBody.data.revision : 1;
// 5. asset-uses(凭 precheckId,落使用事实,单表转 consumed)
const au = await request.post(`${API}/works/${WORK_ID}/asset-uses`, {
headers: AUTH,
data: { commandId: `e2e-au-${Date.now()}`, assetUsePrecheckId, expectedWorkRevision },
});
const auBody = await au.json();
expect(auBody.code, `asset-uses: ${JSON.stringify(auBody)}`).toBe(0);
// 单表设计:assetUseId = precheckId(precheck 即事实载体)
expect(auBody.data?.assetUseId).toBe(assetUsePrecheckId);
});
test('负路:伪造 handoffToken 调 asset-use-precheck 被后端核验拒绝(真实后端 API)', async ({ request }) => {
// 直打后端:伪造 token(从未由 Market 签发),后端 verify 应拒,不落任何使用事实。
const resp = await request.post(`${API}/works/${WORK_ID}/asset-use-prechecks`, {
headers: AUTH,
data: {
commandId: `e2e-forge-${Date.now()}`,
sourceType: 'market_asset',
handoffToken: 'handoff_forged_invalid_token_000000000000',
sourceId: '1',
sourceVersion: 1,
sourceStatus: 'available',
authorizationSummaryId: '45',
authorizationSnapshotId: 'snap-1',
purposes: ['reference'],
},
});
// 后端核验拒绝:HTTP 4xx 或 CommonResult.code 非 0(CONTENT_ASSET_HANDOFF_UNAVAILABLE)。
const body = await resp.json().catch(() => ({ code: -1 }));
expect(body.code).not.toBe(0);
});