{
  "config": {
    "argv": [
      "/opt/hostedtoolcache/node/20.20.2/x64/bin/node",
      "/home/runner/work/dr-card-web/dr-card-web/node_modules/.bin/playwright",
      "test",
      "tests/e2e/test-gate.spec.ts"
    ],
    "configFile": "/home/runner/work/dr-card-web/dr-card-web/playwright.config.ts",
    "rootDir": "/home/runner/work/dr-card-web/dr-card-web/tests/e2e",
    "failOnFlakyTests": false,
    "forbidOnly": true,
    "fullyParallel": true,
    "globalSetup": null,
    "globalTeardown": null,
    "globalTimeout": 0,
    "grep": {},
    "grepInvert": null,
    "maxFailures": 0,
    "metadata": {
      "ci": {
        "commitHref": "https://github.com/sanchuang-dev/dr-card-web/commit/9378e610659c97fe6d74837fd9a2bfe4be192472",
        "commitHash": "9378e610659c97fe6d74837fd9a2bfe4be192472",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/309",
        "prTitle": "release(test): promote #308 invite overlay and QR preview fix",
        "prBaseHash": "8aab0e5e73edcb985e9ee4658e3105ad29f4dd5a",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/42755390443"
      },
      "gitCommit": {
        "shortHash": "9378e61",
        "hash": "9378e610659c97fe6d74837fd9a2bfe4be192472",
        "subject": "Merge d03abf58bc65391afcf7afe6432ad4fb944dd618 into 8aab0e5e73edcb985e9ee4658e3105ad29f4dd5a",
        "body": "Merge d03abf58bc65391afcf7afe6432ad4fb944dd618 into 8aab0e5e73edcb985e9ee4658e3105ad29f4dd5a\n",
        "author": {
          "name": "Tomz Dang",
          "email": "dangjingtao@163.com",
          "time": 1794284511000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1794284511000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/src/app/router/routes.ts b/src/app/router/routes.ts\nindex 80f41f6..a0dcbb1 100644\n--- a/src/app/router/routes.ts\n+++ b/src/app/router/routes.ts\n@@ -900,9 +900,9 @@ export const ROUTES: RouteMeta[] = [\n   {\n     path: '/buddy/accept',\n     title: '接受邀请',\n-    /* 原型是诗得丽专栏首页背景 + 邀请弹窗，不出现二级返回栏。 */\n+    /* 邀请弹窗覆盖当前正式首页；已废弃的独立诗得丽专栏不再用作背景。 */\n     titleBar: 'plain',\n-    titleBarTitle: 'DearSeed',\n+    titleBarTitle: '极地种子品牌专栏',\n     nodes: [36],\n     task: 'T007',\n     entry: 'App 内诗得丽扫一扫解析成功后/通知中心接受邀请',\ndiff --git a/src/pages/BuddyAccept.test.tsx b/src/pages/BuddyAccept.test.tsx\nindex 58f7055..558fd29 100644\n--- a/src/pages/BuddyAccept.test.tsx\n+++ b/src/pages/BuddyAccept.test.tsx\n@@ -21,7 +21,7 @@ vi.mock('../services/buddyRelations', async () => {\n   const actual = await vi.importActual<typeof import('../services/buddyRelations')>('../services/buddyRelations')\n   return { ...actual, previewBuddyQr: mocks.preview, acceptBuddyQr: mocks.accept }\n })\n-vi.mock('./DearseedColumn', () => ({ default: () => <div data-testid=\"dearseed-background\" /> }))\n+vi.mock('./Home', () => ({ default: () => <div data-testid=\"current-home-background\" /> }))\n \n import BuddyAccept from './BuddyAccept'\n \n@@ -52,9 +52,10 @@ describe('H5 scan invite confirmation', () => {\n     mount({ source: 'native-buddy-recognition', raw: 'https://example.com/buddy/invite/scan?token=opaque-identifier-12345678' })\n     await waitFor(() => expect(screen.getByText(/后台邀请人邀请你成为洗头搭子/)).toBeTruthy())\n     expect(screen.getByText(/当前版本暂不支持解除关系/)).toBeTruthy()\n+    expect(screen.getByTestId('current-home-background')).toBeTruthy()\n     expect(mocks.accept).not.toHaveBeenCalled()\n     fireEvent.click(screen.getByRole('button', { name: '取消' }))\n-    expect(mocks.patch).toHaveBeenCalledWith({ state: 'dismissed' })\n+    expect(mocks.navigate).toHaveBeenCalledWith('/', { debug: null }, { replace: true })\n     expect(mocks.accept).not.toHaveBeenCalled()\n   })\n \n@@ -62,22 +63,22 @@ describe('H5 scan invite confirmation', () => {\n     mocks.preview.mockResolvedValue({ inviter, relationship: 'available', demo: true })\n     mocks.accept.mockResolvedValue({ result: 'demo-only', demo: true })\n     mount()\n-    await waitFor(() => expect(screen.getByRole('button', { name: /演示确认/ })).toBeTruthy())\n-    fireEvent.click(screen.getByRole('button', { name: /演示确认/ }))\n+    await waitFor(() => expect(screen.getByRole('button', { name: /演示添加/ })).toBeTruthy())\n+    fireEvent.click(screen.getByRole('button', { name: /演示添加/ }))\n     await waitFor(() => expect(screen.getByText(/不会创建或保存真实搭子关系/)).toBeTruthy())\n     expect(mocks.navigate).not.toHaveBeenCalled()\n   })\n \n-  it('uses server status for already-buddies and self; no confirmation write', async () => {\n+  it('blocks self but never suppresses add on an already-related preview status', async () => {\n     mocks.preview.mockResolvedValue({ inviter, relationship: 'self', demo: false })\n     const { unmount } = mount()\n     await waitFor(() => expect(screen.getByText('不能邀请自己')).toBeTruthy())\n-    expect(screen.queryByRole('button', { name: '确认成为搭子' })).toBeNull()\n+    expect(screen.queryByRole('button', { name: '添加好友' })).toBeNull()\n     unmount()\n     mocks.preview.mockResolvedValue({ inviter, relationship: 'already-buddies', demo: false })\n     mount()\n-    await waitFor(() => expect(screen.getByText('你们已经是搭子啦')).toBeTruthy())\n-    expect(screen.getByRole('button', { name: '查看我的搭子' })).toBeTruthy()\n+    await waitFor(() => expect(screen.getByText(/后台邀请人邀请你成为洗头搭子/)).toBeTruthy())\n+    expect(screen.getByRole('button', { name: '添加好友' })).toBeTruthy()\n     expect(mocks.accept).not.toHaveBeenCalled()\n   })\n \n@@ -91,8 +92,8 @@ describe('H5 scan invite confirmation', () => {\n     })\n     mount({ source: 'native-buddy-recognition', raw: 'https://kbs.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000' })\n     await waitFor(() => expect(screen.getByText(/真实邀请人邀请你成为洗头搭子/)).toBeTruthy())\n-    expect(screen.getByText(/后台尚未开放二维码确认/)).toBeTruthy()\n-    expect((screen.getByRole('button', { name: '确认成为搭子' }) as HTMLButtonElement).disabled).toBe(true)\n+    expect(screen.getByText(/后台暂未开放二维码添加好友接口/)).toBeTruthy()\n+    expect((screen.getByRole('button', { name: '添加好友' }) as HTMLButtonElement).disabled).toBe(true)\n     expect(mocks.accept).not.toHaveBeenCalled()\n   })\n \n@@ -101,7 +102,7 @@ describe('H5 scan invite confirmation', () => {\n     mount()\n     await waitFor(() => expect(screen.getByText('等待诗得丽扫码结果')).toBeTruthy())\n     expect(mocks.preview).not.toHaveBeenCalled()\n-    expect(screen.queryByRole('button', { name: '确认成为搭子' })).toBeNull()\n+    expect(screen.queryByRole('button', { name: '添加好友' })).toBeNull()\n   })\n \n   it('retains failed preview and retries without writing a relation', async () => {\n@@ -110,7 +111,7 @@ describe('H5 scan invite confirmation', () => {\n     mount()\n     await waitFor(() => expect(screen.getByText('网络异常，请稍后重试')).toBeTruthy())\n     fireEvent.click(screen.getByRole('button', { name: '重新查询' }))\n-    await waitFor(() => expect(screen.getByRole('button', { name: /演示确认/ })).toBeTruthy())\n+    await waitFor(() => expect(screen.getByRole('button', { name: /演示添加/ })).toBeTruthy())\n     expect(mocks.accept).not.toHaveBeenCalled()\n   })\n })\ndiff --git a/src/pages/BuddyAccept.tsx b/src/pages/BuddyAccept.tsx\nindex 6fbd1cb..11c17c4 100644\n--- a/src/pages/BuddyAccept.tsx\n+++ b/src/pages/BuddyAccept.tsx\n@@ -12,7 +12,7 @@ import {\n   readBuddyScanNavigation,\n   type BuddyPreview,\n } from '../services/buddyRelations'\n-import DearseedColumn from './DearseedColumn'\n+import Home from './Home'\n \n type ViewState =\n   | { kind: 'loading' }\n@@ -29,7 +29,7 @@ type ViewState =\n export default function BuddyAccept() {\n   const location = useLocation()\n   const fixtureNavigate = useFixtureNavigate()\n-  const { get, patch } = useFixtureQueryControls()\n+  const { get } = useFixtureQueryControls()\n   const debug = useFixtureDebug()\n   const dismissed = get('state') === 'dismissed'\n   const fixtureMode = runtimePolicy.dataMode === 'mock'\n@@ -62,11 +62,11 @@ export default function BuddyAccept() {\n \n   const dismiss = () => {\n     if (sending) return\n-    patch({ state: 'dismissed' })\n+    fixtureNavigate('/', { debug: debug ? '1' : null }, { replace: true })\n   }\n \n   const confirm = async () => {\n-    if (view.kind !== 'ready' || view.preview.relationship !== 'available' || !raw || sending) return\n+    if (view.kind !== 'ready' || ['self', 'unavailable'].includes(view.preview.relationship) || !raw || sending) return\n     setSending(true)\n     setActionError(null)\n     try {\n@@ -85,34 +85,31 @@ export default function BuddyAccept() {\n     }\n   }\n \n-  if (dismissed) return <DearseedColumn />\n+  if (dismissed) return <Home />\n \n   const preview = view.kind === 'ready' ? view.preview : null\n   const relationship = preview?.relationship\n-  const already = relationship === 'already-buddies'\n   const title = view.kind === 'missing' ? '等待诗得丽扫码结果'\n     : view.kind === 'loading' ? '正在查询邀请…'\n       : view.kind === 'failed' ? '邀请查询失败'\n         : view.kind === 'demo-complete' ? '演示确认已完成'\n-          : already ? '你们已经是搭子啦'\n-            : relationship === 'self' ? '不能邀请自己'\n+          : relationship === 'self' ? '不能邀请自己'\n               : relationship === 'unavailable' ? '这张邀请暂不可用'\n                 : `${preview?.inviter.nickname ?? '对方'}邀请你成为洗头搭子`\n   const description = view.kind === 'missing'\n     ? '扫码识别协议尚未接通，请在卡博士 App 内使用诗得丽扫一扫'\n     : view.kind === 'failed' ? view.message\n       : view.kind === 'demo-complete' ? '这只是 H5 演示，不会创建或保存真实搭子关系'\n-        : already ? '你们已建立搭子关系，无需重复绑定'\n-          : relationship === 'self' ? '自己的二维码无法用于成为搭子'\n+        : relationship === 'self' ? '自己的二维码无法用于成为搭子'\n             : relationship === 'unavailable' ? '请确认二维码仍可使用'\n               : preview?.demo ? '演示用户，仅验证确认交互，不会绑定真实账号'\n                 : preview?.confirmationAvailable === false\n-                  ? '已识别真实邀请人；后台尚未开放二维码确认，暂不能建立搭子关系'\n+                  ? '已识别邀请人；后台暂未开放二维码添加好友接口'\n                   : '确认后你们将成为洗头搭子'\n \n   return (\n     <>\n-      <DearseedColumn />\n+      <Home />\n       <PromptOverlay\n         open\n         label=\"接受洗头搭子邀请\"\n@@ -138,16 +135,10 @@ export default function BuddyAccept() {\n         <p role=\"status\" className=\"mt-2 text-sm leading-6 text-text-secondary\">{description}</p>\n         {preview && <p className=\"mt-2 text-xs text-text-secondary\">成为搭子后，当前版本暂不支持解除关系</p>}\n         {actionError && <p role=\"alert\" className=\"mt-2 text-sm text-danger-text\">{actionError}</p>}\n-        {view.kind === 'ready' && relationship === 'available' && (\n+        {view.kind === 'ready' && relationship !== 'self' && relationship !== 'unavailable' && (\n           <Button size=\"large\" leadingIcon={UserRoundCheck} loading={sending}\n             disabled={sending || preview?.confirmationAvailable === false} className=\"mt-5 w-full rounded-full\" onClick={() => void confirm()}>\n-            {preview?.demo ? '演示确认（不真实绑定）' : '确认成为搭子'}\n-          </Button>\n-        )}\n-        {already && (\n-          <Button size=\"large\" className=\"mt-5 w-full rounded-full\"\n-            onClick={() => fixtureNavigate('/buddy', { debug: debug ? '1' : null })}>\n-            查看我的搭子\n+            {preview?.demo ? '演示添加（不真实绑定）' : '添加好友'}\n           </Button>\n         )}\n         {view.kind === 'failed' && (\ndiff --git a/src/services/buddyRelations.test.ts b/src/services/buddyRelations.test.ts\nindex 7c48d97..e8f50ae 100644\n--- a/src/services/buddyRelations.test.ts\n+++ b/src/services/buddyRelations.test.ts\n@@ -125,18 +125,19 @@ describe('real H5 UUID invitation preview without the unsigned binding API', ()\n       confirmationAvailable: false,\n     })\n     expect(live.lookup).toHaveBeenCalledWith(uuid)\n-    expect(live.profile).toHaveBeenCalledOnce()\n-    expect(live.relations).toHaveBeenCalledOnce()\n+    expect(live.profile).not.toHaveBeenCalled()\n+    expect(live.relations).not.toHaveBeenCalled()\n     await expect(acceptBuddyQr(qr)).rejects.toMatchObject({ reason: 'not-configured' })\n   })\n \n-  it('uses real logged-in identity and existing list for self / already-related states', async () => {\n+  it('does not preflight the friendship or self state before showing an inviter', async () => {\n     prepare()\n-    live.profile.mockResolvedValue({ identifyCode: uuid })\n-    expect((await previewBuddyQr(qr)).relationship).toBe('self')\n-    live.profile.mockResolvedValue({ identifyCode: '550e8400-e29b-41d4-a716-446655440111' })\n-    live.relations.mockResolvedValue({ items: [{ id: '9', nickname: '真实邀请人', avatarUrl: null }] })\n-    expect((await previewBuddyQr(qr)).relationship).toBe('already-buddies')\n+    live.profile.mockRejectedValue(new Error('profile unavailable'))\n+    live.relations.mockRejectedValue(new Error('friends unavailable'))\n+    expect((await previewBuddyQr(qr)).relationship).toBe('available')\n+    expect(live.lookup).toHaveBeenCalledWith(uuid)\n+    expect(live.profile).not.toHaveBeenCalled()\n+    expect(live.relations).not.toHaveBeenCalled()\n   })\n \n   it.each([\n@@ -154,7 +155,7 @@ describe('real H5 UUID invitation preview without the unsigned binding API', ()\n \n   it('fails closed on a read endpoint outage and never reports relation creation', async () => {\n     prepare()\n-    live.relations.mockRejectedValue(new Error('down'))\n+    live.lookup.mockRejectedValue(new Error('down'))\n     await expect(previewBuddyQr(qr)).rejects.toMatchObject({ reason: 'request-failed' })\n   })\n })\ndiff --git a/src/services/buddyRelations.ts b/src/services/buddyRelations.ts\nindex cd6ea68..8f912f3 100644\n--- a/src/services/buddyRelations.ts\n+++ b/src/services/buddyRelations.ts\n@@ -2,7 +2,6 @@ import { z } from 'zod'\n import { runtimePolicy } from '../app/config/runtime'\n import { validateBuddyQrUrl } from './buddyQr'\n import { fetchBackendBuddyList, fetchUserByIdentifyCode } from './buddyBackend'\n-import { fetchUserProfile } from './userProfile'\n \n /**\n  * #107 H5-only contract. #105 Backend and #110 Native have not signed their APIs:\n@@ -115,17 +114,10 @@ export async function previewBuddyQr(\n     if (!origin?.trim()) throw new BuddyRelationsError('not-configured')\n     const code = parseBuddyInviteCode(raw, origin)\n     try {\n-      const [inviter, profile, relations] = await Promise.all([\n-        fetchUserByIdentifyCode(code),\n-        fetchUserProfile(),\n-        fetchBackendBuddyList(),\n-      ])\n-      const relationship: BuddyPreviewStatus = profile.identifyCode === code\n-        ? 'self'\n-        : relations.items.some(member => member.id === inviter.id)\n-          ? 'already-buddies'\n-          : 'available'\n-      return { inviter, relationship, demo: false, confirmationAvailable: false }\n+      // The QR preview identifies the inviter only. Friendship/self validation\n+      // must be enforced by the authenticated, idempotent backend confirmation.\n+      const inviter = await fetchUserByIdentifyCode(code)\n+      return { inviter, relationship: 'available', demo: false, confirmationAvailable: false }\n     } catch (error) {\n       if (error instanceof BuddyRelationsError) throw error\n       throw new BuddyRelationsError('request-failed')",
      "actualWorkers": 2
    },
    "preserveOutput": "always",
    "projects": [
      {
        "outputDir": "/home/runner/work/dr-card-web/dr-card-web/test-results/artifacts",
        "repeatEach": 1,
        "retries": 1,
        "metadata": {
          "ci": {
            "commitHref": "https://github.com/sanchuang-dev/dr-card-web/commit/9378e610659c97fe6d74837fd9a2bfe4be192472",
            "commitHash": "9378e610659c97fe6d74837fd9a2bfe4be192472",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/309",
            "prTitle": "release(test): promote #308 invite overlay and QR preview fix",
            "prBaseHash": "8aab0e5e73edcb985e9ee4658e3105ad29f4dd5a",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/42755390443"
          },
          "gitCommit": {
            "shortHash": "9378e61",
            "hash": "9378e610659c97fe6d74837fd9a2bfe4be192472",
            "subject": "Merge d03abf58bc65391afcf7afe6432ad4fb944dd618 into 8aab0e5e73edcb985e9ee4658e3105ad29f4dd5a",
            "body": "Merge d03abf58bc65391afcf7afe6432ad4fb944dd618 into 8aab0e5e73edcb985e9ee4658e3105ad29f4dd5a\n",
            "author": {
              "name": "Tomz Dang",
              "email": "dangjingtao@163.com",
              "time": 1794284511000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1794284511000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/src/app/router/routes.ts b/src/app/router/routes.ts\nindex 80f41f6..a0dcbb1 100644\n--- a/src/app/router/routes.ts\n+++ b/src/app/router/routes.ts\n@@ -900,9 +900,9 @@ export const ROUTES: RouteMeta[] = [\n   {\n     path: '/buddy/accept',\n     title: '接受邀请',\n-    /* 原型是诗得丽专栏首页背景 + 邀请弹窗，不出现二级返回栏。 */\n+    /* 邀请弹窗覆盖当前正式首页；已废弃的独立诗得丽专栏不再用作背景。 */\n     titleBar: 'plain',\n-    titleBarTitle: 'DearSeed',\n+    titleBarTitle: '极地种子品牌专栏',\n     nodes: [36],\n     task: 'T007',\n     entry: 'App 内诗得丽扫一扫解析成功后/通知中心接受邀请',\ndiff --git a/src/pages/BuddyAccept.test.tsx b/src/pages/BuddyAccept.test.tsx\nindex 58f7055..558fd29 100644\n--- a/src/pages/BuddyAccept.test.tsx\n+++ b/src/pages/BuddyAccept.test.tsx\n@@ -21,7 +21,7 @@ vi.mock('../services/buddyRelations', async () => {\n   const actual = await vi.importActual<typeof import('../services/buddyRelations')>('../services/buddyRelations')\n   return { ...actual, previewBuddyQr: mocks.preview, acceptBuddyQr: mocks.accept }\n })\n-vi.mock('./DearseedColumn', () => ({ default: () => <div data-testid=\"dearseed-background\" /> }))\n+vi.mock('./Home', () => ({ default: () => <div data-testid=\"current-home-background\" /> }))\n \n import BuddyAccept from './BuddyAccept'\n \n@@ -52,9 +52,10 @@ describe('H5 scan invite confirmation', () => {\n     mount({ source: 'native-buddy-recognition', raw: 'https://example.com/buddy/invite/scan?token=opaque-identifier-12345678' })\n     await waitFor(() => expect(screen.getByText(/后台邀请人邀请你成为洗头搭子/)).toBeTruthy())\n     expect(screen.getByText(/当前版本暂不支持解除关系/)).toBeTruthy()\n+    expect(screen.getByTestId('current-home-background')).toBeTruthy()\n     expect(mocks.accept).not.toHaveBeenCalled()\n     fireEvent.click(screen.getByRole('button', { name: '取消' }))\n-    expect(mocks.patch).toHaveBeenCalledWith({ state: 'dismissed' })\n+    expect(mocks.navigate).toHaveBeenCalledWith('/', { debug: null }, { replace: true })\n     expect(mocks.accept).not.toHaveBeenCalled()\n   })\n \n@@ -62,22 +63,22 @@ describe('H5 scan invite confirmation', () => {\n     mocks.preview.mockResolvedValue({ inviter, relationship: 'available', demo: true })\n     mocks.accept.mockResolvedValue({ result: 'demo-only', demo: true })\n     mount()\n-    await waitFor(() => expect(screen.getByRole('button', { name: /演示确认/ })).toBeTruthy())\n-    fireEvent.click(screen.getByRole('button', { name: /演示确认/ }))\n+    await waitFor(() => expect(screen.getByRole('button', { name: /演示添加/ })).toBeTruthy())\n+    fireEvent.click(screen.getByRole('button', { name: /演示添加/ }))\n     await waitFor(() => expect(screen.getByText(/不会创建或保存真实搭子关系/)).toBeTruthy())\n     expect(mocks.navigate).not.toHaveBeenCalled()\n   })\n \n-  it('uses server status for already-buddies and self; no confirmation write', async () => {\n+  it('blocks self but never suppresses add on an already-related preview status', async () => {\n     mocks.preview.mockResolvedValue({ inviter, relationship: 'self', demo: false })\n     const { unmount } = mount()\n     await waitFor(() => expect(screen.getByText('不能邀请自己')).toBeTruthy())\n-    expect(screen.queryByRole('button', { name: '确认成为搭子' })).toBeNull()\n+    expect(screen.queryByRole('button', { name: '添加好友' })).toBeNull()\n     unmount()\n     mocks.preview.mockResolvedValue({ inviter, relationship: 'already-buddies', demo: false })\n     mount()\n-    await waitFor(() => expect(screen.getByText('你们已经是搭子啦')).toBeTruthy())\n-    expect(screen.getByRole('button', { name: '查看我的搭子' })).toBeTruthy()\n+    await waitFor(() => expect(screen.getByText(/后台邀请人邀请你成为洗头搭子/)).toBeTruthy())\n+    expect(screen.getByRole('button', { name: '添加好友' })).toBeTruthy()\n     expect(mocks.accept).not.toHaveBeenCalled()\n   })\n \n@@ -91,8 +92,8 @@ describe('H5 scan invite confirmation', () => {\n     })\n     mount({ source: 'native-buddy-recognition', raw: 'https://kbs.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000' })\n     await waitFor(() => expect(screen.getByText(/真实邀请人邀请你成为洗头搭子/)).toBeTruthy())\n-    expect(screen.getByText(/后台尚未开放二维码确认/)).toBeTruthy()\n-    expect((screen.getByRole('button', { name: '确认成为搭子' }) as HTMLButtonElement).disabled).toBe(true)\n+    expect(screen.getByText(/后台暂未开放二维码添加好友接口/)).toBeTruthy()\n+    expect((screen.getByRole('button', { name: '添加好友' }) as HTMLButtonElement).disabled).toBe(true)\n     expect(mocks.accept).not.toHaveBeenCalled()\n   })\n \n@@ -101,7 +102,7 @@ describe('H5 scan invite confirmation', () => {\n     mount()\n     await waitFor(() => expect(screen.getByText('等待诗得丽扫码结果')).toBeTruthy())\n     expect(mocks.preview).not.toHaveBeenCalled()\n-    expect(screen.queryByRole('button', { name: '确认成为搭子' })).toBeNull()\n+    expect(screen.queryByRole('button', { name: '添加好友' })).toBeNull()\n   })\n \n   it('retains failed preview and retries without writing a relation', async () => {\n@@ -110,7 +111,7 @@ describe('H5 scan invite confirmation', () => {\n     mount()\n     await waitFor(() => expect(screen.getByText('网络异常，请稍后重试')).toBeTruthy())\n     fireEvent.click(screen.getByRole('button', { name: '重新查询' }))\n-    await waitFor(() => expect(screen.getByRole('button', { name: /演示确认/ })).toBeTruthy())\n+    await waitFor(() => expect(screen.getByRole('button', { name: /演示添加/ })).toBeTruthy())\n     expect(mocks.accept).not.toHaveBeenCalled()\n   })\n })\ndiff --git a/src/pages/BuddyAccept.tsx b/src/pages/BuddyAccept.tsx\nindex 6fbd1cb..11c17c4 100644\n--- a/src/pages/BuddyAccept.tsx\n+++ b/src/pages/BuddyAccept.tsx\n@@ -12,7 +12,7 @@ import {\n   readBuddyScanNavigation,\n   type BuddyPreview,\n } from '../services/buddyRelations'\n-import DearseedColumn from './DearseedColumn'\n+import Home from './Home'\n \n type ViewState =\n   | { kind: 'loading' }\n@@ -29,7 +29,7 @@ type ViewState =\n export default function BuddyAccept() {\n   const location = useLocation()\n   const fixtureNavigate = useFixtureNavigate()\n-  const { get, patch } = useFixtureQueryControls()\n+  const { get } = useFixtureQueryControls()\n   const debug = useFixtureDebug()\n   const dismissed = get('state') === 'dismissed'\n   const fixtureMode = runtimePolicy.dataMode === 'mock'\n@@ -62,11 +62,11 @@ export default function BuddyAccept() {\n \n   const dismiss = () => {\n     if (sending) return\n-    patch({ state: 'dismissed' })\n+    fixtureNavigate('/', { debug: debug ? '1' : null }, { replace: true })\n   }\n \n   const confirm = async () => {\n-    if (view.kind !== 'ready' || view.preview.relationship !== 'available' || !raw || sending) return\n+    if (view.kind !== 'ready' || ['self', 'unavailable'].includes(view.preview.relationship) || !raw || sending) return\n     setSending(true)\n     setActionError(null)\n     try {\n@@ -85,34 +85,31 @@ export default function BuddyAccept() {\n     }\n   }\n \n-  if (dismissed) return <DearseedColumn />\n+  if (dismissed) return <Home />\n \n   const preview = view.kind === 'ready' ? view.preview : null\n   const relationship = preview?.relationship\n-  const already = relationship === 'already-buddies'\n   const title = view.kind === 'missing' ? '等待诗得丽扫码结果'\n     : view.kind === 'loading' ? '正在查询邀请…'\n       : view.kind === 'failed' ? '邀请查询失败'\n         : view.kind === 'demo-complete' ? '演示确认已完成'\n-          : already ? '你们已经是搭子啦'\n-            : relationship === 'self' ? '不能邀请自己'\n+          : relationship === 'self' ? '不能邀请自己'\n               : relationship === 'unavailable' ? '这张邀请暂不可用'\n                 : `${preview?.inviter.nickname ?? '对方'}邀请你成为洗头搭子`\n   const description = view.kind === 'missing'\n     ? '扫码识别协议尚未接通，请在卡博士 App 内使用诗得丽扫一扫'\n     : view.kind === 'failed' ? view.message\n       : view.kind === 'demo-complete' ? '这只是 H5 演示，不会创建或保存真实搭子关系'\n-        : already ? '你们已建立搭子关系，无需重复绑定'\n-          : relationship === 'self' ? '自己的二维码无法用于成为搭子'\n+        : relationship === 'self' ? '自己的二维码无法用于成为搭子'\n             : relationship === 'unavailable' ? '请确认二维码仍可使用'\n               : preview?.demo ? '演示用户，仅验证确认交互，不会绑定真实账号'\n                 : preview?.confirmationAvailable === false\n-                  ? '已识别真实邀请人；后台尚未开放二维码确认，暂不能建立搭子关系'\n+                  ? '已识别邀请人；后台暂未开放二维码添加好友接口'\n                   : '确认后你们将成为洗头搭子'\n \n   return (\n     <>\n-      <DearseedColumn />\n+      <Home />\n       <PromptOverlay\n         open\n         label=\"接受洗头搭子邀请\"\n@@ -138,16 +135,10 @@ export default function BuddyAccept() {\n         <p role=\"status\" className=\"mt-2 text-sm leading-6 text-text-secondary\">{description}</p>\n         {preview && <p className=\"mt-2 text-xs text-text-secondary\">成为搭子后，当前版本暂不支持解除关系</p>}\n         {actionError && <p role=\"alert\" className=\"mt-2 text-sm text-danger-text\">{actionError}</p>}\n-        {view.kind === 'ready' && relationship === 'available' && (\n+        {view.kind === 'ready' && relationship !== 'self' && relationship !== 'unavailable' && (\n           <Button size=\"large\" leadingIcon={UserRoundCheck} loading={sending}\n             disabled={sending || preview?.confirmationAvailable === false} className=\"mt-5 w-full rounded-full\" onClick={() => void confirm()}>\n-            {preview?.demo ? '演示确认（不真实绑定）' : '确认成为搭子'}\n-          </Button>\n-        )}\n-        {already && (\n-          <Button size=\"large\" className=\"mt-5 w-full rounded-full\"\n-            onClick={() => fixtureNavigate('/buddy', { debug: debug ? '1' : null })}>\n-            查看我的搭子\n+            {preview?.demo ? '演示添加（不真实绑定）' : '添加好友'}\n           </Button>\n         )}\n         {view.kind === 'failed' && (\ndiff --git a/src/services/buddyRelations.test.ts b/src/services/buddyRelations.test.ts\nindex 7c48d97..e8f50ae 100644\n--- a/src/services/buddyRelations.test.ts\n+++ b/src/services/buddyRelations.test.ts\n@@ -125,18 +125,19 @@ describe('real H5 UUID invitation preview without the unsigned binding API', ()\n       confirmationAvailable: false,\n     })\n     expect(live.lookup).toHaveBeenCalledWith(uuid)\n-    expect(live.profile).toHaveBeenCalledOnce()\n-    expect(live.relations).toHaveBeenCalledOnce()\n+    expect(live.profile).not.toHaveBeenCalled()\n+    expect(live.relations).not.toHaveBeenCalled()\n     await expect(acceptBuddyQr(qr)).rejects.toMatchObject({ reason: 'not-configured' })\n   })\n \n-  it('uses real logged-in identity and existing list for self / already-related states', async () => {\n+  it('does not preflight the friendship or self state before showing an inviter', async () => {\n     prepare()\n-    live.profile.mockResolvedValue({ identifyCode: uuid })\n-    expect((await previewBuddyQr(qr)).relationship).toBe('self')\n-    live.profile.mockResolvedValue({ identifyCode: '550e8400-e29b-41d4-a716-446655440111' })\n-    live.relations.mockResolvedValue({ items: [{ id: '9', nickname: '真实邀请人', avatarUrl: null }] })\n-    expect((await previewBuddyQr(qr)).relationship).toBe('already-buddies')\n+    live.profile.mockRejectedValue(new Error('profile unavailable'))\n+    live.relations.mockRejectedValue(new Error('friends unavailable'))\n+    expect((await previewBuddyQr(qr)).relationship).toBe('available')\n+    expect(live.lookup).toHaveBeenCalledWith(uuid)\n+    expect(live.profile).not.toHaveBeenCalled()\n+    expect(live.relations).not.toHaveBeenCalled()\n   })\n \n   it.each([\n@@ -154,7 +155,7 @@ describe('real H5 UUID invitation preview without the unsigned binding API', ()\n \n   it('fails closed on a read endpoint outage and never reports relation creation', async () => {\n     prepare()\n-    live.relations.mockRejectedValue(new Error('down'))\n+    live.lookup.mockRejectedValue(new Error('down'))\n     await expect(previewBuddyQr(qr)).rejects.toMatchObject({ reason: 'request-failed' })\n   })\n })\ndiff --git a/src/services/buddyRelations.ts b/src/services/buddyRelations.ts\nindex cd6ea68..8f912f3 100644\n--- a/src/services/buddyRelations.ts\n+++ b/src/services/buddyRelations.ts\n@@ -2,7 +2,6 @@ import { z } from 'zod'\n import { runtimePolicy } from '../app/config/runtime'\n import { validateBuddyQrUrl } from './buddyQr'\n import { fetchBackendBuddyList, fetchUserByIdentifyCode } from './buddyBackend'\n-import { fetchUserProfile } from './userProfile'\n \n /**\n  * #107 H5-only contract. #105 Backend and #110 Native have not signed their APIs:\n@@ -115,17 +114,10 @@ export async function previewBuddyQr(\n     if (!origin?.trim()) throw new BuddyRelationsError('not-configured')\n     const code = parseBuddyInviteCode(raw, origin)\n     try {\n-      const [inviter, profile, relations] = await Promise.all([\n-        fetchUserByIdentifyCode(code),\n-        fetchUserProfile(),\n-        fetchBackendBuddyList(),\n-      ])\n-      const relationship: BuddyPreviewStatus = profile.identifyCode === code\n-        ? 'self'\n-        : relations.items.some(member => member.id === inviter.id)\n-          ? 'already-buddies'\n-          : 'available'\n-      return { inviter, relationship, demo: false, confirmationAvailable: false }\n+      // The QR preview identifies the inviter only. Friendship/self validation\n+      // must be enforced by the authenticated, idempotent backend confirmation.\n+      const inviter = await fetchUserByIdentifyCode(code)\n+      return { inviter, relationship: 'available', demo: false, confirmationAvailable: false }\n     } catch (error) {\n       if (error instanceof BuddyRelationsError) throw error\n       throw new BuddyRelationsError('request-failed')",
          "actualWorkers": 2
        },
        "id": "chromium-mobile",
        "name": "chromium-mobile",
        "testDir": "/home/runner/work/dr-card-web/dr-card-web/tests/e2e",
        "testIgnore": [],
        "testMatch": [
          "**/*.@(spec|test).?(c|m)[jt]s?(x)"
        ],
        "timeout": 30000
      }
    ],
    "quiet": false,
    "reporter": [
      [
        "line",
        null
      ],
      [
        "html",
        {
          "outputFolder": "playwright-report",
          "open": "never"
        }
      ],
      [
        "json",
        {
          "outputFile": "test-results/playwright-results.json"
        }
      ]
    ],
    "reportSlowTests": {
      "max": 5,
      "threshold": 300000
    },
    "shard": null,
    "tags": [],
    "updateSnapshots": "missing",
    "updateSourceMethod": "patch",
    "version": "1.63.0",
    "workers": 2,
    "webServer": null
  },
  "suites": [
    {
      "title": "test-gate.spec.ts",
      "file": "test-gate.spec.ts",
      "column": 0,
      "line": 0,
      "specs": [
        {
          "title": "@test-gate build identity matches the production-like test policy",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 48,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T04:22:46.671Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-c3b6e0ce5d8c5d8734e5",
          "file": "test-gate.spec.ts",
          "line": 31,
          "column": 1
        },
        {
          "title": "@test-gate browser entry does not start the app router",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 1114,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T04:22:46.681Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8ed37c8041402a2a6b34",
          "file": "test-gate.spec.ts",
          "line": 40,
          "column": 1
        },
        {
          "title": "@test-gate / shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 1087,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T04:22:46.771Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-b030d57053896c93a550",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /profile shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 777,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T04:22:48.068Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8605535a2807e17fcccc",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /settings shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 844,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T04:22:48.076Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-4a2fdc9884c1725de4f8",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate gated browser entry issues no business API request",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 803,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T04:22:48.869Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-09f38a2b8ed735e192d8",
          "file": "test-gate.spec.ts",
          "line": 65,
          "column": 1
        },
        {
          "title": "@test-gate injected native host bypasses the notice and starts the app",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 839,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T04:22:48.935Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-11-10T04:22:45.902Z",
    "duration": 3916.1400000000003,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}