oh-my-muse/muse-studio/e2e/market-install.spec.ts
zizi 6dcabf69fb feat(solo): 收口治理入口并裁剪 studio 表面
完成 2.0.0 S4 的外部 AI、admin、account 与 solo SQL 收口,并推进 S5 studio 市场/handoff/旧编辑器入口裁剪。S5 的前端单测、构建与 route-only e2e 已留证;MSW-off 创作主线 e2e 仍因本地后端活体启动口径待修正,已在总账标为未完成验收。
2026-07-08 11:45:26 +08:00

128 lines
6.1 KiB
TypeScript

import { expect, test, type Page } from '@playwright/test';
import { Client } from 'pg';
test.skip(true, '2.0.0 S5 单人版已移除市场、handoff 或多用户前端入口,本 spec 隔离保留待恢复');
/**
* 市场资产安装 UI e2e(真实后端,MSW off,反假绿)。
*
* 验「修复用户可见假绿」全链:后端 enrich isAcquired/canInstall(此前裸写 false,致 MarketBrowse 安装按钮永不渲染)
* → 已获取资产在市场页真实渲染出「安装」按钮 → 点击经真实浏览器 UI → 真后端落 muse_market_installation → UI 态翻转。
*
* 反假绿四重锚点(任何一处假绿都会断言失败):
* 1) DB 起点:user1 对该资产无 installation(global-setup #16 每轮复位为「未获取、未安装」)。
* 2) 前置 purchase(page.request 真打 /purchase)后,市场卡片**真实渲染出安装按钮**——这正是修前恒缺失的按钮
* (修前 isAcquired 裸写 false → canInstall=asset.isAcquired&&… 恒 false → 按钮永不出现)。
* 3) 点击安装 → 真后端 install 返回 installed。
* 4) DB 终点:muse_market_installation 新增该 user+asset 的 installed 行;UI 状态文案翻转为「已安装」。
*
* 前置:vite VITE_API_MOCK=false、真实 muse-server 48080、muse_slice_live;
* global-setup #16 每轮 seed 固定 marker 的 listed agent 资产并复位 user1 在其上的授权/安装/命令。
* PG 凭据从环境变量读取(MUSE_POSTGRES_*,见 ~/.config/muse-repo/infra.env;绝不入库):
* 跑前 `set -a; . ~/.config/muse-repo/infra.env; set +a`。
*/
const TOKEN = 'test1';
const API = 'http://localhost:48080/app-api/muse';
const AUTH = { Authorization: 'Bearer test1', 'tenant-id': '1', 'X-API-Version': '1' };
/** global-setup #16 seed 的固定 marker 资产名(本 spec 与 global-setup 必须一致)。 */
const ASSET_NAME = '活体市场资产·安装e2e';
function pgClient(): Client {
return 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,
});
}
/** 读 global-setup seed 的安装 e2e 资产 id(按 marker 名定位)。 */
async function readAssetId(): Promise<number> {
const c = pgClient();
await c.connect();
try {
const r = await c.query(
'SELECT id FROM muse_market_asset WHERE tenant_id=1 AND name=$1 AND deleted=false',
[ASSET_NAME]
);
expect(r.rowCount, 'global-setup #16 应已 seed 安装 e2e 资产').toBeGreaterThan(0);
return Number(r.rows[0].id);
} finally {
await c.end();
}
}
/** 读 user1 对指定资产的 installed 安装行数(DB 核验:0=未安装,>=1=已安装)。 */
async function countInstallation(assetId: number): Promise<number> {
const c = pgClient();
await c.connect();
try {
const r = await c.query(
"SELECT count(*)::int AS n FROM muse_market_installation WHERE tenant_id=1 AND user_id=1 AND asset_id=$1 AND status='installed'",
[assetId]
);
return Number(r.rows[0].n);
} finally {
await c.end();
}
}
async function seedToken(page: Page): Promise<void> {
await page.addInitScript((t) => {
window.localStorage.setItem('accessToken', t);
window.localStorage.setItem('tenantId', '1');
}, TOKEN);
}
test('用户可经真实浏览器 UI 安装已获取的市场资产(真实后端,enrich 驱动安装按钮渲染)', async ({ page, request }) => {
const assetId = await readAssetId();
// 1. DB 起点:global-setup #16 已复位为未安装(反假绿基准,确保终点新增可归因于本次 UI 安装)。
expect(await countInstallation(assetId), 'bind 前 user1 对该资产应无 installed 安装行(global-setup 复位)').toBe(0);
// 2. 前置 purchase(真打后端造 active 授权)→ 使资产 isAcquired=true,这是安装按钮渲染的前置条件。
const purchaseResp = await request.post(`${API}/marketplace/assets/${assetId}/purchase`, {
headers: AUTH,
data: { commandId: `e2e-acq-${Date.now()}` },
});
expect(purchaseResp.status(), 'purchase HTTP').toBe(200);
expect((await purchaseResp.json()).code, 'purchase code').toBe(0);
// 3. 进市场页 → 筛选 agent(seed 资产是 agent 类型)。
await seedToken(page);
await page.goto('/market');
await page.getByRole('button', { name: '智能体', exact: true }).click();
// 4. 定位该资产卡片(按资产名),断言**安装按钮真实渲染出来**——修前 enrich 裸写 false 时此按钮永不出现。
const card = page.locator('article').filter({ hasText: ASSET_NAME });
await expect(card, '已获取资产的卡片应可见').toBeVisible({ timeout: 15_000 });
const installButton = card.getByRole('button', { name: '安装', exact: true });
await expect(installButton, '已获取未安装资产应渲染「安装」按钮(enrich isAcquired=true 驱动)').toBeVisible({
timeout: 15_000,
});
// 5. 点击安装 → 等真后端 install 返回 installed(非 mock 截获)。
const [installResp] = await Promise.all([
page.waitForResponse(
(r) => new RegExp(`/marketplace/assets/${assetId}/install$`).test(r.url()) && r.request().method() === 'POST',
{ timeout: 20_000 }
),
installButton.click(),
]);
expect(installResp.status(), 'install HTTP').toBe(200);
const installBody = await installResp.json();
expect(installBody.code, `install 返回: ${JSON.stringify(installBody)}`).toBe(0);
expect(installBody.data?.status, 'install status').toBe('installed');
// 6a. UI 态翻转:安装成功后卡片状态文案变「已安装」(useInstallMarketAsset onSuccess 失效列表 → 重查 enrich isInstalled=true)。
await expect(card.getByText('已安装', { exact: true }), '安装后卡片状态应翻转为「已安装」').toBeVisible({
timeout: 15_000,
});
// 6b. DB 终点核验:user1 对该资产新增 installed 行(证 UI 操作真落库,非 mock 假绿)。
expect(await countInstallation(assetId), '安装后 user1 应有 installed 安装行').toBeGreaterThanOrEqual(1);
});