test(e2e): cover 143 reload, fail-closed GET, and new-project compile
Live M1 sign-off needs browser proof that missing projection is not 404, outsiders share DOCUMENT_NOT_FOUND, and a real PI compile survives reload. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -60,7 +60,7 @@
|
||||
|
||||
| holder | phase | since | scope | queue |
|
||||
|--------|-------|-------|-------|-------|
|
||||
| Cursor | merging | 2026-09-14 | DOC-RELIABILITY M1 live | - |
|
||||
| Cursor | e2e | 2026-09-14 | DOC-RELIABILITY M1 live | - |
|
||||
|
||||
**phase**: `idle` | `merging` | `smoke` | `e2e`
|
||||
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { test, expect, type Page } from '@playwright/test';
|
||||
import { test, expect, type Page, type Browser } from '@playwright/test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { GOVERNANCE_BLOCKED_HINT } from '../src/lib/blockedGovernanceCopy';
|
||||
import { currentProjectBannerName } from '../src/lib/currentProjectRestore';
|
||||
import {
|
||||
ANY_TERMINAL_STATUS_RE,
|
||||
evaluateSquadObservation,
|
||||
} from '../src/lib/squadCompletionObservation';
|
||||
import {
|
||||
CATEGORY_TRIGGER_LABEL,
|
||||
requiredCategoryForDocOpen,
|
||||
} from '../src/lib/workflowCategoryNavigation';
|
||||
|
||||
/**
|
||||
* M1 live: ledger-first GET. Projection missing must not become 404/409.
|
||||
@@ -22,9 +32,24 @@ const ADMIN_USER = process.env.ADMIN_USERNAME?.trim() || '';
|
||||
const ADMIN_PASS = process.env.ADMIN_PASSWORD?.trim() || '';
|
||||
const BASE = process.env.BASE_URL || 'http://127.0.0.1:8080';
|
||||
const TARGET_DOC = 'doc_proj143_intake';
|
||||
const suffix = `${Date.now().toString(36)}`;
|
||||
const USER_B = `m1b_${suffix}`;
|
||||
const USER_C = `m1c_${suffix}`;
|
||||
const USER_D = `m1d_${suffix}`;
|
||||
const INIT_PASS = `Init-${suffix}9`;
|
||||
const FINAL_PASS_B = `PassB-${suffix}9`;
|
||||
const FINAL_PASS_C = `PassC-${suffix}9`;
|
||||
const FINAL_PASS_D = `PassD-${suffix}9`;
|
||||
const PROJECT_NAME = `M1活验-${suffix}`;
|
||||
|
||||
test.skip(!runAcceptance, 'set RUN_UI_ACCEPTANCE=1 against the rebuilt dev stack');
|
||||
|
||||
const state: {
|
||||
newDocId?: string;
|
||||
newMarkdown?: string;
|
||||
outsiderReady?: boolean;
|
||||
} = {};
|
||||
|
||||
async function login(page: Page, username: string, password: string) {
|
||||
await page.goto('/');
|
||||
await page.fill('input[placeholder*="系统用户名"]', username);
|
||||
@@ -33,6 +58,144 @@ async function login(page: Page, username: string, password: string) {
|
||||
await expect(page.getByText('新建立项会话')).toBeVisible({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
async function changePasswordFirstLogin(page: Page, oldPass: string, newPass: string) {
|
||||
await expect(page.getByPlaceholder('请输入初始密码')).toBeVisible({ timeout: 15_000 });
|
||||
await page.fill('input[placeholder="请输入初始密码"]', oldPass);
|
||||
await page.fill('input[placeholder*="至少 6 位"]', newPass);
|
||||
await page.fill('input[placeholder="再次输入新密码"]', newPass);
|
||||
await page.getByRole('button', { name: /确认重置并进入系统/ }).click();
|
||||
await expect(page.getByText('新建立项会话')).toBeVisible({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
async function logoutViaUI(page: Page) {
|
||||
await page.locator('button[title="个人资料与系统偏好设置"]').click({ timeout: 15_000 });
|
||||
const logout = page.getByRole('button', { name: '退出当前登录' });
|
||||
await expect(logout).toBeVisible({ timeout: 10_000 });
|
||||
await logout.click();
|
||||
await expect(page.getByPlaceholder(/系统用户名/)).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
async function sessionToken(page: Page): Promise<string> {
|
||||
const token = await page.evaluate(() => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem('smdf_auth_session');
|
||||
if (!raw) return '';
|
||||
const session = JSON.parse(raw) as { token?: string };
|
||||
return session.token || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
if (!token) {
|
||||
throw new Error('login did not persist smdf_auth_session.token');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
async function getContent(page: Page, docId: string, token?: string) {
|
||||
const headers = token ? { Authorization: `Bearer ${token}` } : {};
|
||||
const res = await page.request.get(`${BASE}/api/v1/docgov/documents/${docId}/content`, {
|
||||
headers,
|
||||
});
|
||||
let body: { markdown?: string; consistency?: string; error_code?: string } = {};
|
||||
try {
|
||||
body = (await res.json()) as typeof body;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return { status: res.status(), body };
|
||||
}
|
||||
|
||||
function expectClosed404(status: number, body: { error_code?: string }, label: string) {
|
||||
expect(status, `${label} ${JSON.stringify(body)}`).toBe(404);
|
||||
expect(body.error_code, label).toBe('DOCUMENT_NOT_FOUND');
|
||||
}
|
||||
|
||||
async function goToDutyStep(page: Page) {
|
||||
const approval = page.locator('label', { hasText: '纳入人工审批' }).first();
|
||||
const selectHint = page.getByText('选择要激活的已发布工作流版本。激活后将冻结配置,运行中不可切换。');
|
||||
await expect(selectHint.or(approval)).toBeVisible({ timeout: 20_000 });
|
||||
if (await approval.isVisible().catch(() => false)) {
|
||||
return;
|
||||
}
|
||||
await page.getByRole('button', { name: /下一步/ }).click();
|
||||
await expect(approval).toBeVisible({ timeout: 20_000 });
|
||||
}
|
||||
|
||||
function dutyRoleSelect(page: Page, placeholder: '作者' | '审核' | '批准') {
|
||||
return page
|
||||
.locator('select')
|
||||
.filter({ has: page.getByRole('option', { name: placeholder, exact: true }) })
|
||||
.first();
|
||||
}
|
||||
|
||||
function turnAssistantLocator(page: Page, userPromptSnippet: string) {
|
||||
const userBubble = page
|
||||
.locator('[data-chat-role="user"]')
|
||||
.filter({ hasText: userPromptSnippet })
|
||||
.last();
|
||||
return {
|
||||
userBubble,
|
||||
assistant: userBubble.locator(
|
||||
'xpath=ancestor::*[@data-index][1]/following-sibling::*[@data-index][1]//*[@data-chat-role="assistant"]',
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function waitForSquadCompletion(page: Page, userPromptSnippet: string, timeoutMs = 420_000) {
|
||||
const { userBubble, assistant } = turnAssistantLocator(page, userPromptSnippet);
|
||||
await expect(userBubble).toBeVisible({ timeout: 30_000 });
|
||||
await expect(assistant).toBeVisible({ timeout: 30_000 });
|
||||
const connErr = page.locator('text=连接异常');
|
||||
const observe = async () =>
|
||||
evaluateSquadObservation(
|
||||
{
|
||||
userPromptMatched: true,
|
||||
assistantAdjacent: true,
|
||||
status: (await assistant.getAttribute('data-chat-status')) || '',
|
||||
hasVisibleError: await connErr.first().isVisible().catch(() => false),
|
||||
},
|
||||
{ requireInflight: false },
|
||||
);
|
||||
let verdict = await observe();
|
||||
if (verdict.action === 'reject') {
|
||||
throw new Error(`squad observation rejected: ${verdict.reason}`);
|
||||
}
|
||||
if (verdict.action === 'accept') {
|
||||
return;
|
||||
}
|
||||
await expect(assistant).toHaveAttribute('data-chat-status', ANY_TERMINAL_STATUS_RE, {
|
||||
timeout: timeoutMs,
|
||||
});
|
||||
verdict = await observe();
|
||||
if (verdict.action !== 'accept') {
|
||||
throw new Error(`squad terminal not accepted: ${verdict.reason}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function openWorkflowDocByLabel(page: Page, label: RegExp) {
|
||||
const category = requiredCategoryForDocOpen({ kind: 'workflow-label' });
|
||||
const catTrigger = page.getByTestId('doc-category-trigger');
|
||||
await expect(catTrigger).toBeVisible({ timeout: 15_000 });
|
||||
await catTrigger.click();
|
||||
const catItem = page.locator(`.dropdown-menu button[data-category="${category}"]`).first();
|
||||
await catItem.click();
|
||||
await expect(catTrigger).toContainText(CATEGORY_TRIGGER_LABEL[category], { timeout: 10_000 });
|
||||
const trigger = page.getByTestId('doc-selector-trigger');
|
||||
await expect(trigger).toBeVisible({ timeout: 15_000 });
|
||||
await trigger.click();
|
||||
const item = page.locator('.dropdown-menu button[data-doc-id]').filter({ hasText: label }).first();
|
||||
if ((await item.count()) === 0) {
|
||||
throw new Error(`workflow slot not in doc-selector menu: ${label}`);
|
||||
}
|
||||
const docId = (await item.getAttribute('data-doc-id')) || '';
|
||||
await item.click();
|
||||
if (!docId) {
|
||||
throw new Error(`selected menu item missing data-doc-id for ${label}`);
|
||||
}
|
||||
return docId;
|
||||
}
|
||||
|
||||
test.describe.serial('controlled document reload M1 live', () => {
|
||||
test('project 143 content is 200 with consistency; anonymous is 401; unknown is 404', async ({
|
||||
browser,
|
||||
@@ -62,53 +225,235 @@ test.describe.serial('controlled document reload M1 live', () => {
|
||||
await expect(page.getByRole('banner')).toContainText('对话协同会话', { timeout: 20_000 });
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS, '01-project-143.png'), fullPage: true });
|
||||
|
||||
const token = await page.evaluate(() => {
|
||||
try {
|
||||
const raw = window.localStorage.getItem('smdf_auth_session');
|
||||
if (!raw) return '';
|
||||
const session = JSON.parse(raw) as { token?: string };
|
||||
return session.token || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
if (!token) {
|
||||
throw new Error('login did not persist smdf_auth_session.token');
|
||||
}
|
||||
|
||||
const authed = await page.request.get(`${BASE}/api/v1/docgov/documents/${TARGET_DOC}/content`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
const authedBody = await authed.json();
|
||||
expect(authed.status(), `content ${JSON.stringify(authedBody)}`).toBe(200);
|
||||
expect(authedBody.markdown, 'ledger markdown required').toBeTruthy();
|
||||
expect(['aligned', 'missing', 'drift', 'empty', 'ledger_missing']).toContain(authedBody.consistency);
|
||||
expect(authedBody.error_code ?? '').not.toMatch(/PROJECTION/);
|
||||
const token = await sessionToken(page);
|
||||
const authed = await getContent(page, TARGET_DOC, token);
|
||||
expect(authed.status, `content ${JSON.stringify(authed.body)}`).toBe(200);
|
||||
expect(authed.body.markdown, 'ledger markdown required').toBeTruthy();
|
||||
expect(['aligned', 'missing', 'drift', 'empty', 'ledger_missing']).toContain(authed.body.consistency);
|
||||
expect(authed.body.error_code ?? '').not.toMatch(/PROJECTION/);
|
||||
await expect(page.getByText('加载失败,请点击重试')).toHaveCount(0);
|
||||
|
||||
await page.reload();
|
||||
await expect(page.getByRole('banner')).toContainText('对话协同会话', { timeout: 20_000 });
|
||||
const afterReload = await page.request.get(`${BASE}/api/v1/docgov/documents/${TARGET_DOC}/content`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
expect(afterReload.status()).toBe(200);
|
||||
const reloadBody = await afterReload.json();
|
||||
expect(reloadBody.markdown).toBeTruthy();
|
||||
|
||||
const afterReload = await getContent(page, TARGET_DOC, token);
|
||||
expect(afterReload.status).toBe(200);
|
||||
expect(afterReload.body.markdown).toBeTruthy();
|
||||
await expect(page.getByText('加载失败,请点击重试')).toHaveCount(0);
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS, '02-after-reload.png'), fullPage: true });
|
||||
|
||||
const anon = await page.request.get(`${BASE}/api/v1/docgov/documents/${TARGET_DOC}/content`);
|
||||
expect(anon.status()).toBe(401);
|
||||
await logoutViaUI(page);
|
||||
await login(page, ADMIN_USER, ADMIN_PASS);
|
||||
await page.getByRole('complementary').getByTitle('对话协同会话', { exact: true }).click({ force: true });
|
||||
await expect(page.getByRole('banner')).toContainText('对话协同会话', { timeout: 20_000 });
|
||||
const afterRelogin = await getContent(page, TARGET_DOC, await sessionToken(page));
|
||||
expect(afterRelogin.status).toBe(200);
|
||||
expect(afterRelogin.body.markdown).toBeTruthy();
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS, '03-after-relogin.png'), fullPage: true });
|
||||
|
||||
const missing = await page.request.get(`${BASE}/api/v1/docgov/documents/doc_does_not_exist_m1/content`, {
|
||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||
});
|
||||
expect(missing.status()).toBe(404);
|
||||
const missingBody = await missing.json();
|
||||
expect(missingBody.error_code).toBe('DOCUMENT_NOT_FOUND');
|
||||
const anon = await getContent(page, TARGET_DOC);
|
||||
expect(anon.status).toBe(401);
|
||||
|
||||
const missing = await getContent(page, 'doc_does_not_exist_m1', await sessionToken(page));
|
||||
expectClosed404(missing.status, missing.body, 'unknown doc');
|
||||
|
||||
const projection409 = contentHits.filter(
|
||||
(h) => h.status === 409 || String(h.error || '').includes('PROJECTION'),
|
||||
);
|
||||
expect(projection409, JSON.stringify(contentHits)).toHaveLength(0);
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('non-member GET of project 143 is the same 404 as unknown', async ({ browser }) => {
|
||||
if (!ADMIN_USER || !ADMIN_PASS) {
|
||||
throw new Error('ADMIN_USERNAME and ADMIN_PASSWORD are required');
|
||||
}
|
||||
const admin = await browser.newPage();
|
||||
await login(admin, ADMIN_USER, ADMIN_PASS);
|
||||
await admin.locator('button[title*="系统管理与准入治理"]').click();
|
||||
for (const [user, name, role] of [
|
||||
[USER_B, `审核协作者-${suffix}`, 'role-engineer'],
|
||||
[USER_C, `批准协作者-${suffix}`, 'role-engineer'],
|
||||
[USER_D, `局外人-${suffix}`, 'role-guest'],
|
||||
] as const) {
|
||||
await admin.getByRole('button', { name: '开通受控员工账号' }).click();
|
||||
const form = admin.locator('form').filter({ hasText: '用户名 (系统账号)' });
|
||||
await form.locator('input[placeholder="如 li_si"]').fill(user);
|
||||
await form.locator('input[placeholder="如 李四"]').fill(name);
|
||||
const passBox = form.locator('xpath=.//label[contains(.,"临时初始密码")]/following::input[1]');
|
||||
await passBox.fill(INIT_PASS);
|
||||
await form.locator('select').selectOption(role);
|
||||
await form.getByRole('button', { name: '确认开通账号' }).click();
|
||||
await expect(form).toBeHidden({ timeout: 15_000 });
|
||||
}
|
||||
const adminHeading = admin.getByText('系统管理控制台', { exact: true });
|
||||
const adminModal = adminHeading.locator('xpath=ancestor::div[contains(@style,"position: fixed")][1]');
|
||||
await adminModal.locator('button').first().click();
|
||||
await expect(adminHeading).toBeHidden({ timeout: 10_000 });
|
||||
await admin.close();
|
||||
|
||||
const outsider = await browser.newPage();
|
||||
await outsider.goto('/');
|
||||
await outsider.fill('input[placeholder*="系统用户名"]', USER_D);
|
||||
await outsider.fill('input[placeholder*="登录密码"]', INIT_PASS);
|
||||
await outsider.click('button:has-text("进入医疗协同工作台")');
|
||||
await changePasswordFirstLogin(outsider, INIT_PASS, FINAL_PASS_D);
|
||||
const token = await sessionToken(outsider);
|
||||
const closed = await getContent(outsider, TARGET_DOC, token);
|
||||
expectClosed404(closed.status, closed.body, 'non-member 143');
|
||||
const unknown = await getContent(outsider, 'doc_does_not_exist_m1', token);
|
||||
expectClosed404(unknown.status, unknown.body, 'outsider unknown');
|
||||
await outsider.screenshot({ path: path.join(SCREENSHOTS, '04-outsider-143.png'), fullPage: true });
|
||||
await outsider.close();
|
||||
state.outsiderReady = true;
|
||||
});
|
||||
|
||||
test('new project PI compile then reload keeps ledger markdown', async ({ browser }) => {
|
||||
if (!ADMIN_USER || !ADMIN_PASS) {
|
||||
throw new Error('ADMIN_USERNAME and ADMIN_PASSWORD are required');
|
||||
}
|
||||
const reviewer = await browser.newPage();
|
||||
await reviewer.goto('/');
|
||||
await reviewer.fill('input[placeholder*="系统用户名"]', USER_B);
|
||||
await reviewer.fill('input[placeholder*="登录密码"]', INIT_PASS);
|
||||
await reviewer.click('button:has-text("进入医疗协同工作台")');
|
||||
await changePasswordFirstLogin(reviewer, INIT_PASS, FINAL_PASS_B);
|
||||
await reviewer.close();
|
||||
|
||||
const page = await browser.newPage();
|
||||
const runCreates: { status: number; body: string }[] = [];
|
||||
page.on('response', async (res) => {
|
||||
if (res.url().includes('/api/v1/tasks/runs') && res.request().method() === 'POST') {
|
||||
runCreates.push({ status: res.status(), body: await res.text().catch(() => '') });
|
||||
}
|
||||
});
|
||||
await login(page, ADMIN_USER, ADMIN_PASS);
|
||||
await page.getByRole('button', { name: '新建立项会话' }).click();
|
||||
const heading = page.getByText('发起医疗器械新项目立项', { exact: true });
|
||||
await expect(heading).toBeVisible({ timeout: 20_000 });
|
||||
const modal = heading.locator('xpath=ancestor::div[contains(@style,"position: fixed")][1]');
|
||||
await modal.locator('input[placeholder*="无线便携式"]').fill(PROJECT_NAME);
|
||||
await modal.locator('textarea').fill('M1 活验:新建项目编制一条受控文档后刷新。');
|
||||
for (const name of [`审核协作者-${suffix}`, `批准协作者-${suffix}`]) {
|
||||
const memberName = modal.getByText(name, { exact: true });
|
||||
await expect(memberName).toBeVisible({ timeout: 15_000 });
|
||||
await memberName.locator('xpath=ancestor::div[contains(@style,"cursor: pointer")][1]').click();
|
||||
}
|
||||
await modal.getByRole('button', { name: /确认立项/ }).click();
|
||||
await expect(currentProjectBannerName(page, PROJECT_NAME)).toBeVisible({ timeout: 30_000 });
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS, '05-new-project.png'), fullPage: true });
|
||||
|
||||
const generatePrompt = '请编制用户初始需求收集表';
|
||||
const input = page.getByPlaceholder(/向项目助手提问/);
|
||||
await input.fill(generatePrompt);
|
||||
await page.getByRole('button', { name: /发送/ }).click();
|
||||
await expect(page.getByText(generatePrompt)).toBeVisible({ timeout: 20_000 });
|
||||
await expect(page.getByText(GOVERNANCE_BLOCKED_HINT)).toBeVisible({ timeout: 45_000 });
|
||||
const blocked = runCreates.find((row) => row.status === 202 && row.body.includes('blocked_pending_governance'));
|
||||
if (!blocked) {
|
||||
throw new Error(`expected 202 blocked run before activation, got ${JSON.stringify(runCreates)}`);
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: '配置工作流' }).click();
|
||||
await goToDutyStep(page);
|
||||
const toggles = page.locator('label', { hasText: '纳入人工审批' });
|
||||
const toggleCount = await toggles.count();
|
||||
for (let i = 1; i < toggleCount; i += 1) {
|
||||
const box = toggles.nth(i).locator('input[type="checkbox"]');
|
||||
if (await box.isChecked()) await box.uncheck();
|
||||
}
|
||||
const firstToggle = toggles.first().locator('input[type="checkbox"]');
|
||||
if (!(await firstToggle.isChecked())) await firstToggle.check();
|
||||
const author = dutyRoleSelect(page, '作者');
|
||||
await expect(author).toBeVisible({ timeout: 10_000 });
|
||||
const values = (
|
||||
await author.locator('option').evaluateAll((nodes) => nodes.map((n) => (n as HTMLOptionElement).value))
|
||||
).filter(Boolean);
|
||||
if (values.length < 3) {
|
||||
throw new Error(`need 3 members for full duties, got ${values.length}`);
|
||||
}
|
||||
await author.selectOption(values[0]);
|
||||
await dutyRoleSelect(page, '审核').selectOption(values[1]);
|
||||
await dutyRoleSelect(page, '批准').selectOption(values[2]);
|
||||
await page.getByRole('button', { name: /校验并预览/ }).click();
|
||||
await expect(page.getByText('激活预览')).toBeVisible({ timeout: 20_000 });
|
||||
await page.getByRole('button', { name: '确认激活' }).click();
|
||||
await expect(page.getByText('运行中')).toBeVisible({ timeout: 20_000 });
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS, '06-activated.png'), fullPage: true });
|
||||
|
||||
const compilePrompt = '请基于本项目编制首轮《用户需求》摸底文档,覆盖预期用途、关键性能与适用法规框架,直接生成受控交付物。';
|
||||
const taskRunWait = page.waitForResponse(
|
||||
(r) => {
|
||||
try {
|
||||
return new URL(r.url()).pathname.includes('/api/v1/tasks/runs') && r.request().method() === 'POST';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
await input.fill(compilePrompt);
|
||||
await page.getByRole('button', { name: /发送/ }).click();
|
||||
const taskRes = await taskRunWait;
|
||||
if (taskRes.status() < 200 || taskRes.status() >= 300) {
|
||||
throw new Error(`compile tasks/runs HTTP ${taskRes.status()}`);
|
||||
}
|
||||
await waitForSquadCompletion(page, '请基于本项目编制首轮', 420_000);
|
||||
const docId = await openWorkflowDocByLabel(page, /用户初始需求|需求摸底|intake/i);
|
||||
if (!/^doc_proj\d+_/.test(docId)) {
|
||||
throw new Error(`expected server doc_proj* slot, got ${docId}`);
|
||||
}
|
||||
state.newDocId = docId;
|
||||
const token = await sessionToken(page);
|
||||
const first = await getContent(page, docId, token);
|
||||
expect(first.status, JSON.stringify(first.body)).toBe(200);
|
||||
expect(first.body.markdown).toBeTruthy();
|
||||
expect(['aligned', 'missing', 'drift', 'empty', 'ledger_missing']).toContain(first.body.consistency);
|
||||
expect(first.body.error_code ?? '').not.toMatch(/PROJECTION/);
|
||||
state.newMarkdown = String(first.body.markdown);
|
||||
await expect(page.getByText('加载失败,请点击重试')).toHaveCount(0);
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS, '07-new-doc.png'), fullPage: true });
|
||||
|
||||
await page.reload();
|
||||
await expect(currentProjectBannerName(page, PROJECT_NAME)).toBeVisible({ timeout: 30_000 });
|
||||
const afterReload = await getContent(page, docId, token);
|
||||
expect(afterReload.status).toBe(200);
|
||||
expect(afterReload.body.markdown).toBe(state.newMarkdown);
|
||||
await expect(page.getByText('加载失败,请点击重试')).toHaveCount(0);
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS, '08-new-doc-reload.png'), fullPage: true });
|
||||
|
||||
await logoutViaUI(page);
|
||||
await login(page, ADMIN_USER, ADMIN_PASS);
|
||||
await page.getByRole('complementary').getByTitle(PROJECT_NAME, { exact: true }).click({ force: true });
|
||||
await expect(currentProjectBannerName(page, PROJECT_NAME)).toBeVisible({ timeout: 30_000 });
|
||||
const afterRelogin = await getContent(page, docId, await sessionToken(page));
|
||||
expect(afterRelogin.status).toBe(200);
|
||||
expect(afterRelogin.body.markdown).toBe(state.newMarkdown);
|
||||
await page.screenshot({ path: path.join(SCREENSHOTS, '09-new-doc-relogin.png'), fullPage: true });
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('cross-project and wrong-stage GET stay DOCUMENT_NOT_FOUND', async ({ browser }: { browser: Browser }) => {
|
||||
if (!state.newDocId) {
|
||||
throw new Error('new project document id missing; compile test did not leave a ledger');
|
||||
}
|
||||
const outsider = await browser.newPage();
|
||||
await login(outsider, USER_D, FINAL_PASS_D);
|
||||
const tokenD = await sessionToken(outsider);
|
||||
const cross143 = await getContent(outsider, TARGET_DOC, tokenD);
|
||||
expectClosed404(cross143.status, cross143.body, 'D vs 143');
|
||||
const crossNew = await getContent(outsider, state.newDocId, tokenD);
|
||||
expectClosed404(crossNew.status, crossNew.body, 'D vs new project doc');
|
||||
await outsider.close();
|
||||
|
||||
const reviewer = await browser.newPage();
|
||||
await login(reviewer, USER_B, FINAL_PASS_B);
|
||||
const tokenB = await sessionToken(reviewer);
|
||||
const otherStage = state.newDocId.replace(/_[a-z0-9]+$/i, '_design_control');
|
||||
const wrongDuty = await getContent(reviewer, otherStage, tokenB);
|
||||
expectClosed404(wrongDuty.status, wrongDuty.body, 'B vs wrong stage-duty');
|
||||
const stillClosed143 = await getContent(reviewer, TARGET_DOC, tokenB);
|
||||
expectClosed404(stillClosed143.status, stillClosed143.body, 'B vs 143');
|
||||
await reviewer.screenshot({ path: path.join(SCREENSHOTS, '10-fail-closed.png'), fullPage: true });
|
||||
await reviewer.close();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user