{
  "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/5d70d08ecbeae785ca47f1afe8fff822109d39f7",
        "commitHash": "5d70d08ecbeae785ca47f1afe8fff822109d39f7",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/290",
        "prTitle": "test｜定向晋级 #107 H5 UUID 真实用户预览",
        "prBaseHash": "f145716be6fe96a350235e0cc2c1ffae492c93d6",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/42536567085"
      },
      "gitCommit": {
        "shortHash": "5d70d08",
        "hash": "5d70d08ecbeae785ca47f1afe8fff822109d39f7",
        "subject": "Merge cfa776a684c4924c4e18b056fc2430818cde02dc into f145716be6fe96a350235e0cc2c1ffae492c93d6",
        "body": "Merge cfa776a684c4924c4e18b056fc2430818cde02dc into f145716be6fe96a350235e0cc2c1ffae492c93d6\n",
        "author": {
          "name": "Tomz Dang",
          "email": "dangjingtao@163.com",
          "time": 1794181429000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1794181429000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx\nindex 8fa14ea..31dec60 100644\n--- a/src/app/router/index.tsx\n+++ b/src/app/router/index.tsx\n@@ -251,6 +251,14 @@ const bridgeLabRoutes =\n             return { Component }\n           },\n         },\n+        {\n+          // Non-production test-only H5 probe; not a Native protocol or public binding URL.\n+          path: '/__debug/buddy-qr',\n+          lazy: async () => {\n+            const { default: Component } = await import('../../pages/BuddyQrProbe')\n+            return { Component }\n+          },\n+        },\n       ]\n \n // OSS artifacts are mounted at /h5/releases/<sha>/<run>/, whereas normal H5\ndiff --git a/src/pages/BuddyAccept.test.tsx b/src/pages/BuddyAccept.test.tsx\nindex 59ba684..58f7055 100644\n--- a/src/pages/BuddyAccept.test.tsx\n+++ b/src/pages/BuddyAccept.test.tsx\n@@ -81,6 +81,21 @@ describe('H5 scan invite confirmation', () => {\n     expect(mocks.accept).not.toHaveBeenCalled()\n   })\n \n+  it('shows the real inviter but disables unsupported QR relationship creation', async () => {\n+    mocks.mode = 'api'\n+    mocks.preview.mockResolvedValue({\n+      inviter: { id: '9', nickname: '真实邀请人', avatarUrl: null },\n+      relationship: 'available',\n+      demo: false,\n+      confirmationAvailable: false,\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(mocks.accept).not.toHaveBeenCalled()\n+  })\n+\n   it('does not expose a fake acceptance in API mode without a native recognition result', async () => {\n     mocks.mode = 'api'\n     mount()\ndiff --git a/src/pages/BuddyAccept.tsx b/src/pages/BuddyAccept.tsx\nindex 1cb9a3a..6fbd1cb 100644\n--- a/src/pages/BuddyAccept.tsx\n+++ b/src/pages/BuddyAccept.tsx\n@@ -24,7 +24,7 @@ type ViewState =\n /**\n  * #107: consumes ONLY an in-App, route-state QR recognition result; never the public\n  * /buddy/invite/scan URL. #110 Native must preserve existing device/card redemption.\n- * API/test/prod have no signed #105 adapter yet and fail closed.\n+ * API/test/prod may preview the actual UUID owner, but QR relation writes remain blocked until #105.\n  */\n export default function BuddyAccept() {\n   const location = useLocation()\n@@ -106,7 +106,9 @@ export default function BuddyAccept() {\n           : relationship === 'self' ? '自己的二维码无法用于成为搭子'\n             : relationship === 'unavailable' ? '请确认二维码仍可使用'\n               : preview?.demo ? '演示用户，仅验证确认交互，不会绑定真实账号'\n-                : '确认后你们将成为洗头搭子'\n+                : preview?.confirmationAvailable === false\n+                  ? '已识别真实邀请人；后台尚未开放二维码确认，暂不能建立搭子关系'\n+                  : '确认后你们将成为洗头搭子'\n \n   return (\n     <>\n@@ -138,7 +140,7 @@ export default function BuddyAccept() {\n         {actionError && <p role=\"alert\" className=\"mt-2 text-sm text-danger-text\">{actionError}</p>}\n         {view.kind === 'ready' && relationship === 'available' && (\n           <Button size=\"large\" leadingIcon={UserRoundCheck} loading={sending}\n-            disabled={sending} className=\"mt-5 w-full rounded-full\" onClick={() => void confirm()}>\n+            disabled={sending || preview?.confirmationAvailable === false} className=\"mt-5 w-full rounded-full\" onClick={() => void confirm()}>\n             {preview?.demo ? '演示确认（不真实绑定）' : '确认成为搭子'}\n           </Button>\n         )}\ndiff --git a/src/pages/BuddyQrProbe.test.tsx b/src/pages/BuddyQrProbe.test.tsx\nnew file mode 100644\nindex 0000000..2d771bb\n--- /dev/null\n+++ b/src/pages/BuddyQrProbe.test.tsx\n@@ -0,0 +1,49 @@\n+import { afterEach, describe, expect, it, vi } from 'vitest'\n+import { fireEvent, render, screen, waitFor } from '@testing-library/react'\n+import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'\n+import BuddyQrProbe from './BuddyQrProbe'\n+\n+const qr = 'https://kbs.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000'\n+\n+function ReceiveState() {\n+  const location = useLocation()\n+  const scan = (location.state as { buddyScan?: { source: string; raw: string } } | null)?.buddyScan\n+  return <div data-testid=\"route-scan\">{scan?.source} / {scan?.raw}</div>\n+}\n+\n+function mount() {\n+  return render(\n+    <MemoryRouter initialEntries={['/__debug/buddy-qr']}>\n+      <Routes>\n+        <Route path=\"/__debug/buddy-qr\" element={<BuddyQrProbe />} />\n+        <Route path=\"/buddy/accept\" element={<ReceiveState />} />\n+      </Routes>\n+    </MemoryRouter>,\n+  )\n+}\n+\n+afterEach(() => vi.unstubAllEnvs())\n+\n+describe('#107 H5-only QR acceptance rehearsal entry', () => {\n+  it('transfers a validated QR only via internal route state, never public query parameters', async () => {\n+    vi.stubEnv('VITE_BUDDY_PUBLIC_ORIGIN', 'https://kbs.3cgroup.cn')\n+    mount()\n+    fireEvent.change(screen.getByLabelText('扫码结果 URL'), { target: { value: qr } })\n+    fireEvent.click(screen.getByRole('button', { name: '查询并打开邀请确认页' }))\n+    await waitFor(() => {\n+      expect(screen.getByTestId('route-scan').textContent).toContain('native-buddy-recognition')\n+      expect(screen.getByTestId('route-scan').textContent).toContain(qr)\n+    })\n+  })\n+\n+  it('rejects external hosts and cannot navigate without a trusted origin', () => {\n+    vi.stubEnv('VITE_BUDDY_PUBLIC_ORIGIN', 'https://kbs.3cgroup.cn')\n+    mount()\n+    fireEvent.change(screen.getByLabelText('扫码结果 URL'), {\n+      target: { value: qr.replace('kbs.3cgroup.cn', 'untrusted.example') },\n+    })\n+    fireEvent.click(screen.getByRole('button', { name: '查询并打开邀请确认页' }))\n+    expect(screen.getByRole('alert').textContent).toContain('不是有效的洗头搭子二维码')\n+    expect(screen.queryByTestId('route-scan')).toBeNull()\n+  })\n+})\ndiff --git a/src/pages/BuddyQrProbe.tsx b/src/pages/BuddyQrProbe.tsx\nnew file mode 100644\nindex 0000000..38a9488\n--- /dev/null\n+++ b/src/pages/BuddyQrProbe.tsx\n@@ -0,0 +1,65 @@\n+import { useState, type FormEvent } from 'react'\n+import { useNavigate } from 'react-router-dom'\n+import { parseBuddyInviteCode } from '../services/buddyRelations'\n+\n+/**\n+ * #107 H5-only test entry. It can create the same internal navigation state that a future\n+ * dedicated Native QR recognition handoff will supply, without changing the actual Bridge.\n+ * This module is only imported from the non-production debug route.\n+ */\n+export default function BuddyQrProbe() {\n+  const navigate = useNavigate()\n+  const [raw, setRaw] = useState('')\n+  const [error, setError] = useState<string | null>(null)\n+  const origin = import.meta.env.VITE_BUDDY_PUBLIC_ORIGIN\n+\n+  const preview = (event: FormEvent<HTMLFormElement>) => {\n+    event.preventDefault()\n+    setError(null)\n+    if (!origin?.trim()) {\n+      setError('当前构建未配置官方二维码域名 VITE_BUDDY_PUBLIC_ORIGIN')\n+      return\n+    }\n+    try {\n+      parseBuddyInviteCode(raw, origin)\n+      navigate('/buddy/accept', {\n+        state: { buddyScan: { source: 'native-buddy-recognition', raw } },\n+      })\n+    } catch (cause) {\n+      setError(cause instanceof Error ? cause.message : '二维码内容无效')\n+    }\n+  }\n+\n+  return (\n+    <main className=\"min-h-dvh bg-background px-4 py-8 text-text-primary\">\n+      <div className=\"mx-auto max-w-xl rounded-container border border-border bg-surface p-5\">\n+        <h1 className=\"text-xl font-semibold\">搭子二维码 H5 验收</h1>\n+        <p className=\"mt-2 text-sm leading-6 text-text-secondary\">\n+          手动粘贴真实扫码返回的完整 HTTPS URL，验证 UUID 查询、邀请人资料与确认页。\n+          本入口不调用 Native，也不会代替真实扫码联调或创建搭子关系。\n+        </p>\n+        <p className=\"mt-2 break-all text-xs text-text-tertiary\">\n+          官方域名：{origin || '未配置'}\n+        </p>\n+        <form onSubmit={preview} className=\"mt-5\">\n+          <label className=\"block text-sm font-medium\" htmlFor=\"buddy-qr-input\">\n+            扫码结果 URL\n+          </label>\n+          <textarea\n+            id=\"buddy-qr-input\"\n+            value={raw}\n+            onChange={event => setRaw(event.target.value)}\n+            rows={4}\n+            className=\"mt-2 w-full rounded-control border border-border bg-surface px-3 py-2 font-mono text-xs text-text-primary\"\n+            placeholder=\"https://.../buddy/invite/scan?code=...\"\n+          />\n+          {error && <p role=\"alert\" className=\"mt-2 text-sm text-danger-text\">{error}</p>}\n+          <button type=\"submit\"\n+            className=\"mt-4 rounded-button bg-reward-strong px-4 py-2.5 text-sm font-semibold text-white\">\n+            查询并打开邀请确认页\n+          </button>\n+        </form>\n+      </div>\n+    </main>\n+  )\n+}\ndiff --git a/src/services/buddyRelations.test.ts b/src/services/buddyRelations.test.ts\nindex e160cf1..7c48d97 100644\n--- a/src/services/buddyRelations.test.ts\n+++ b/src/services/buddyRelations.test.ts\n@@ -1,8 +1,24 @@\n-import { describe, expect, it, vi } from 'vitest'\n+import { afterEach, describe, expect, it, vi } from 'vitest'\n \n-vi.mock('../app/config/runtime', () => ({\n-  runtimePolicy: { dataMode: 'mock' },\n+const live = vi.hoisted(() => ({\n+  mode: { dataMode: 'mock' },\n+  lookup: vi.fn(),\n+  profile: vi.fn(),\n+  relations: vi.fn(),\n }))\n+vi.mock('../app/config/runtime', () => ({ runtimePolicy: live.mode }))\n+vi.mock('./buddyBackend', () => ({\n+  fetchUserByIdentifyCode: live.lookup,\n+  fetchBackendBuddyList: live.relations,\n+}))\n+vi.mock('./userProfile', () => ({ fetchUserProfile: live.profile }))\n+afterEach(() => {\n+  live.mode.dataMode = 'mock'\n+  live.lookup.mockReset()\n+  live.profile.mockReset()\n+  live.relations.mockReset()\n+  vi.unstubAllEnvs()\n+})\n \n import {\n   acceptBuddyQr,\n@@ -10,6 +26,7 @@ import {\n   MOCK_BUDDY_SCAN_URL,\n   previewBuddyQr,\n   readBuddyScanNavigation,\n+  parseBuddyInviteCode,\n   type BuddyRelationsBackend,\n } from './buddyRelations'\n \n@@ -83,3 +100,61 @@ describe('QR recognition and relationship backend boundary', () => {\n     await expect(acceptBuddyQr(valid)).rejects.toMatchObject({ reason: 'not-configured' })\n   })\n })\n+\n+describe('real H5 UUID invitation preview without the unsigned binding API', () => {\n+  const origin = 'https://kbs.3cgroup.cn'\n+  const uuid = '550e8400-e29b-41d4-a716-446655440000'\n+  const qr = `${origin}/buddy/invite/scan?code=${uuid}`\n+\n+  function prepare() {\n+    live.mode.dataMode = 'api'\n+    vi.stubEnv('VITE_BUDDY_PUBLIC_ORIGIN', origin)\n+    live.lookup.mockResolvedValue({ id: '9', nickname: '真实邀请人', avatarUrl: null })\n+    live.profile.mockResolvedValue({ identifyCode: '550e8400-e29b-41d4-a716-446655440111' })\n+    live.relations.mockResolvedValue({ items: [] })\n+  }\n+\n+  it('parses exactly the trusted UUID and reads real account data without any write', async () => {\n+    prepare()\n+    expect(parseBuddyInviteCode(qr, origin)).toBe(uuid)\n+    const preview = await previewBuddyQr(qr)\n+    expect(preview).toEqual({\n+      demo: false,\n+      inviter: { id: '9', nickname: '真实邀请人', avatarUrl: null },\n+      relationship: 'available',\n+      confirmationAvailable: false,\n+    })\n+    expect(live.lookup).toHaveBeenCalledWith(uuid)\n+    expect(live.profile).toHaveBeenCalledOnce()\n+    expect(live.relations).toHaveBeenCalledOnce()\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+    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+  })\n+\n+  it.each([\n+    'http://kbs.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000',\n+    'https://evil.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000',\n+    'https://kbs.3cgroup.cn/buddy/invite/scan?code=invalid',\n+    'https://kbs.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000&code=550e8400-e29b-41d4-a716-446655440111',\n+    'https://kbs.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000&redirect=/buddy/accept',\n+    '550e8400-e29b-41d4-a716-446655440000',\n+  ])('rejects untrusted or malformed recognition, never queries the backend: %s', async raw => {\n+    prepare()\n+    await expect(previewBuddyQr(raw)).rejects.toMatchObject({ reason: 'invalid-code' })\n+    expect(live.lookup).not.toHaveBeenCalled()\n+  })\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+    await expect(previewBuddyQr(qr)).rejects.toMatchObject({ reason: 'request-failed' })\n+  })\n+})\ndiff --git a/src/services/buddyRelations.ts b/src/services/buddyRelations.ts\nindex 207f4e4..cd6ea68 100644\n--- a/src/services/buddyRelations.ts\n+++ b/src/services/buddyRelations.ts\n@@ -1,7 +1,8 @@\n import { z } from 'zod'\n import { runtimePolicy } from '../app/config/runtime'\n import { validateBuddyQrUrl } from './buddyQr'\n-import { fetchBackendBuddyList } from './buddyBackend'\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@@ -19,7 +20,7 @@ const previewSchema = z.object({\n   inviter: memberSchema,\n   relationship: z.enum(['available', 'self', 'already-buddies', 'unavailable']),\n })\n-export type BuddyPreview = z.infer<typeof previewSchema> & { demo: boolean }\n+export type BuddyPreview = z.infer<typeof previewSchema> & { demo: boolean; confirmationAvailable?: boolean }\n export type BuddyPreviewStatus = BuddyPreview['relationship']\n \n const acceptSchema = z.object({\n@@ -63,6 +64,22 @@ export function readBuddyScanNavigation(value: unknown): string | null {\n     raw.length > 0 && raw.length <= 2048 ? raw : null\n }\n \n+/** QR recognition is always a complete, trusted HTTPS URL, never a naked UUID or an external query-driven action. */\n+export function parseBuddyInviteCode(raw: string, trustedOrigin: string): string {\n+  try {\n+    validateBuddyQrUrl(raw, trustedOrigin)\n+    const url = new URL(raw)\n+    const values = url.searchParams.getAll('code')\n+    if ([...url.searchParams.keys()].length !== 1 || values.length !== 1 ||\n+        !z.string().uuid().safeParse(values[0]).success) {\n+      throw new Error('unsupported QR parameters')\n+    }\n+    return values[0]!\n+  } catch {\n+    throw new BuddyRelationsError('invalid-code')\n+  }\n+}\n+\n function checkQr(raw: string, adapter?: BuddyRelationsBackend): boolean {\n   // Reject obviously malformed QR data before the backend is wired. A well-formed\n   // QR still requires #105's authenticated server verification, not a client-side guess.\n@@ -91,6 +108,29 @@ export async function previewBuddyQr(\n   raw: string,\n   adapter: BuddyRelationsBackend | null = signedBackend,\n ): Promise<BuddyPreview> {\n+  // #107 independent H5 slice: existing authenticated read-only backend endpoints.\n+  // Relation creation stays blocked until #105 provides its dedicated authorized QR acceptance.\n+  if (!adapter && runtimePolicy.dataMode === 'api') {\n+    const origin = import.meta.env.VITE_BUDDY_PUBLIC_ORIGIN\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+    } catch (error) {\n+      if (error instanceof BuddyRelationsError) throw error\n+      throw new BuddyRelationsError('request-failed')\n+    }\n+  }\n   const demo = checkQr(raw, adapter ?? undefined)\n   if (demo) return {\n     inviter: { id: 'demo-buddy-inviter', nickname: '演示搭子', avatarUrl: null },",
      "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/5d70d08ecbeae785ca47f1afe8fff822109d39f7",
            "commitHash": "5d70d08ecbeae785ca47f1afe8fff822109d39f7",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/290",
            "prTitle": "test｜定向晋级 #107 H5 UUID 真实用户预览",
            "prBaseHash": "f145716be6fe96a350235e0cc2c1ffae492c93d6",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/42536567085"
          },
          "gitCommit": {
            "shortHash": "5d70d08",
            "hash": "5d70d08ecbeae785ca47f1afe8fff822109d39f7",
            "subject": "Merge cfa776a684c4924c4e18b056fc2430818cde02dc into f145716be6fe96a350235e0cc2c1ffae492c93d6",
            "body": "Merge cfa776a684c4924c4e18b056fc2430818cde02dc into f145716be6fe96a350235e0cc2c1ffae492c93d6\n",
            "author": {
              "name": "Tomz Dang",
              "email": "dangjingtao@163.com",
              "time": 1794181429000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1794181429000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/src/app/router/index.tsx b/src/app/router/index.tsx\nindex 8fa14ea..31dec60 100644\n--- a/src/app/router/index.tsx\n+++ b/src/app/router/index.tsx\n@@ -251,6 +251,14 @@ const bridgeLabRoutes =\n             return { Component }\n           },\n         },\n+        {\n+          // Non-production test-only H5 probe; not a Native protocol or public binding URL.\n+          path: '/__debug/buddy-qr',\n+          lazy: async () => {\n+            const { default: Component } = await import('../../pages/BuddyQrProbe')\n+            return { Component }\n+          },\n+        },\n       ]\n \n // OSS artifacts are mounted at /h5/releases/<sha>/<run>/, whereas normal H5\ndiff --git a/src/pages/BuddyAccept.test.tsx b/src/pages/BuddyAccept.test.tsx\nindex 59ba684..58f7055 100644\n--- a/src/pages/BuddyAccept.test.tsx\n+++ b/src/pages/BuddyAccept.test.tsx\n@@ -81,6 +81,21 @@ describe('H5 scan invite confirmation', () => {\n     expect(mocks.accept).not.toHaveBeenCalled()\n   })\n \n+  it('shows the real inviter but disables unsupported QR relationship creation', async () => {\n+    mocks.mode = 'api'\n+    mocks.preview.mockResolvedValue({\n+      inviter: { id: '9', nickname: '真实邀请人', avatarUrl: null },\n+      relationship: 'available',\n+      demo: false,\n+      confirmationAvailable: false,\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(mocks.accept).not.toHaveBeenCalled()\n+  })\n+\n   it('does not expose a fake acceptance in API mode without a native recognition result', async () => {\n     mocks.mode = 'api'\n     mount()\ndiff --git a/src/pages/BuddyAccept.tsx b/src/pages/BuddyAccept.tsx\nindex 1cb9a3a..6fbd1cb 100644\n--- a/src/pages/BuddyAccept.tsx\n+++ b/src/pages/BuddyAccept.tsx\n@@ -24,7 +24,7 @@ type ViewState =\n /**\n  * #107: consumes ONLY an in-App, route-state QR recognition result; never the public\n  * /buddy/invite/scan URL. #110 Native must preserve existing device/card redemption.\n- * API/test/prod have no signed #105 adapter yet and fail closed.\n+ * API/test/prod may preview the actual UUID owner, but QR relation writes remain blocked until #105.\n  */\n export default function BuddyAccept() {\n   const location = useLocation()\n@@ -106,7 +106,9 @@ export default function BuddyAccept() {\n           : relationship === 'self' ? '自己的二维码无法用于成为搭子'\n             : relationship === 'unavailable' ? '请确认二维码仍可使用'\n               : preview?.demo ? '演示用户，仅验证确认交互，不会绑定真实账号'\n-                : '确认后你们将成为洗头搭子'\n+                : preview?.confirmationAvailable === false\n+                  ? '已识别真实邀请人；后台尚未开放二维码确认，暂不能建立搭子关系'\n+                  : '确认后你们将成为洗头搭子'\n \n   return (\n     <>\n@@ -138,7 +140,7 @@ export default function BuddyAccept() {\n         {actionError && <p role=\"alert\" className=\"mt-2 text-sm text-danger-text\">{actionError}</p>}\n         {view.kind === 'ready' && relationship === 'available' && (\n           <Button size=\"large\" leadingIcon={UserRoundCheck} loading={sending}\n-            disabled={sending} className=\"mt-5 w-full rounded-full\" onClick={() => void confirm()}>\n+            disabled={sending || preview?.confirmationAvailable === false} className=\"mt-5 w-full rounded-full\" onClick={() => void confirm()}>\n             {preview?.demo ? '演示确认（不真实绑定）' : '确认成为搭子'}\n           </Button>\n         )}\ndiff --git a/src/pages/BuddyQrProbe.test.tsx b/src/pages/BuddyQrProbe.test.tsx\nnew file mode 100644\nindex 0000000..2d771bb\n--- /dev/null\n+++ b/src/pages/BuddyQrProbe.test.tsx\n@@ -0,0 +1,49 @@\n+import { afterEach, describe, expect, it, vi } from 'vitest'\n+import { fireEvent, render, screen, waitFor } from '@testing-library/react'\n+import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom'\n+import BuddyQrProbe from './BuddyQrProbe'\n+\n+const qr = 'https://kbs.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000'\n+\n+function ReceiveState() {\n+  const location = useLocation()\n+  const scan = (location.state as { buddyScan?: { source: string; raw: string } } | null)?.buddyScan\n+  return <div data-testid=\"route-scan\">{scan?.source} / {scan?.raw}</div>\n+}\n+\n+function mount() {\n+  return render(\n+    <MemoryRouter initialEntries={['/__debug/buddy-qr']}>\n+      <Routes>\n+        <Route path=\"/__debug/buddy-qr\" element={<BuddyQrProbe />} />\n+        <Route path=\"/buddy/accept\" element={<ReceiveState />} />\n+      </Routes>\n+    </MemoryRouter>,\n+  )\n+}\n+\n+afterEach(() => vi.unstubAllEnvs())\n+\n+describe('#107 H5-only QR acceptance rehearsal entry', () => {\n+  it('transfers a validated QR only via internal route state, never public query parameters', async () => {\n+    vi.stubEnv('VITE_BUDDY_PUBLIC_ORIGIN', 'https://kbs.3cgroup.cn')\n+    mount()\n+    fireEvent.change(screen.getByLabelText('扫码结果 URL'), { target: { value: qr } })\n+    fireEvent.click(screen.getByRole('button', { name: '查询并打开邀请确认页' }))\n+    await waitFor(() => {\n+      expect(screen.getByTestId('route-scan').textContent).toContain('native-buddy-recognition')\n+      expect(screen.getByTestId('route-scan').textContent).toContain(qr)\n+    })\n+  })\n+\n+  it('rejects external hosts and cannot navigate without a trusted origin', () => {\n+    vi.stubEnv('VITE_BUDDY_PUBLIC_ORIGIN', 'https://kbs.3cgroup.cn')\n+    mount()\n+    fireEvent.change(screen.getByLabelText('扫码结果 URL'), {\n+      target: { value: qr.replace('kbs.3cgroup.cn', 'untrusted.example') },\n+    })\n+    fireEvent.click(screen.getByRole('button', { name: '查询并打开邀请确认页' }))\n+    expect(screen.getByRole('alert').textContent).toContain('不是有效的洗头搭子二维码')\n+    expect(screen.queryByTestId('route-scan')).toBeNull()\n+  })\n+})\ndiff --git a/src/pages/BuddyQrProbe.tsx b/src/pages/BuddyQrProbe.tsx\nnew file mode 100644\nindex 0000000..38a9488\n--- /dev/null\n+++ b/src/pages/BuddyQrProbe.tsx\n@@ -0,0 +1,65 @@\n+import { useState, type FormEvent } from 'react'\n+import { useNavigate } from 'react-router-dom'\n+import { parseBuddyInviteCode } from '../services/buddyRelations'\n+\n+/**\n+ * #107 H5-only test entry. It can create the same internal navigation state that a future\n+ * dedicated Native QR recognition handoff will supply, without changing the actual Bridge.\n+ * This module is only imported from the non-production debug route.\n+ */\n+export default function BuddyQrProbe() {\n+  const navigate = useNavigate()\n+  const [raw, setRaw] = useState('')\n+  const [error, setError] = useState<string | null>(null)\n+  const origin = import.meta.env.VITE_BUDDY_PUBLIC_ORIGIN\n+\n+  const preview = (event: FormEvent<HTMLFormElement>) => {\n+    event.preventDefault()\n+    setError(null)\n+    if (!origin?.trim()) {\n+      setError('当前构建未配置官方二维码域名 VITE_BUDDY_PUBLIC_ORIGIN')\n+      return\n+    }\n+    try {\n+      parseBuddyInviteCode(raw, origin)\n+      navigate('/buddy/accept', {\n+        state: { buddyScan: { source: 'native-buddy-recognition', raw } },\n+      })\n+    } catch (cause) {\n+      setError(cause instanceof Error ? cause.message : '二维码内容无效')\n+    }\n+  }\n+\n+  return (\n+    <main className=\"min-h-dvh bg-background px-4 py-8 text-text-primary\">\n+      <div className=\"mx-auto max-w-xl rounded-container border border-border bg-surface p-5\">\n+        <h1 className=\"text-xl font-semibold\">搭子二维码 H5 验收</h1>\n+        <p className=\"mt-2 text-sm leading-6 text-text-secondary\">\n+          手动粘贴真实扫码返回的完整 HTTPS URL，验证 UUID 查询、邀请人资料与确认页。\n+          本入口不调用 Native，也不会代替真实扫码联调或创建搭子关系。\n+        </p>\n+        <p className=\"mt-2 break-all text-xs text-text-tertiary\">\n+          官方域名：{origin || '未配置'}\n+        </p>\n+        <form onSubmit={preview} className=\"mt-5\">\n+          <label className=\"block text-sm font-medium\" htmlFor=\"buddy-qr-input\">\n+            扫码结果 URL\n+          </label>\n+          <textarea\n+            id=\"buddy-qr-input\"\n+            value={raw}\n+            onChange={event => setRaw(event.target.value)}\n+            rows={4}\n+            className=\"mt-2 w-full rounded-control border border-border bg-surface px-3 py-2 font-mono text-xs text-text-primary\"\n+            placeholder=\"https://.../buddy/invite/scan?code=...\"\n+          />\n+          {error && <p role=\"alert\" className=\"mt-2 text-sm text-danger-text\">{error}</p>}\n+          <button type=\"submit\"\n+            className=\"mt-4 rounded-button bg-reward-strong px-4 py-2.5 text-sm font-semibold text-white\">\n+            查询并打开邀请确认页\n+          </button>\n+        </form>\n+      </div>\n+    </main>\n+  )\n+}\ndiff --git a/src/services/buddyRelations.test.ts b/src/services/buddyRelations.test.ts\nindex e160cf1..7c48d97 100644\n--- a/src/services/buddyRelations.test.ts\n+++ b/src/services/buddyRelations.test.ts\n@@ -1,8 +1,24 @@\n-import { describe, expect, it, vi } from 'vitest'\n+import { afterEach, describe, expect, it, vi } from 'vitest'\n \n-vi.mock('../app/config/runtime', () => ({\n-  runtimePolicy: { dataMode: 'mock' },\n+const live = vi.hoisted(() => ({\n+  mode: { dataMode: 'mock' },\n+  lookup: vi.fn(),\n+  profile: vi.fn(),\n+  relations: vi.fn(),\n }))\n+vi.mock('../app/config/runtime', () => ({ runtimePolicy: live.mode }))\n+vi.mock('./buddyBackend', () => ({\n+  fetchUserByIdentifyCode: live.lookup,\n+  fetchBackendBuddyList: live.relations,\n+}))\n+vi.mock('./userProfile', () => ({ fetchUserProfile: live.profile }))\n+afterEach(() => {\n+  live.mode.dataMode = 'mock'\n+  live.lookup.mockReset()\n+  live.profile.mockReset()\n+  live.relations.mockReset()\n+  vi.unstubAllEnvs()\n+})\n \n import {\n   acceptBuddyQr,\n@@ -10,6 +26,7 @@ import {\n   MOCK_BUDDY_SCAN_URL,\n   previewBuddyQr,\n   readBuddyScanNavigation,\n+  parseBuddyInviteCode,\n   type BuddyRelationsBackend,\n } from './buddyRelations'\n \n@@ -83,3 +100,61 @@ describe('QR recognition and relationship backend boundary', () => {\n     await expect(acceptBuddyQr(valid)).rejects.toMatchObject({ reason: 'not-configured' })\n   })\n })\n+\n+describe('real H5 UUID invitation preview without the unsigned binding API', () => {\n+  const origin = 'https://kbs.3cgroup.cn'\n+  const uuid = '550e8400-e29b-41d4-a716-446655440000'\n+  const qr = `${origin}/buddy/invite/scan?code=${uuid}`\n+\n+  function prepare() {\n+    live.mode.dataMode = 'api'\n+    vi.stubEnv('VITE_BUDDY_PUBLIC_ORIGIN', origin)\n+    live.lookup.mockResolvedValue({ id: '9', nickname: '真实邀请人', avatarUrl: null })\n+    live.profile.mockResolvedValue({ identifyCode: '550e8400-e29b-41d4-a716-446655440111' })\n+    live.relations.mockResolvedValue({ items: [] })\n+  }\n+\n+  it('parses exactly the trusted UUID and reads real account data without any write', async () => {\n+    prepare()\n+    expect(parseBuddyInviteCode(qr, origin)).toBe(uuid)\n+    const preview = await previewBuddyQr(qr)\n+    expect(preview).toEqual({\n+      demo: false,\n+      inviter: { id: '9', nickname: '真实邀请人', avatarUrl: null },\n+      relationship: 'available',\n+      confirmationAvailable: false,\n+    })\n+    expect(live.lookup).toHaveBeenCalledWith(uuid)\n+    expect(live.profile).toHaveBeenCalledOnce()\n+    expect(live.relations).toHaveBeenCalledOnce()\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+    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+  })\n+\n+  it.each([\n+    'http://kbs.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000',\n+    'https://evil.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000',\n+    'https://kbs.3cgroup.cn/buddy/invite/scan?code=invalid',\n+    'https://kbs.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000&code=550e8400-e29b-41d4-a716-446655440111',\n+    'https://kbs.3cgroup.cn/buddy/invite/scan?code=550e8400-e29b-41d4-a716-446655440000&redirect=/buddy/accept',\n+    '550e8400-e29b-41d4-a716-446655440000',\n+  ])('rejects untrusted or malformed recognition, never queries the backend: %s', async raw => {\n+    prepare()\n+    await expect(previewBuddyQr(raw)).rejects.toMatchObject({ reason: 'invalid-code' })\n+    expect(live.lookup).not.toHaveBeenCalled()\n+  })\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+    await expect(previewBuddyQr(qr)).rejects.toMatchObject({ reason: 'request-failed' })\n+  })\n+})\ndiff --git a/src/services/buddyRelations.ts b/src/services/buddyRelations.ts\nindex 207f4e4..cd6ea68 100644\n--- a/src/services/buddyRelations.ts\n+++ b/src/services/buddyRelations.ts\n@@ -1,7 +1,8 @@\n import { z } from 'zod'\n import { runtimePolicy } from '../app/config/runtime'\n import { validateBuddyQrUrl } from './buddyQr'\n-import { fetchBackendBuddyList } from './buddyBackend'\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@@ -19,7 +20,7 @@ const previewSchema = z.object({\n   inviter: memberSchema,\n   relationship: z.enum(['available', 'self', 'already-buddies', 'unavailable']),\n })\n-export type BuddyPreview = z.infer<typeof previewSchema> & { demo: boolean }\n+export type BuddyPreview = z.infer<typeof previewSchema> & { demo: boolean; confirmationAvailable?: boolean }\n export type BuddyPreviewStatus = BuddyPreview['relationship']\n \n const acceptSchema = z.object({\n@@ -63,6 +64,22 @@ export function readBuddyScanNavigation(value: unknown): string | null {\n     raw.length > 0 && raw.length <= 2048 ? raw : null\n }\n \n+/** QR recognition is always a complete, trusted HTTPS URL, never a naked UUID or an external query-driven action. */\n+export function parseBuddyInviteCode(raw: string, trustedOrigin: string): string {\n+  try {\n+    validateBuddyQrUrl(raw, trustedOrigin)\n+    const url = new URL(raw)\n+    const values = url.searchParams.getAll('code')\n+    if ([...url.searchParams.keys()].length !== 1 || values.length !== 1 ||\n+        !z.string().uuid().safeParse(values[0]).success) {\n+      throw new Error('unsupported QR parameters')\n+    }\n+    return values[0]!\n+  } catch {\n+    throw new BuddyRelationsError('invalid-code')\n+  }\n+}\n+\n function checkQr(raw: string, adapter?: BuddyRelationsBackend): boolean {\n   // Reject obviously malformed QR data before the backend is wired. A well-formed\n   // QR still requires #105's authenticated server verification, not a client-side guess.\n@@ -91,6 +108,29 @@ export async function previewBuddyQr(\n   raw: string,\n   adapter: BuddyRelationsBackend | null = signedBackend,\n ): Promise<BuddyPreview> {\n+  // #107 independent H5 slice: existing authenticated read-only backend endpoints.\n+  // Relation creation stays blocked until #105 provides its dedicated authorized QR acceptance.\n+  if (!adapter && runtimePolicy.dataMode === 'api') {\n+    const origin = import.meta.env.VITE_BUDDY_PUBLIC_ORIGIN\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+    } catch (error) {\n+      if (error instanceof BuddyRelationsError) throw error\n+      throw new BuddyRelationsError('request-failed')\n+    }\n+  }\n   const demo = checkQr(raw, adapter ?? undefined)\n   if (demo) return {\n     inviter: { id: 'demo-buddy-inviter', nickname: '演示搭子', avatarUrl: null },",
          "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": 53,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-08T23:45:47.089Z",
                  "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": 1009,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-08T23:45:47.093Z",
                  "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": 1037,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-08T23:45:47.202Z",
                  "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": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 888,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-08T23:45:48.448Z",
                  "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": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 854,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-08T23:45:48.483Z",
                  "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": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 895,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-08T23:45:49.347Z",
                  "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": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 938,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-08T23:45:49.360Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-11-08T23:45:46.346Z",
    "duration": 3997.909,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}