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:
parent
de5f4d0bf9
commit
ca0e04c9a9
@ -113,14 +113,28 @@ async function globalSetup(): Promise<void> {
|
|||||||
await crudEnt('crudshiti-yi');
|
await crudEnt('crudshiti-yi');
|
||||||
await crudEnt('crudshiti-bing');
|
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');
|
await client.query('DELETE FROM muse_content_block_source_attribution WHERE tenant_id=1 AND block_id=3');
|
||||||
const blk = await client.query(
|
const blk = await client.query(
|
||||||
"UPDATE muse_content_block SET revision=1, content_text='活体原始正文待采纳', word_count=9 WHERE tenant_id=1 AND id=3"
|
"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");
|
await client.query("UPDATE muse_ai_suggestion SET status='pending', source_status='active' WHERE tenant_id=1 AND id=3");
|
||||||
if (blk.rowCount === 0) {
|
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 记录派生)。
|
// 4) account-security-ack:把"待确认演练"事件复位为未确认 + 清其确认记录(已确认态由 ack 记录派生)。
|
||||||
|
|||||||
@ -3,10 +3,18 @@ import { expect, test, type Page } from '@playwright/test';
|
|||||||
/**
|
/**
|
||||||
* Block 来源归因查看端到端(真实后端,MSW off)。
|
* Block 来源归因查看端到端(真实后端,MSW off)。
|
||||||
*
|
*
|
||||||
* 覆盖 E 链路:进 work1 workspace → 右侧「来源」Tab → 真实 GET source-attribution →
|
* 覆盖 E 链路:进 work3 workspace → 右侧「来源」Tab → 真实 GET source-attribution →
|
||||||
* 展示来源(work1 block 真后端含 user_original → 渲染「用户原创」)。纯读,无副作用,幂等。
|
* 展示来源(work3/block3 真后端含 user_original → 渲染「用户原创」)。纯读,无副作用,幂等。
|
||||||
|
*
|
||||||
|
* WHY 用 work3 而非 work1(反污染、根治偶发红):本读断言「block 当前 revision 的来源=用户原创」。若读 work1/block1,
|
||||||
|
* 该 block 被 accept-suggestion(写 ai_suggestion 来源)、block-autosave、block-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';
|
const TOKEN = 'test1';
|
||||||
|
/** 读取来源归因的目标作品:work3/block3 是无任何 spec 写入的 pristine Block(见上文 WHY),globalSetup §3 每轮复位其来源。 */
|
||||||
|
const WORK_ID = 3;
|
||||||
|
|
||||||
async function seedToken(page: Page): Promise<void> {
|
async function seedToken(page: Page): Promise<void> {
|
||||||
await page.addInitScript((t) => {
|
await page.addInitScript((t) => {
|
||||||
@ -18,7 +26,7 @@ async function seedToken(page: Page): Promise<void> {
|
|||||||
test('用户查看章节正文来源归因(真实后端)', async ({ page }) => {
|
test('用户查看章节正文来源归因(真实后端)', async ({ page }) => {
|
||||||
await seedToken(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 });
|
await expect(page.getByRole('heading', { name: /正在创作/ })).toBeVisible({ timeout: 15_000 });
|
||||||
// 等章节结构条出现(activeBlock 已加载,来源 Tab 才有目标 Block)。
|
// 等章节结构条出现(activeBlock 已加载,来源 Tab 才有目标 Block)。
|
||||||
await expect(page.getByText(/章节结构/)).toBeVisible({ timeout: 15_000 });
|
await expect(page.getByText(/章节结构/)).toBeVisible({ timeout: 15_000 });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user