test(studio-e2e): source-attribution 解耦到 pristine block3 根治测试间来源污染 flaky

source-attribution 只读 spec 期望 block1 来源=user_original,但全量串行时前序 accept-suggestion(写 ai_suggestion)/block-autosave/block-structure 按字母序先跑且都写 block1,block1 顶层 revision 来源在 user_original/ai_suggestion 漂移→偶发渲染"AI 生成"红;global-setup 每轮仅跑一次、无法撤销跑中途污染。修:spec 改读 pristine work3/block3(grep 确认无人导航 /workspace/3、无人写 block3)+global-setup §3 每轮删净 block3 归因并重建唯一 user_original rev1 行(幂等)。断言未放宽仍真断言 user_original。免疫验证:强灌 block1 顶层 ai_suggestion(旧 spec 必红条件)新 spec 仍绿;独立连跑 source-attribution 3 次全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-25 14:35:19 -07:00
parent de5f4d0bf9
commit ca0e04c9a9
2 changed files with 27 additions and 5 deletions

View File

@ -113,14 +113,28 @@ async function globalSetup(): Promise<void> {
await crudEnt('crudshiti-yi');
await crudEnt('crudshiti-bing');
// 3) accept-suggestion:复位 block3→rev1 + 清来源归因;suggestion3 复 pending/active(spec 硬编码 work3/block3/suggestion3)。
// 3) accept-suggestion + source-attribution 只读 fixture:复位 block3(work3)→rev1 + 重建来源归因为 user_original。
// WHY(source-attribution 反污染,根治偶发红):source-attribution.spec 读「某 Block 当前 revision 的来源」并断言
// 「用户原创」。它原指向 /workspace/1(=block1),但 block1 被 accept-suggestion(写 ai_suggestion 来源)、
// block-autosave、block-structure 三个 spec 在前序消费(三者按字母序均先于 source-attribution 跑),其当前 revision
// 的 source_type 由「最后一个写 block1 的 spec」决定 → 非确定(实测 block1 顶层 revision 在 user_original/ai_suggestion
// 间漂移,顶层落 ai_suggestion 时 source-attribution 渲染「AI 生成」而非「用户原创」→ 偶发红)。globalSetup 每轮只跑
// 一次(在所有 spec 之前),无法撤销 spec 跑中途的污染,故根治法是让 source-attribution 改读「无任何 spec 会写」的
// pristine Block:work3/block3(grep 全 spec 确认无人导航 /workspace/3、无人写 block3;此处每轮把 block3 复位 rev1 +
// 重建唯一一条 user_original 来源,使该读断言恒确定真)。block3 的 user_original 是真实初态(用户原创正文),断言未放宽。
await client.query('DELETE FROM muse_content_block_source_attribution WHERE tenant_id=1 AND block_id=3');
const blk = await client.query(
"UPDATE muse_content_block SET revision=1, content_text='活体原始正文待采纳', word_count=9 WHERE tenant_id=1 AND id=3"
);
// 重建 block3 rev1 的 user_original 来源(source-attribution.spec 读此并断言「用户原创」);幂等:上面已先删 block3 全部归因。
await client.query(
`INSERT INTO muse_content_block_source_attribution
(work_id, block_id, revision, source_type, source_object_id, source_status, tenant_id)
VALUES (3,3,1,'user_original','3','active',1)`
);
await client.query("UPDATE muse_ai_suggestion SET status='pending', source_status='active' WHERE tenant_id=1 AND id=3");
if (blk.rowCount === 0) {
console.warn('[e2e globalSetup] ⚠️ 未找到 block id=3(accept-suggestion 基础种子缺失,需先建基础数据)');
console.warn('[e2e globalSetup] ⚠️ 未找到 block id=3(source-attribution 只读 fixture 需基础种子,需先建基础数据)');
}
// 4) account-security-ack:把"待确认演练"事件复位为未确认 + 清其确认记录(已确认态由 ack 记录派生)。

View File

@ -3,10 +3,18 @@ import { expect, test, type Page } from '@playwright/test';
/**
* Block (,MSW off)
*
* E 链路: work1 workspace Tab GET source-attribution
* (work1 block user_original ),,
* E 链路: work3 workspace Tab GET source-attribution
* (work3/block3 user_original ),,
*
* WHY work3 work1():block revision = work1/block1,
* block accept-suggestion( ai_suggestion )block-autosaveblock-structure spec (
* spec), revision source_type block1 spec , ai_suggestion
* AI work3/block3:无任何 spec /workspace/3 block3(grep spec ),globalSetup §3
* block3 rev1 + user_original ,使(,)
*/
const TOKEN = 'test1';
/** 读取来源归因的目标作品:work3/block3 是无任何 spec 写入的 pristine Block(见上文 WHY),globalSetup §3 每轮复位其来源。 */
const WORK_ID = 3;
async function seedToken(page: Page): Promise<void> {
await page.addInitScript((t) => {
@ -18,7 +26,7 @@ async function seedToken(page: Page): Promise<void> {
test('用户查看章节正文来源归因(真实后端)', async ({ page }) => {
await seedToken(page);
await page.goto('/workspace/1');
await page.goto(`/workspace/${WORK_ID}`);
await expect(page.getByRole('heading', { name: /正在创作/ })).toBeVisible({ timeout: 15_000 });
// 等章节结构条出现(activeBlock 已加载,来源 Tab 才有目标 Block)。
await expect(page.getByText(/章节结构/)).toBeVisible({ timeout: 15_000 });