test(handoff): P2 agent 兑现真后端 e2e + seed market agent fixture
global-setup #13:seed market 类型 agent(固定 agent_key)+ active version + work4 槽位绑定, 每轮幂等复位(子表先删再重建);供 market→agent 跨空间 handoff e2e(market→agent 资产物化未接, 此处为预置 fixture)。 handoff-agent.spec(真后端,反假绿): - 正路:bind-precheck(targetOwner=agent)→ createHandoff 签一次性 token → agent precheck (market_agent + token):后端 verify+consume+放宽接纳 market 类型 agent → bind 落槽位(slotRevision=2) - 负路:伪造 token 调 agent precheck → 后端 verify 拒(code≠0),不放宽、0 写 验证:V27 已应用共享 muse_slice_live(flyway "now at version v27",用户拍板应用);handoff-agent 2/2 passed(真 PG + 真 token + V27 真列);全量 e2e 50 passed(唯一 flaky=knowledge-disable-restore 全量时序、单跑 2/2 绿、用 KB id=1 与 handoff 用 work4 无关,非 P2 回归)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4c3e8bb905
commit
2a7906c74e
@ -273,7 +273,40 @@ async function globalSetup(): Promise<void> {
|
||||
"DELETE FROM muse_knowledge_source_binding_projection WHERE tenant_id=1 AND work_id=4 AND kb_id=1 AND source_type='market_kb'"
|
||||
);
|
||||
|
||||
console.log(`[e2e globalSetup] 已复位每轮 fixture(graph/confirm-draft/accept/security-ack/appeal/binding/meta-schema/block/kb-status/installed-kb/handoff-bind);清理 e2e 累积 agent ${agentCleanup.rowCount} 条`);
|
||||
// 13) handoff-agent(P2):seed market 类型 agent + active version + work4 槽位绑定,供 market→agent 跨空间 handoff e2e。
|
||||
// market agent 用固定 agent_key 便于 spec 查其 id 作 sourceAgentId;requireVisibleActiveSourceAgent 校验 agent/version
|
||||
// active(放宽只跳 agentType 可见性、不放宽 active 态)。幂等:先删子表(slot binding/version)再删 agent 再插。
|
||||
// 注:market→agent 资产物化(install 自动建)未接,此处为 e2e 预置 fixture(见进度账"资产物化维度"订正)。
|
||||
const HANDOFF_AGENT_KEY = 'market:handoff:e2e';
|
||||
const HANDOFF_SLOT_KEY = 'handoff-writer';
|
||||
await client.query('DELETE FROM muse_agent_slot_binding WHERE tenant_id=1 AND work_id=4 AND slot_key=$1', [
|
||||
HANDOFF_SLOT_KEY,
|
||||
]);
|
||||
await client.query(
|
||||
'DELETE FROM muse_agent_version WHERE tenant_id=1 AND agent_id IN (SELECT id FROM muse_agent WHERE tenant_id=1 AND agent_key=$1)',
|
||||
[HANDOFF_AGENT_KEY]
|
||||
);
|
||||
await client.query('DELETE FROM muse_agent WHERE tenant_id=1 AND agent_key=$1', [HANDOFF_AGENT_KEY]);
|
||||
const mktAgent = await client.query(
|
||||
`INSERT INTO muse_agent (agent_key, name, agent_type, status, tenant_id)
|
||||
VALUES ($1,'活体市场智能体-handoff','market','active',1) RETURNING id`,
|
||||
[HANDOFF_AGENT_KEY]
|
||||
);
|
||||
const mktAgentId = mktAgent.rows[0].id as number;
|
||||
const mktVersion = await client.query(
|
||||
`INSERT INTO muse_agent_version (agent_id, version, status, tenant_id)
|
||||
VALUES ($1,'1','active',1) RETURNING id`,
|
||||
[mktAgentId]
|
||||
);
|
||||
await client.query('UPDATE muse_agent SET current_version_id=$1 WHERE id=$2', [mktVersion.rows[0].id, mktAgentId]);
|
||||
// work4 槽位初始绑现有 agent1(被兑现替换成 market agent);precheck requireSlotBinding 需现有 binding。
|
||||
await client.query(
|
||||
`INSERT INTO muse_agent_slot_binding (work_id, slot_key, agent_id, agent_version, status, revision, tenant_id)
|
||||
VALUES (4,$1,1,'1','active',1,1)`,
|
||||
[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} 条`);
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
|
||||
133
muse-studio/e2e/handoff-agent.spec.ts
Normal file
133
muse-studio/e2e/handoff-agent.spec.ts
Normal file
@ -0,0 +1,133 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { Client } from 'pg';
|
||||
|
||||
/**
|
||||
* 跨空间 handoff → agent 兑现端到端 e2e(真实后端,反假绿)。
|
||||
*
|
||||
* 前置:真实 muse-server 48080(含 P2 放宽 + V27 迁移)、muse_slice_live;global-setup #13 seed 了
|
||||
* market 类型 agent(agent_key=market:handoff:e2e)+ active version + work4 槽位绑定(handoff-writer)。
|
||||
*
|
||||
* 正路(API 全链):bind-precheck 拿授权摘要 → createHandoff(targetOwner=agent)签一次性 token →
|
||||
* agent precheck(sourceType=market_agent + token):后端 verify(targetOwner=agent)+consume+放宽接纳 market 类型 agent
|
||||
* → bind 落槽位绑定。证 P2 放宽红线端到端真生效(真 PG + 真 token)。
|
||||
* 负路(API 直打):伪造 handoffToken 调 agent precheck → 后端 verify 拒(code≠0,AI_MARKET_HANDOFF_UNAVAILABLE)。
|
||||
* 证"不信客户端 token"红线在 agent 兑现侧后端真拦截。
|
||||
*/
|
||||
const API = 'http://localhost:48080/app-api/muse';
|
||||
const AUTH = { Authorization: 'Bearer test1', 'tenant-id': '1', 'X-API-Version': '1' };
|
||||
const WORK_ID = 4;
|
||||
const SLOT_KEY = 'handoff-writer';
|
||||
const AGENT_KEY = 'market:handoff:e2e';
|
||||
|
||||
/** 查 global-setup #13 seed 的 market agent id(固定 agent_key);agent id 为 identity 自增、跑前查。 */
|
||||
async function seedMarketAgentId(): Promise<number> {
|
||||
const c = new Client({
|
||||
host: process.env.MUSE_POSTGRES_HOST,
|
||||
port: Number(process.env.MUSE_POSTGRES_PORT ?? '5433'),
|
||||
database: process.env.MUSE_POSTGRES_DATABASE ?? 'muse_slice_live',
|
||||
user: process.env.MUSE_POSTGRES_USERNAME ?? 'root',
|
||||
password: process.env.MUSE_POSTGRES_PASSWORD,
|
||||
ssl: false,
|
||||
});
|
||||
await c.connect();
|
||||
try {
|
||||
const r = await c.query('SELECT id FROM muse_agent WHERE tenant_id=1 AND agent_key=$1', [AGENT_KEY]);
|
||||
if (r.rowCount === 0) throw new Error(`seed market agent(${AGENT_KEY})缺失,检查 global-setup #13`);
|
||||
return Number(r.rows[0].id);
|
||||
} finally {
|
||||
await c.end();
|
||||
}
|
||||
}
|
||||
|
||||
test('正路:市场 agent handoff 兑现到作品槽位(真实后端)', async ({ request }) => {
|
||||
const sourceAgentId = await seedMarketAgentId();
|
||||
|
||||
// 前提:asset 1 已授权(幂等 best-effort;bind-precheck 需授权存在)
|
||||
await request
|
||||
.post(`${API}/marketplace/assets/1/purchase`, { headers: AUTH, data: { commandId: `e2e-acq-${Date.now()}` } })
|
||||
.catch(() => undefined);
|
||||
|
||||
// 1. 来源侧 bind-precheck → authorizationSummaryId
|
||||
const bp = await request.post(`${API}/marketplace/assets/1/bind-precheck`, {
|
||||
headers: AUTH,
|
||||
data: { commandId: `e2e-bp-${Date.now()}`, targetOwner: 'agent', targetAction: 'bind', targetWorkId: WORK_ID },
|
||||
});
|
||||
const bpBody = await bp.json();
|
||||
expect(bpBody.code, `bind-precheck: ${JSON.stringify(bpBody)}`).toBe(0);
|
||||
const authorizationSummaryId = bpBody.data.authorizationSummaryId;
|
||||
|
||||
// 2. createHandoff(targetOwner=agent)签一次性 token
|
||||
const ho = await request.post(`${API}/marketplace/handoffs`, {
|
||||
headers: AUTH,
|
||||
data: {
|
||||
commandId: `e2e-ho-${Date.now()}`,
|
||||
assetId: '1',
|
||||
targetOwner: 'agent',
|
||||
targetAction: 'bind',
|
||||
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. agent precheck(market_agent + token):后端 verify(targetOwner=agent)+consume+放宽接纳 market 类型 agent
|
||||
const pc = await request.post(`${API}/works/${WORK_ID}/agent-slots/${SLOT_KEY}/prechecks`, {
|
||||
headers: AUTH,
|
||||
data: {
|
||||
commandId: `e2e-pc-${Date.now()}`,
|
||||
sourceType: 'market_agent',
|
||||
handoffToken: token,
|
||||
sourceAgentId,
|
||||
sourceAgentVersion: 1,
|
||||
expectedSlotRevision: 1,
|
||||
sourceId: '1',
|
||||
sourceVersion: 1,
|
||||
authorizationSummaryId: String(authorizationSummaryId),
|
||||
},
|
||||
});
|
||||
const pcBody = await pc.json();
|
||||
expect(pcBody.code, `agent precheck: ${JSON.stringify(pcBody)}`).toBe(0);
|
||||
const agentSlotPrecheckId = pcBody.data?.agentSlotPrecheckId;
|
||||
expect(agentSlotPrecheckId).toBeTruthy();
|
||||
|
||||
// 4. bind(只消费 precheckId,落槽位绑定)
|
||||
const bd = await request.post(`${API}/works/${WORK_ID}/agent-slots/${SLOT_KEY}/bind`, {
|
||||
headers: AUTH,
|
||||
data: {
|
||||
commandId: `e2e-bd-${Date.now()}`,
|
||||
agentSlotPrecheckId,
|
||||
sourceAgentId,
|
||||
sourceAgentVersion: 1,
|
||||
expectedSlotRevision: 1,
|
||||
},
|
||||
});
|
||||
const bdBody = await bd.json();
|
||||
expect(bdBody.code, `bind: ${JSON.stringify(bdBody)}`).toBe(0);
|
||||
expect(bdBody.data?.slotRevision).toBe(2);
|
||||
});
|
||||
|
||||
test('负路:伪造 handoffToken 调 agent precheck 被后端核验拒绝(真实后端 API)', async ({ request }) => {
|
||||
const sourceAgentId = await seedMarketAgentId();
|
||||
// 直打后端:伪造 token(从未由 Market 签发),后端 verify 应拒,不放宽、不落任何绑定。
|
||||
const resp = await request.post(`${API}/works/${WORK_ID}/agent-slots/${SLOT_KEY}/prechecks`, {
|
||||
headers: AUTH,
|
||||
data: {
|
||||
commandId: `e2e-forge-${Date.now()}`,
|
||||
sourceType: 'market_agent',
|
||||
handoffToken: 'handoff_forged_invalid_token_000000000000',
|
||||
sourceAgentId,
|
||||
sourceAgentVersion: 1,
|
||||
expectedSlotRevision: 1,
|
||||
sourceId: '1',
|
||||
sourceVersion: 1,
|
||||
authorizationSummaryId: '9001',
|
||||
},
|
||||
});
|
||||
// 后端核验拒绝:HTTP 4xx 或 CommonResult.code 非 0(AI_MARKET_HANDOFF_UNAVAILABLE)。
|
||||
const body = await resp.json().catch(() => ({ code: -1 }));
|
||||
expect(body.code).not.toBe(0);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user