From d91200192abb604e5a8b7f7b6d8cd6aad5558267 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Fri, 23 Oct 2026 08:34:58 +0000 Subject: [PATCH 1/3] agent-exec transport 0091f9ff5f85f944b825407b437f0281/e0053536f16c --- e2e/admin-user-create.playwright.ts | 37 ++++++++++++++++++++-------- e2e/rdm-audit.playwright.ts | 38 +++++++++++++++++++++-------- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/e2e/admin-user-create.playwright.ts b/e2e/admin-user-create.playwright.ts index 65fcbae..3d35819 100644 --- a/e2e/admin-user-create.playwright.ts +++ b/e2e/admin-user-create.playwright.ts @@ -15,22 +15,39 @@ const PASSWORD2 = process.env.E2E_PASSWORD2 || 'E2eAudit#2026b' test.skip(!process.env.PLAYWRIGHT_BASE_URL, 'set PLAYWRIGHT_BASE_URL (scripts/e2e.sh drives this suite)') -async function login(page: Page) { - await page.goto('/') +async function passwordLogin(page: Page, password: string) { await expect(page.getByPlaceholder('由管理员分配')).toBeVisible() await page.getByPlaceholder('由管理员分配').fill(OWNER) - await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD) + await page.getByPlaceholder('初始密码或常用密码').fill(password) await page.getByRole('button', { name: '登录', exact: true }).click() - // 三种落点:直接进工作台(密码已生效)|强制改密门|初始密码已失效(种子二次运行)→ 改用新密码重登。 +} + +/** 强制改密门:设新密码后服务端销毁会话,回到登录页。 */ +async function completePasswordGate(page: Page) { + // 门模式异步加载:current_required 渲染 #forced-current,其余模式渲染受限设密提示。 + const current = page.locator('#forced-current') + await expect(current.or(page.locator('.lg-card .lg-hint'))).toBeVisible() + if (await current.isVisible()) await current.fill(PASSWORD) + // 「新密码(≥8 位)」「确认新密码」label 未关联输入框,getByLabel 无法命中,按门内顺序定位。 + const pw = page.locator('.lg-card input[type="password"]:not(#forced-current)') + await pw.nth(0).fill(PASSWORD2) + await pw.nth(1).fill(PASSWORD2) + await page.getByRole('button', { name: '设置新密码', exact: true }).click() + await expect(page.getByRole('heading', { name: '密码已更新' })).toBeVisible({ timeout: 10_000 }) + await page.getByRole('button', { name: '返回登录', exact: true }).click() +} + +async function login(page: Page) { + await page.goto('/') + await passwordLogin(page, PASSWORD) + // 三种落点:直接进工作台(密码已生效)|强制改密门 → 设新密码后重登|初始密码已失效(种子二次运行)→ 改用新密码重登。 const entered = await page.locator('.tsb-user').waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false) if (!entered) { - const gated = await page.getByText('修改密码').waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false) + // 门标题与提交按钮同名(设置新密码):用 heading 角色唯一定位。 + const gated = await page.getByRole('heading', { name: '设置新密码' }).waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false) if (gated) { - const pwInputs = page.locator('.lg-card input[type="password"]') - await pwInputs.nth(0).fill(PASSWORD) - await pwInputs.nth(1).fill(PASSWORD2) - await pwInputs.nth(2).fill(PASSWORD2) - await page.getByRole('button', { name: '设置新密码并进入' }).click() + await completePasswordGate(page) + await passwordLogin(page, PASSWORD2) } else { await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD2) await page.getByRole('button', { name: '登录', exact: true }).click() diff --git a/e2e/rdm-audit.playwright.ts b/e2e/rdm-audit.playwright.ts index 13f52ee..6b98204 100644 --- a/e2e/rdm-audit.playwright.ts +++ b/e2e/rdm-audit.playwright.ts @@ -16,21 +16,39 @@ const ISOLATED = process.env.E2E_PROJECT_ISOLATED || '隔离检查项目' test.skip(!process.env.PLAYWRIGHT_BASE_URL, 'set PLAYWRIGHT_BASE_URL (scripts/e2e.sh drives this suite)') -async function login(page: Page) { - await page.goto('/') +async function passwordLogin(page: Page, password: string) { await expect(page.getByPlaceholder('由管理员分配')).toBeVisible() await page.getByPlaceholder('由管理员分配').fill(OWNER) - // 首个用例用初始密码(触发强制改密门),后续用例用改后的新密码。 - await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD) + await page.getByPlaceholder('初始密码或常用密码').fill(password) await page.getByRole('button', { name: '登录', exact: true }).click() - const gate = page.getByText('修改密码') +} + +/** 强制改密门:设新密码后服务端销毁会话,回到登录页。 */ +async function completePasswordGate(page: Page) { + // 门模式异步加载:current_required 渲染 #forced-current,其余模式渲染受限设密提示。 + const current = page.locator('#forced-current') + await expect(current.or(page.locator('.lg-card .lg-hint'))).toBeVisible() + if (await current.isVisible()) await current.fill(PASSWORD) + // 「新密码(≥8 位)」「确认新密码」label 未关联输入框,getByLabel 无法命中,按门内顺序定位。 + const pw = page.locator('.lg-card input[type="password"]:not(#forced-current)') + await pw.nth(0).fill(PASSWORD2) + await pw.nth(1).fill(PASSWORD2) + await page.getByRole('button', { name: '设置新密码', exact: true }).click() + await expect(page.getByRole('heading', { name: '密码已更新' })).toBeVisible({ timeout: 10_000 }) + await page.getByRole('button', { name: '返回登录', exact: true }).click() +} + +async function login(page: Page) { + await page.goto('/') + // 首个用例用初始密码(触发强制改密门),后续用例用改后的新密码。 + await passwordLogin(page, PASSWORD) + // 门标题与提交按钮同名(设置新密码):用 heading 角色唯一定位。 + const gate = page.getByRole('heading', { name: '设置新密码' }) const gated = await gate.waitFor({ state: 'visible', timeout: 5_000 }).then(() => true).catch(() => false) if (gated) { - const pwInputs = page.locator('.lg-card input[type="password"]') - await pwInputs.nth(0).fill(PASSWORD) - await pwInputs.nth(1).fill(PASSWORD2) - await pwInputs.nth(2).fill(PASSWORD2) - await page.getByRole('button', { name: '设置新密码并进入' }).click() + // 设密后会话已销毁:用新密码重登。 + await completePasswordGate(page) + await passwordLogin(page, PASSWORD2) } else { // 已改密:改用新密码重登。 await page.getByPlaceholder('初始密码或常用密码').fill(PASSWORD2) -- 2.54.0 From ded9b568b03f8d6cfd1bfd4b0e3b3814876b32f8 Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Fri, 23 Oct 2026 09:48:49 +0000 Subject: [PATCH 2/3] agent-exec transport 4a92e3a9fabe41f2d62ffbceeb92daa1/267104e747c8 --- e2e/admin-user-create.playwright.ts | 9 ++++-- e2e/rdm-cockpit.playwright.ts | 41 ++++++++++++++++++++++----- src/components/adminSections.test.tsx | 22 ++++++++++++++ src/components/adminSections.tsx | 8 +++++- 4 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 src/components/adminSections.test.tsx diff --git a/e2e/admin-user-create.playwright.ts b/e2e/admin-user-create.playwright.ts index 3d35819..60e05bb 100644 --- a/e2e/admin-user-create.playwright.ts +++ b/e2e/admin-user-create.playwright.ts @@ -86,10 +86,15 @@ test('管理员建号:UI 请求新契约并给出待初始化提示', async ({ // 契约断言二:页面创建后账号可直接使用,不再提示 CLI 初始化。 await expect(page.locator('.ad-notice')).toContainText(`已创建 ${newOwner}`) - // 契约断言三:用户表出现该账号,状态为未设密码(HTTP 建号不能设密码/授权限)。 + // 契约断言三:请求不带密码时服务端生成一次性初始密码(响应 initialPassword, + // 账号 hasPassword=true 且 mustChangePassword=true)——页面一次性交付该密码, + // 用户表该账号状态为待改密而非未设密码,且未被授予管理员。 + await expect(page.locator('.ad-reset .ad-hint')).toContainText(`初始密码仅显示一次,请立即安全交付给 ${newOwner}`) + await expect(page.locator('.ad-reset input[aria-label="一次性密码"]')).not.toHaveValue('') const row = page.locator('.ad-table tbody tr', { hasText: newOwner }) await expect(row).toBeVisible() - await expect(row).toContainText('未设密码') + await expect(row).toContainText('待改密') + await expect(row).not.toContainText('未设密码') await expect(row).not.toContainText('ADMIN') // 界面说明与后端流程一致(激活 Key 由服务器本地 CLI 发放)。 diff --git a/e2e/rdm-cockpit.playwright.ts b/e2e/rdm-cockpit.playwright.ts index b807120..a4afab1 100644 --- a/e2e/rdm-cockpit.playwright.ts +++ b/e2e/rdm-cockpit.playwright.ts @@ -53,6 +53,37 @@ async function openReaderOnDoc(page: Page, title: string) { await expect(page.locator('.cockpit-reader')).toBeVisible() } +/** + * 以本人会话落一条已提交的需求账本条目(唯一键防幂等跳过)。正式项目命令必须锚定 + * 同会话已提交的来源消息(基座来源门禁,与页面审签 documentDecision 同一路径): + * 宿主对话项目 → 新建探针对话 → 提交来源消息 → 以该消息为锚点分发;回执必须 committed。 + */ +async function commitProbeChange(page: Page, rdmProjectId: string) { + const projects = await (await page.request.get('/api/projects')).json() as { id: string; settings?: Record }[] + const host = projects.find(p => p.settings?.['rdm.projectId'] === rdmProjectId) + expect(host, `宿主对话项目须绑定 ${rdmProjectId}`).toBeTruthy() + const conv = await page.request.post(`/api/projects/${host!.id}/conversations`, { data: { title: '驾驶舱一致性探针' } }) + expect(conv.ok(), `探针对话创建:HTTP ${conv.status()}`).toBe(true) + const conversationId = (await conv.json()).id as string + const stamp = Date.now() + const title = `UR-PROBE-${stamp} 一致性探针` + const sent = await page.request.post(`/api/conversations/${conversationId}/messages`, { + data: { text: `一致性探针:向需求账本导入条目「${title}」`, attachmentIds: [], requestId: `cockpit-probe:${stamp}` }, + }) + expect(sent.ok(), `探针来源消息:HTTP ${sent.status()}`).toBe(true) + const anchorMessageId = (await sent.json()).message?.id as string + expect(anchorMessageId, '来源消息须已提交').toBeTruthy() + const res = await page.request.post('/api/commands/rdm.reqs.import', { + data: { + params: { projectId: rdmProjectId, items: [{ kind: 'UR', title, statement: '刷新一致性驱动条目', source: 'e2e-probe' }] }, + conversationId, anchorMessageId, entrance: 'api', + }, + failOnStatusCode: false, + }) + const body = await res.json().catch(() => null) + expect(body?.receipt?.state, `探针写入回执:HTTP ${res.status()} ${JSON.stringify(body?.receipt ?? null)}`).toBe('committed') +} + test.describe.serial('I4 文档驾驶舱组合验收', () => { // 场景 1:首次进入项目——统计带、结构树、全景可见(DV-R01/R02)。 test('1 首屏:统计、树、全景可见且统计可判读', async ({ page }) => { @@ -125,13 +156,9 @@ test.describe.serial('I4 文档驾驶舱组合验收', () => { await branch4.locator('.cockpit-tree-caret').click() await branch4.locator('.cockpit-tree-title', { hasText: '设计说明' }).first().click() const snapBefore = await page.locator('.cockpit-snap').innerText() - // 通过应用自身 API 落一个已提交变更(需求账本新增条目,唯一键防幂等跳过; - // 采纳走高危确认卡、服务端不允许客户端伪造 Confirmed,故用低危提交面驱动)。 - await page.request.post('/api/commands/rdm.reqs.import', { - data: { params: { projectId: await page.locator('.cockpit-project').inputValue(), - items: [{ kind: 'UR', title: `UR-PROBE-${Date.now()} 一致性探针`, statement: '刷新一致性驱动条目', source: 'e2e-probe' }] } }, - failOnStatusCode: false, - }) + // 通过应用自身 API 落一个已提交变更(采纳走高危确认卡、服务端不允许客户端伪造 + // Confirmed,故用低危提交面驱动);探针被拒即在此失败,不让水位断言替它背锅。 + await commitProbeChange(page, await page.locator('.cockpit-project').inputValue()) await page.getByRole('button', { name: '刷新' }).click() await expect(page.locator('.cockpit-snap')).not.toHaveText(snapBefore) // 版本块前进:出现新版本条目。 diff --git a/src/components/adminSections.test.tsx b/src/components/adminSections.test.tsx new file mode 100644 index 0000000..7419e41 --- /dev/null +++ b/src/components/adminSections.test.tsx @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest' +import { renderToStaticMarkup } from 'react-dom/server' +import { UserPasswordStatus } from './adminSections' + +const status = (hasPassword: boolean, mustChangePassword: boolean) => + renderToStaticMarkup() + +describe('UserPasswordStatus', () => { + it('shows an account without a password as unset even when it carries the must-change flag', () => { + expect(status(false, true)).toBe('未设密码') + expect(status(false, false)).toBe('未设密码') + }) + + it('shows a password that must be changed, such as a server-generated initial password, as pending change', () => { + expect(status(true, true)).toContain('待改密') + expect(status(true, true)).toContain('ad-chip-warn') + }) + + it('shows an account with a settled password as normal', () => { + expect(status(true, false)).toBe('正常') + }) +}) diff --git a/src/components/adminSections.tsx b/src/components/adminSections.tsx index afc58bd..f668c43 100644 --- a/src/components/adminSections.tsx +++ b/src/components/adminSections.tsx @@ -17,6 +17,12 @@ import { GeneratedCredentialPanel, oneTimePasswordCredential, type GeneratedCred // 管理员生成一次性登录密码,用户登录后必须立即改密。 ---- interface AdminUserRow { userId: string; owner: string; name: string; isAdmin: boolean; isSuperAdmin?: boolean; mustChangePassword: boolean; hasPassword: boolean; passwordSetAt?: string; createdAt: string; stateVersion: number } +// 无密码账号的改密旗标没有可改的密码,不得冒充「待改密」。 +export function UserPasswordStatus({ account }: { account: Pick }) { + if (!account.hasPassword) return <>未设密码 + return account.mustChangePassword ? 待改密 : <>正常 +} + export function UsersPanel() { const me = useStore(s => s.me) const [users, setUsers] = useState(null) @@ -83,7 +89,7 @@ export function UsersPanel() { {u.owner} {u.name || '—'} {u.isSuperAdmin ? : u.isAdmin ? ADMIN : USER} - {u.mustChangePassword ? 待改密 : u.hasPassword ? '正常' : '未设密码'} + {u.passwordSetAt ? new Date(u.passwordSetAt).toLocaleString() : '—'} {me?.isAdmin && (policy.resetPassword ? -- 2.54.0 From dc062820bc2053c6ddb6680a7abd4323ab7b75bc Mon Sep 17 00:00:00 2001 From: agent-exec-transport Date: Fri, 23 Oct 2026 09:56:32 +0000 Subject: [PATCH 3/3] agent-exec transport 6b75522dbe048a8b6a6156edfa6f508e/a250410c65cf --- e2e/admin-user-create.playwright.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/e2e/admin-user-create.playwright.ts b/e2e/admin-user-create.playwright.ts index 60e05bb..cd740e4 100644 --- a/e2e/admin-user-create.playwright.ts +++ b/e2e/admin-user-create.playwright.ts @@ -98,6 +98,6 @@ test('管理员建号:UI 请求新契约并给出待初始化提示', async ({ await expect(row).not.toContainText('ADMIN') // 界面说明与后端流程一致(激活 Key 由服务器本地 CLI 发放)。 - await expect(page.locator('.ad-create .ad-hint')).toContainText('初始密码') - await expect(page.locator('.ad-create .ad-hint')).not.toContainText('user initialize-account') + await expect(page.locator('.ad-create .ad-hint').filter({ hasText: '初始密码' })).toContainText('初始密码') + await expect(page.locator('.ad-create .ad-hint').filter({ hasText: 'user initialize-account' })).toHaveCount(0) }) -- 2.54.0