{
  "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/8d68060022acea708a1805b5ba87ebce94eb360a",
        "commitHash": "8d68060022acea708a1805b5ba87ebce94eb360a",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/288",
        "prTitle": "promote(dev→test): Bridge Lab callback observability",
        "prBaseHash": "5b2b55f6cf5f92784085d90b2ec4dbfa12a6767a",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/42186033358"
      },
      "gitCommit": {
        "shortHash": "8d68060",
        "hash": "8d68060022acea708a1805b5ba87ebce94eb360a",
        "subject": "Merge 46e3459728a35c32c7758c473fc9dbd4b3876d02 into 5b2b55f6cf5f92784085d90b2ec4dbfa12a6767a",
        "body": "Merge 46e3459728a35c32c7758c473fc9dbd4b3876d02 into 5b2b55f6cf5f92784085d90b2ec4dbfa12a6767a\n",
        "author": {
          "name": "Tomz Dang",
          "email": "dangjingtao@163.com",
          "time": 1793952750000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1793952750000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/docs/workbench/evidence/h027-bridge-lab.md b/docs/workbench/evidence/h027-bridge-lab.md\nindex 61d9eeb..ba7fecc 100644\n--- a/docs/workbench/evidence/h027-bridge-lab.md\n+++ b/docs/workbench/evidence/h027-bridge-lab.md\n@@ -179,3 +179,30 @@ H027 Bridge Lab 因此保持两层：\n - iOS Raw Probe：仍保留旧 `getAuthorizationInfo + onToken` 预设，用于旧 App build / 历史协议诊断。\n \n 该更新不删除 H026 messageHandler transport；它只改变当前 production auth protocol 的事实来源。\n+\n+## 11. Async callback observability\n+\n+Bridge Lab subscribes to a passive diagnostic stream emitted by the existing injected-object callback transport. The stream does not create a second callback channel and does not alter request settlement.\n+\n+Each registered async invocation can be inspected through:\n+\n+- Native object and method actually invoked;\n+- generated callbackId and callback endpoints configured for the request;\n+- safe argument shape summary (JSON keys plus callbackId / scanType only; arbitrary string contents are not copied into logs);\n+- actual callback endpoint used by Native;\n+- callbackId pending match / unmatched status;\n+- callback count and capability-specific intermediate / terminal classification;\n+- callback payload type and timeout.\n+\n+Privacy boundary:\n+\n+- scan callback envelope metadata such as numeric Native code, message and scanType remain visible for diagnosis;\n+- scanned QR text (including a legacy top-level string `code`) is masked in the default log view;\n+- explicit reveal remains a user action and exposes only the in-memory current log item;\n+- unknown / unmatched callbacks are fully masked because their originating capability cannot be established;\n+- the observer is subscribed only while Bridge Lab is mounted; with no subscribers, diagnostic events are not delivered or retained;\n+- listener exceptions are isolated and cannot interrupt Native callback dispatch or Promise settlement.\n+\n+The added automated tests cover request correlation, both Android callback aliases, callback counts and stage classification, unmatched callbacks, timeout events, component log rendering and Playwright browser smoke.\n+\n+**Validation boundary:** these tests use stub Native hosts and prove the H5 diagnostic mechanism only. They do not prove that the current App build emits a callback, that the real Native callbackId matches, or that Android/iOS scan business flows work. A real App WebView scan is still required after the change reaches the test environment.\ndiff --git a/docs/workbench/tasks/H027-bridge-lab.md b/docs/workbench/tasks/H027-bridge-lab.md\nindex 36eae8e..32bbb5d 100644\n--- a/docs/workbench/tasks/H027-bridge-lab.md\n+++ b/docs/workbench/tasks/H027-bridge-lab.md\n@@ -12,6 +12,7 @@\n - Android / iOS 宿主协议可通过隔离的 Raw Probe 临时探测；\n - 不为每个新增 Native 方法继续手写一套固定按钮；\n - 调试信息可观察，但敏感返回默认脱敏；\n+- 异步 Native callback 可按 callbackId 追踪实际入口、回调次数、中间态/终态与超时；扫码原文默认脱敏；\n - Lab 只服务 dev / preview / test 联调，不进入 production 正式业务导航或 production bundle。\n \n ## 当前事实\n@@ -29,6 +30,14 @@\n - 已确认的 capability 通过同一 production runtime/facade 链调用；\n - 浏览器或宿主缺失时明确显示 unsupported，不 fake success。\n \n+### Async callback trace\n+\n+- 记录 Native 方法实际调用、object/method、callbackId、候选 callback endpoints 与安全的参数摘要；\n+- 记录每次 Native callback 实际抵达的 endpoint、callbackId 是否命中 pending、回调次数、payload 类型及 capability 阶段判定；\n+- 记录 callback timeout；未匹配 callbackId 也要可见，并继续保持既有 fallback 行为；\n+- callback observer 仅在 Bridge Lab 挂载时订阅；没有 observer 时 transport 不向订阅者分发或保留诊断事件；\n+- 扫码 payload 保留 envelope 状态字段用于诊断，但默认遮蔽二维码正文；显式 reveal 仍受现有调试动作控制��\n+\n ### Android Raw Probe\n \n 支持动态输入：\n@@ -74,6 +83,7 @@\n - iOS Raw Probe 复用 H026 transport，并按 callback name 持久化 single-flight channel 状态；\n - timeout 后 channel 保持 fail-closed，显式 reset 才恢复；\n - Lab 日志记录开始、耗时、结果与错误，并默认脱敏；\n+- Registered async callback 增加被动诊断事件：Native 调用、callbackId、实际回调入口、回调计数、阶段判定、未匹配回调与超时；扫码正文默认遮蔽；\n - production 使用编译期 route gate + lazy import，CI 额外扫描 dist，避免 Bridge Lab / Raw Probe 落入 production bundle；\n - 新增 Vitest / React component / Playwright browser smoke 覆盖。\n \n@@ -87,6 +97,7 @@\n - [x] 敏感返回默认脱敏，raw 查看需要显式动作；\n - [x] 普通浏览器无宿主对象时明确 unsupported；\n - [x] Lab 不进入 production 正式业务导航，并由 bundle gate 验证；\n+- [ ] Registered async callback trace 可关联 request / callbackId / 实际 endpoint / callback 次数 / intermediate-terminal / timeout；扫码正文默认脱敏；\n - [ ] 当前 App WebView 对本轮 Lab 再跑一次真人 smoke：registered `getLoginToken` + 至少一次 Android Raw Probe；\n - [ ] 用户验收后才标记 Accepted。\n \ndiff --git a/scripts/verify-h015-jsbridge.mjs b/scripts/verify-h015-jsbridge.mjs\nindex 3a51702..8fb0d5f 100644\n--- a/scripts/verify-h015-jsbridge.mjs\n+++ b/scripts/verify-h015-jsbridge.mjs\n@@ -42,6 +42,8 @@ function collectFormalH5PageFiles(directory, pageRoot = path.normalize('src/page\n     }\n \n     if (!/\\.[jt]sx?$/.test(entry.name)) return []\n+    // Test fixtures exercise fake Native globals; only shipped page modules are subject to H015.\n+    if (/\\.test\\.[jt]sx?$/.test(entry.name)) return []\n     if (path.normalize(directory) === pageRoot && NATIVE_REFERENCE_TOP_LEVEL_PAGES.has(entry.name)) {\n       return []\n     }\ndiff --git a/src/pages/BridgeLab.test.tsx b/src/pages/BridgeLab.test.tsx\nindex 717e845..f7b8213 100644\n--- a/src/pages/BridgeLab.test.tsx\n+++ b/src/pages/BridgeLab.test.tsx\n@@ -164,6 +164,56 @@ describe('Bridge Lab page', () => {\n     expect(screen.queryByText(/bridge-lab-scan/)).toBeNull()\n   })\n \n+  it('shows scan callback correlation, actual endpoint, both stages, and redacts the QR text', async () => {\n+    usePlatform('android')\n+    let request: { scanType: string; callbackId: string } | undefined\n+    labWindow.androidBridge = {\n+      scanCode(payload: unknown) {\n+        request = JSON.parse(payload as string) as { scanType: string; callbackId: string }\n+      },\n+    }\n+\n+    render(<BridgeLab />)\n+    fireEvent.click(document.querySelector('[data-capability-name=\"scanCode\"]') as HTMLButtonElement)\n+    fireEvent.click(screen.getByRole('button', { name: '调用 scanCode' }))\n+\n+    await waitFor(() => {\n+      expect(request).toBeDefined()\n+      const invocationLog = document.querySelector('[data-bridge-lab-logs]')?.textContent ?? ''\n+      expect(invocationLog).toContain('Native invoke')\n+      expect(invocationLog).toContain('androidBridge')\n+      expect(invocationLog).toContain('scanCode')\n+    })\n+\n+    const logs = document.querySelector('[data-bridge-lab-logs]') as HTMLElement\n+    expect(logs.textContent).toContain(request!.callbackId)\n+    expect(logs.textContent).toContain('\"scanType\": \"all\"')\n+    expect(logs.textContent).toContain('window.nativeBridgeCallback')\n+    expect(logs.textContent).toContain('window.androidBridgeCallback')\n+\n+    labWindow.androidBridgeCallback?.(request!.callbackId, {\n+      code: 0,\n+      message: 'ok',\n+      data: { text: 'buddy-qr-private-value', scanType: 'qr' },\n+    })\n+\n+    await waitFor(() => expect(logs.textContent).toContain('intermediate'))\n+    expect(logs.textContent).toContain('\"text\": \"[REDACTED]\"')\n+    expect(logs.textContent).not.toContain('buddy-qr-private-value')\n+\n+    labWindow.androidBridgeCallback?.(request!.callbackId, {\n+      code: 0,\n+      message: 'ok',\n+      data: { text: 'buddy-qr-private-value', scanType: 'qr' },\n+    })\n+\n+    await waitFor(() => {\n+      expect(logs.textContent).toContain('terminal')\n+      expect(logs.textContent).toContain('callbackCount')\n+    })\n+    expect(logs.textContent).not.toContain('buddy-qr-private-value')\n+  })\n+\n   it('can invoke a registered H032 capability with editable JSON input', async () => {\n     usePlatform('android')\n     labWindow.androidBridge = {\ndiff --git a/src/pages/BridgeLab.tsx b/src/pages/BridgeLab.tsx\nindex 3684419..da12714 100644\n--- a/src/pages/BridgeLab.tsx\n+++ b/src/pages/BridgeLab.tsx\n@@ -1,4 +1,4 @@\n-import { useEffect, useMemo, useRef, useState } from 'react'\n+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\n import {\n   Bug,\n   Eye,\n@@ -15,6 +15,7 @@ import {\n   getNativeBridgeCapabilityCatalog,\n   getNativeBridgeDiagnostics,\n   invokeRegisteredNativeCapabilityForDebug,\n+  subscribeNativeBridgeTransportDebugEvents,\n   type NativeCapabilityName,\n } from '../services/nativeBridge'\n import {\n@@ -34,7 +35,7 @@ type BridgeLabCallbackWindow = Window & {\n   androidBridgeCallback?: (callbackId: string, payload: unknown) => void\n   testFunc?: (params: unknown) => string\n }\n-type LogLevel = 'call' | 'result' | 'callback' | 'error'\n+type LogLevel = 'call' | 'result' | 'callback' | 'error' | 'timeout'\n \n type LabLog = {\n   id: string\n@@ -126,6 +127,79 @@ function errorValue(error: unknown): unknown {\n   return error\n }\n \n+function summarizeNativeArguments(args: readonly unknown[]) {\n+  return args.map((argument) => {\n+    if (typeof argument !== 'string') {\n+      return { type: argument === null ? 'null' : Array.isArray(argument) ? 'array' : typeof argument }\n+    }\n+    try {\n+      const parsed: unknown = JSON.parse(argument)\n+      if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {\n+        const record = parsed as Record<string, unknown>\n+        return {\n+          type: 'JSON string',\n+          keys: Object.keys(record),\n+          ...(typeof record.callbackId === 'string' ? { callbackId: record.callbackId } : {}),\n+          ...(typeof record.scanType === 'string' ? { scanType: record.scanType } : {}),\n+        }\n+      }\n+    } catch {\n+      // Plain strings are represented by length only; their contents may be sensitive.\n+    }\n+    return { type: 'string', length: argument.length }\n+  })\n+}\n+\n+function getNativePayloadType(payload: unknown): string {\n+  if (typeof payload === 'string') {\n+    try {\n+      JSON.parse(payload)\n+      return 'JSON string'\n+    } catch {\n+      return 'string'\n+    }\n+  }\n+  if (payload === null) return 'null'\n+  if (Array.isArray(payload)) return 'array'\n+  return typeof payload\n+}\n+\n+function redactNativeCallbackPayload(\n+  payload: unknown,\n+  methodName: string | undefined,\n+  forceSensitive: boolean,\n+) {\n+  if (methodName !== 'scanCode') return redactBridgeValue(payload, forceSensitive)\n+\n+  let normalized = payload\n+  if (typeof normalized === 'string') {\n+    try {\n+      normalized = JSON.parse(normalized)\n+    } catch {\n+      return { value: '[REDACTED]', redacted: true }\n+    }\n+  }\n+  const redaction = redactBridgeValue(normalized)\n+  if (redaction.value === null || typeof redaction.value !== 'object' || Array.isArray(redaction.value)) {\n+    return { value: '[REDACTED]', redacted: true }\n+  }\n+\n+  const record = redaction.value as Record<string, unknown>\n+  if (typeof record.code === 'string' && record.code.length > 0) {\n+    record.code = '[REDACTED]'\n+    redaction.redacted = true\n+  }\n+  const data = record.data\n+  if (data !== null && typeof data === 'object' && !Array.isArray(data)) {\n+    const dataRecord = data as Record<string, unknown>\n+    if (typeof dataRecord.text === 'string') {\n+      dataRecord.text = '[REDACTED]'\n+      redaction.redacted = true\n+    }\n+  }\n+  return redaction\n+}\n+\n function inputClassName() {\n   return 'h-10 w-full rounded-control border border-border bg-surface px-3 text-sm text-text-primary outline-none transition focus:border-reward-strong'\n }\n@@ -222,21 +296,23 @@ export default function BridgeLab() {\n     }\n   }\n \n-  const appendLog = (\n+  const appendLog = useCallback((\n     level: LogLevel,\n     title: string,\n     options: {\n       value?: unknown\n       forceSensitive?: boolean\n+      redaction?: { value: unknown; redacted: boolean }\n       durationMs?: number\n     } = {},\n   ) => {\n     sequence.current += 1\n     const id = `${Date.now()}-${sequence.current}`\n     const redaction =\n-      options.value === undefined\n+      options.redaction ??\n+      (options.value === undefined\n         ? { value: undefined, redacted: false }\n-        : redactBridgeValue(options.value, options.forceSensitive)\n+        : redactBridgeValue(options.value, options.forceSensitive))\n \n     setLogs((current) =>\n       [\n@@ -253,7 +329,70 @@ export default function BridgeLab() {\n         ...current,\n       ].slice(0, 50),\n     )\n-  }\n+  }, [])\n+\n+  useEffect(() => {\n+    if (!runtimePolicy.bridgeLabEnabled) return\n+\n+    return subscribeNativeBridgeTransportDebugEvents((event) => {\n+      if (event.type === 'request') {\n+        appendLog('call', `Native invoke · ${event.objectName}.${event.methodName}`, {\n+          value: {\n+            callbackId: event.callbackId,\n+            callbackEndpoints: event.callbackNames.map((name) => `window.${name}`),\n+            timeoutMs: event.timeoutMs,\n+            args: summarizeNativeArguments(event.args),\n+          },\n+        })\n+        return\n+      }\n+\n+      if (event.type === 'callback') {\n+        const capability = event.methodName\n+          ? platformCatalog.find((item) => item.name === event.methodName)\n+          : undefined\n+        const endpoint = event.callbackName ? `window.${event.callbackName}` : 'method return'\n+        const stage = !event.pendingMatched\n+          ? 'unmatched callbackId'\n+          : event.terminal === false\n+            ? 'intermediate'\n+            : event.terminal === true\n+              ? 'terminal'\n+              : 'stage unknown'\n+        const payloadRedaction = redactNativeCallbackPayload(\n+          event.payload,\n+          event.methodName,\n+          !event.pendingMatched || capability?.sensitiveResult === true,\n+        )\n+        const details = {\n+          callbackId: event.callbackId,\n+          endpoint,\n+          source: event.source,\n+          pendingMatched: event.pendingMatched,\n+          ...(event.callbackCount === undefined ? {} : { callbackCount: event.callbackCount }),\n+          ...(event.terminal === undefined ? {} : { terminal: event.terminal }),\n+          ...(event.classificationError ? { classificationError: event.classificationError } : {}),\n+          payloadType: getNativePayloadType(event.payload),\n+        }\n+        appendLog(\n+          'callback',\n+          `Native callback · ${endpoint} · ${stage}${event.callbackCount === undefined ? '' : ` · #${event.callbackCount}`}`,\n+          {\n+            value: { ...details, payload: event.payload },\n+            redaction: {\n+              value: { ...details, payload: payloadRedaction.value },\n+              redacted: payloadRedaction.redacted,\n+            },\n+          },\n+        )\n+        return\n+      }\n+\n+      appendLog('timeout', `Native callback timeout · ${event.objectName}.${event.methodName}`, {\n+        value: { callbackId: event.callbackId, timeoutMs: event.timeoutMs },\n+      })\n+    })\n+  }, [appendLog, platformCatalog])\n \n   const runRegisteredCapability = async () => {\n     if (!currentCapability) return\n@@ -270,7 +409,7 @@ export default function BridgeLab() {\n       }\n     }\n \n-    appendLog('call', `capability · ${currentCapability.name}`)\n+    appendLog('call', `H5 requested · capability · ${currentCapability.name}`)\n     const startedAt = performance.now()\n     try {\n       const result = await invokeRegisteredNativeCapabilityForDebug(\n@@ -484,7 +623,7 @@ export default function BridgeLab() {\n                   input JSON\n                 </label>\n                 <p className=\"mb-2 mt-1 text-[11px] leading-4 text-text-tertiary\">\n-                  异步 capability 的 callbackId 由 H5 Runtime 自动生成并注入，无需手填。\n+                  异步 capability 的 callbackId 由 H5 Runtime 自动生成并注入；日志会记录实际调用、回调入口、次数与阶段。扫码正文默认脱敏。\n                 </p>\n                 <textarea\n                   id=\"capability-input\"\n@@ -739,7 +878,7 @@ export default function BridgeLab() {\n               <h2 className=\"text-sm font-semibold\">Native → H5 callback endpoints</h2>\n               <p className=\"mt-1 text-xs leading-5 text-text-tertiary\">\n                 异步能力统一目标为 nativeBridgeCallback；Android 当前已验证的 androidBridgeCallback\n-                作为兼容入口接入同一个 callbackId pending channel。\n+                作为兼容入口接入同一个 callbackId pending channel。实际到达的入口与 callbackId 会自动写入 Lab logs。\n               </p>\n             </div>\n             <div className=\"mt-3 space-y-2 rounded-control bg-surface-subtle p-3 text-xs leading-5 text-text-secondary\">\ndiff --git a/src/services/nativeBridge.ts b/src/services/nativeBridge.ts\nindex 18ba72c..b6dda21 100644\n--- a/src/services/nativeBridge.ts\n+++ b/src/services/nativeBridge.ts\n@@ -23,6 +23,8 @@ import type {\n } from './nativeBridge/types'\n \n export { NativeBridgeError } from './nativeBridge/errors'\n+export { subscribeNativeBridgeTransportDebugEvents } from './nativeBridgeTransport'\n+export type { NativeBridgeTransportDebugEvent } from './nativeBridgeTransport'\n export {\n   getNativeBridgeCapabilityCatalog,\n   getNativeBridgeDiagnostics,\ndiff --git a/src/services/nativeBridgeTransport.callback.test.ts b/src/services/nativeBridgeTransport.callback.test.ts\nindex 916b01a..1c0c059 100644\n--- a/src/services/nativeBridgeTransport.callback.test.ts\n+++ b/src/services/nativeBridgeTransport.callback.test.ts\n@@ -3,6 +3,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'\n import {\n   createCallbackInjectedObjectTransport,\n   INJECTED_CALLBACK_TIMEOUT_MS,\n+  subscribeNativeBridgeTransportDebugEvents,\n+  type NativeBridgeTransportDebugEvent,\n   type NativeTransportResolution,\n   type NativeTransportWindow,\n } from './nativeBridgeTransport'\n@@ -126,6 +128,102 @@ describe('Android injected callback dispatcher', () => {\n     await expect(promise).resolves.toBe('device-finished')\n   })\n \n+  it('emits request correlation and callback-stage diagnostics without changing settlement', async () => {\n+    const events: NativeBridgeTransportDebugEvent[] = []\n+    const unsubscribe = subscribeNativeBridgeTransportDebugEvents((event) => events.push(event))\n+    const requests: Array<{ scanType: string; callbackId: string }> = []\n+    hostWindow.androidBridge = {\n+      scanCode(payload: unknown) {\n+        requests.push(JSON.parse(payload as string) as { scanType: string; callbackId: string })\n+      },\n+    }\n+    const transport = createCallbackInjectedObjectTransport<'all', string>({\n+      objectName: 'androidBridge',\n+      methodName: 'scanCode',\n+      callbackName: 'nativeBridgeCallback',\n+      callbackAliases: ['androidBridgeCallback'],\n+      timeoutMs: 1_000,\n+      serializeArgs: (scanType, callbackId) => [JSON.stringify({ scanType, callbackId })],\n+      isTerminalPayload: (_payload, callbackCount) => callbackCount > 1,\n+      parseResult: (payload) => (payload as { result: string }).result,\n+    })\n+\n+    try {\n+      const promise = invoke(transport.resolve(hostWindow, 'all'))\n+      const request = requests[0]\n+      expect(request).toBeDefined()\n+      expect(events[0]).toMatchObject({\n+        type: 'request',\n+        objectName: 'androidBridge',\n+        methodName: 'scanCode',\n+        callbackId: request.callbackId,\n+        callbackNames: ['nativeBridgeCallback', 'androidBridgeCallback'],\n+        timeoutMs: 1_000,\n+        args: [expect.stringContaining(request.callbackId)],\n+      })\n+\n+      hostWindow.androidBridgeCallback?.(request.callbackId, { result: 'scan-recognized' })\n+      expect(events[1]).toMatchObject({\n+        type: 'callback',\n+        objectName: 'androidBridge',\n+        methodName: 'scanCode',\n+        callbackId: request.callbackId,\n+        callbackName: 'androidBridgeCallback',\n+        source: 'callback',\n+        callbackCount: 1,\n+        pendingMatched: true,\n+        terminal: false,\n+      })\n+\n+      hostWindow.nativeBridgeCallback?.(request.callbackId, { result: 'device-finished' })\n+      expect(events[2]).toMatchObject({\n+        type: 'callback',\n+        callbackId: request.callbackId,\n+        callbackName: 'nativeBridgeCallback',\n+        source: 'callback',\n+        callbackCount: 2,\n+        pendingMatched: true,\n+        terminal: true,\n+      })\n+      await expect(promise).resolves.toBe('device-finished')\n+\n+      hostWindow.androidBridgeCallback?.('unmatched-callback-id', { error: 'unknown' })\n+      expect(events[3]).toMatchObject({\n+        type: 'callback',\n+        callbackId: 'unmatched-callback-id',\n+        callbackName: 'androidBridgeCallback',\n+        pendingMatched: false,\n+      })\n+      expect(events).toHaveLength(4)\n+    } finally {\n+      unsubscribe()\n+    }\n+  })\n+\n+  it('emits a correlated timeout event and still rejects the pending invocation', async () => {\n+    vi.useFakeTimers()\n+    const events: NativeBridgeTransportDebugEvent[] = []\n+    const unsubscribe = subscribeNativeBridgeTransportDebugEvents((event) => events.push(event))\n+    hostWindow.androidBridge = { scanCode() {} }\n+    const transport = createProbeTransport('scanCode', 25)\n+\n+    try {\n+      const promise = invoke(transport.resolve(hostWindow, 'waiting'))\n+      const rejection = expect(promise).rejects.toMatchObject({ code: 'callback-timeout' })\n+      await vi.advanceTimersByTimeAsync(25)\n+      await rejection\n+      expect(events).toContainEqual(expect.objectContaining({\n+        type: 'timeout',\n+        objectName: 'androidBridge',\n+        methodName: 'scanCode',\n+        timeoutMs: 25,\n+        callbackId: expect.any(String),\n+      }))\n+    } finally {\n+      unsubscribe()\n+    }\n+  })\n+\n   it('resolves out-of-order callbacks by callbackId instead of invocation order', async () => {\n     const requests: CallbackRequest[] = []\n     hostWindow.androidBridge = {\ndiff --git a/src/services/nativeBridgeTransport.ts b/src/services/nativeBridgeTransport.ts\nindex b80362d..5370299 100644\n--- a/src/services/nativeBridgeTransport.ts\n+++ b/src/services/nativeBridgeTransport.ts\n@@ -211,6 +211,59 @@ export type InjectedCallbackName =\n   | 'nativeBridgeCallback'\n   | 'androidBridgeCallback'\n \n+export type NativeBridgeTransportDebugEvent =\n+  | {\n+      type: 'request'\n+      objectName: string\n+      methodName: string\n+      callbackId: string\n+      callbackNames: readonly InjectedCallbackName[]\n+      timeoutMs: number\n+      args: readonly unknown[]\n+    }\n+  | {\n+      type: 'callback'\n+      objectName?: string\n+      methodName?: string\n+      callbackId: string\n+      callbackName: InjectedCallbackName | null\n+      source: 'callback' | 'method-return'\n+      callbackCount?: number\n+      pendingMatched: boolean\n+      terminal?: boolean\n+      classificationError?: string\n+      payload: unknown\n+    }\n+  | {\n+      type: 'timeout'\n+      objectName: string\n+      methodName: string\n+      callbackId: string\n+      timeoutMs: number\n+    }\n+\n+type NativeBridgeTransportDebugListener = (event: NativeBridgeTransportDebugEvent) => void\n+const nativeBridgeTransportDebugListeners = new Set<NativeBridgeTransportDebugListener>()\n+\n+/** Passive Bridge Lab observer. Events are delivered only to active debug listeners. */\n+export function subscribeNativeBridgeTransportDebugEvents(\n+  listener: NativeBridgeTransportDebugListener,\n+): () => void {\n+  nativeBridgeTransportDebugListeners.add(listener)\n+  return () => nativeBridgeTransportDebugListeners.delete(listener)\n+}\n+\n+function emitNativeBridgeTransportDebugEvent(event: NativeBridgeTransportDebugEvent): void {\n+  if (nativeBridgeTransportDebugListeners.size === 0) return\n+  for (const listener of nativeBridgeTransportDebugListeners) {\n+    try {\n+      listener(event)\n+    } catch {\n+      // Diagnostics must never interfere with Native callback delivery or Promise settlement.\n+    }\n+  }\n+}\n+\n export interface CallbackInjectedObjectTransportConfig<TInput, TResult>\n   extends Omit<InjectedObjectTransportConfig<TInput, TResult>, 'serializeArgs'> {\n   serializeArgs: (input: TInput, callbackId: string) => readonly unknown[]\n@@ -232,7 +285,7 @@ export interface CallbackInjectedObjectTransportConfig<TInput, TResult>\n }\n \n type InjectedCallbackPending = {\n-  handle: (payload: unknown) => void\n+  handle: (payload: unknown, callbackName: InjectedCallbackName) => void\n }\n \n type InjectedCallbackDispatcher = (callbackId: string, payload: unknown) => void\n@@ -265,9 +318,17 @@ function installInjectedCallbackEndpoint(\n     dispatcher = (callbackId, payload) => {\n       const request = channel.pending.get(callbackId)\n       if (request) {\n-        request.handle(payload)\n+        request.handle(payload, callbackName)\n         return\n       }\n+      emitNativeBridgeTransportDebugEvent({\n+        type: 'callback',\n+        callbackId,\n+        callbackName,\n+        source: 'callback',\n+        pendingMatched: false,\n+        payload,\n+      })\n       channel.fallbacks.get(callbackName)?.call(hostWindow, callbackId, payload)\n     }\n     channel.dispatchers.set(callbackName, dispatcher)\n@@ -348,6 +409,7 @@ export function createCallbackInjectedObjectTransport<TInput, TResult>(\n           }\n \n           const callbackName = config.callbackName ?? 'nativeBridgeCallback'\n+          const callbackNames = [...new Set([callbackName, ...(config.callbackAliases ?? [])])]\n           const channel = getInjectedCallbackChannel(\n             hostWindow,\n             callbackName,\n@@ -377,13 +439,29 @@ export function createCallbackInjectedObjectTransport<TInput, TResult>(\n               cleanup()\n               work()\n             }\n-            const handlePayload = (payload: unknown) => {\n+            const handlePayload = (\n+              payload: unknown,\n+              source: 'callback' | 'method-return',\n+              endpoint: InjectedCallbackName | null,\n+            ) => {\n               callbackCount += 1\n \n               let isTerminal = true\n               try {\n                 isTerminal = config.isTerminalPayload?.(payload, callbackCount) ?? true\n               } catch (error) {\n+                emitNativeBridgeTransportDebugEvent({\n+                  type: 'callback',\n+                  objectName: config.objectName,\n+                  methodName: config.methodName,\n+                  callbackId,\n+                  callbackName: endpoint,\n+                  source,\n+                  callbackCount,\n+                  pendingMatched: true,\n+                  classificationError: error instanceof Error ? error.message : String(error),\n+                  payload,\n+                })\n                 finish(() =>\n                   reject(\n                     error instanceof NativeTransportError\n@@ -398,6 +476,19 @@ export function createCallbackInjectedObjectTransport<TInput, TResult>(\n                 return\n               }\n \n+              emitNativeBridgeTransportDebugEvent({\n+                type: 'callback',\n+                objectName: config.objectName,\n+                methodName: config.methodName,\n+                callbackId,\n+                callbackName: endpoint,\n+                source,\n+                callbackCount,\n+                pendingMatched: true,\n+                terminal: isTerminal,\n+                payload,\n+              })\n+\n               if (!isTerminal) return\n \n               finish(() => {\n@@ -409,8 +500,17 @@ export function createCallbackInjectedObjectTransport<TInput, TResult>(\n               })\n             }\n \n-            channel.pending.set(callbackId, { handle: handlePayload })\n+            channel.pending.set(callbackId, {\n+              handle: (payload, endpoint) => handlePayload(payload, 'callback', endpoint),\n+            })\n             const timer = window.setTimeout(() => {\n+              emitNativeBridgeTransportDebugEvent({\n+                type: 'timeout',\n+                objectName: config.objectName,\n+                methodName: config.methodName,\n+                callbackId,\n+                timeoutMs,\n+              })\n               finish(() =>\n                 reject(\n                   new NativeTransportError(\n@@ -423,8 +523,17 @@ export function createCallbackInjectedObjectTransport<TInput, TResult>(\n \n             try {\n               const args = config.serializeArgs(input, callbackId)\n+              emitNativeBridgeTransportDebugEvent({\n+                type: 'request',\n+                objectName: config.objectName,\n+                methodName: config.methodName,\n+                callbackId,\n+                callbackNames,\n+                timeoutMs,\n+                args,\n+              })\n               const rawResult = method.call(bridge, ...args)\n-              if (rawResult !== undefined) handlePayload(rawResult)\n+              if (rawResult !== undefined) handlePayload(rawResult, 'method-return', null)\n             } catch (error) {\n               finish(() =>\n                 reject(\ndiff --git a/tests/e2e/bridge-lab.spec.ts b/tests/e2e/bridge-lab.spec.ts\nindex 6f416f7..7bcc433 100644\n--- a/tests/e2e/bridge-lab.spec.ts\n+++ b/tests/e2e/bridge-lab.spec.ts\n@@ -59,6 +59,46 @@ test.describe('H027 Bridge Lab', () => {\n     await expect(logs).toContainText('\"success\": true')\n   })\n \n+  test('shows correlated Android scan callback stages without exposing QR text by default', async ({ page }) => {\n+    await page.addInitScript(() => {\n+      const host = window as unknown as {\n+        androidBridge?: { scanCode(payload: unknown): void }\n+        androidBridgeCallback?: (callbackId: string, payload: unknown) => void\n+      }\n+      host.androidBridge = {\n+        scanCode(payload) {\n+          const request = JSON.parse(payload as string) as { callbackId: string }\n+          setTimeout(() => {\n+            host.androidBridgeCallback?.(request.callbackId, {\n+              code: 0,\n+              message: 'ok',\n+              data: { text: 'e2e-buddy-qr-private', scanType: 'qr' },\n+            })\n+          }, 0)\n+          setTimeout(() => {\n+            host.androidBridgeCallback?.(request.callbackId, {\n+              code: 0,\n+              message: 'ok',\n+              data: { text: 'e2e-buddy-qr-private', scanType: 'qr' },\n+            })\n+          }, 20)\n+        },\n+      }\n+    })\n+\n+    await page.goto('/__debug/bridge-lab?osType=android', { waitUntil: 'domcontentloaded' })\n+    await page.locator('[data-capability-name=\"scanCode\"]').click()\n+    await page.getByRole('button', { name: '调用 scanCode' }).click()\n+\n+    const logs = page.locator('[data-bridge-lab-logs]')\n+    await expect(logs).toContainText('Native invoke · androidBridge.scanCode')\n+    await expect(logs).toContainText('androidBridgeCallback')\n+    await expect(logs).toContainText('intermediate')\n+    await expect(logs).toContainText('terminal')\n+    await expect(logs).toContainText('[REDACTED]')\n+    await expect(logs).not.toContainText('e2e-buddy-qr-private')\n+  })\n+\n   test('Native can still call the legacy window.testFunc endpoint', async ({ page }) => {\n     await page.goto('/__debug/bridge-lab?osType=android', { waitUntil: 'domcontentloaded' })",
      "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/8d68060022acea708a1805b5ba87ebce94eb360a",
            "commitHash": "8d68060022acea708a1805b5ba87ebce94eb360a",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/288",
            "prTitle": "promote(dev→test): Bridge Lab callback observability",
            "prBaseHash": "5b2b55f6cf5f92784085d90b2ec4dbfa12a6767a",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/42186033358"
          },
          "gitCommit": {
            "shortHash": "8d68060",
            "hash": "8d68060022acea708a1805b5ba87ebce94eb360a",
            "subject": "Merge 46e3459728a35c32c7758c473fc9dbd4b3876d02 into 5b2b55f6cf5f92784085d90b2ec4dbfa12a6767a",
            "body": "Merge 46e3459728a35c32c7758c473fc9dbd4b3876d02 into 5b2b55f6cf5f92784085d90b2ec4dbfa12a6767a\n",
            "author": {
              "name": "Tomz Dang",
              "email": "dangjingtao@163.com",
              "time": 1793952750000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1793952750000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/docs/workbench/evidence/h027-bridge-lab.md b/docs/workbench/evidence/h027-bridge-lab.md\nindex 61d9eeb..ba7fecc 100644\n--- a/docs/workbench/evidence/h027-bridge-lab.md\n+++ b/docs/workbench/evidence/h027-bridge-lab.md\n@@ -179,3 +179,30 @@ H027 Bridge Lab 因此保持两层：\n - iOS Raw Probe：仍保留旧 `getAuthorizationInfo + onToken` 预设，用于旧 App build / 历史协议诊断。\n \n 该更新不删除 H026 messageHandler transport；它只改变当前 production auth protocol 的事实来源。\n+\n+## 11. Async callback observability\n+\n+Bridge Lab subscribes to a passive diagnostic stream emitted by the existing injected-object callback transport. The stream does not create a second callback channel and does not alter request settlement.\n+\n+Each registered async invocation can be inspected through:\n+\n+- Native object and method actually invoked;\n+- generated callbackId and callback endpoints configured for the request;\n+- safe argument shape summary (JSON keys plus callbackId / scanType only; arbitrary string contents are not copied into logs);\n+- actual callback endpoint used by Native;\n+- callbackId pending match / unmatched status;\n+- callback count and capability-specific intermediate / terminal classification;\n+- callback payload type and timeout.\n+\n+Privacy boundary:\n+\n+- scan callback envelope metadata such as numeric Native code, message and scanType remain visible for diagnosis;\n+- scanned QR text (including a legacy top-level string `code`) is masked in the default log view;\n+- explicit reveal remains a user action and exposes only the in-memory current log item;\n+- unknown / unmatched callbacks are fully masked because their originating capability cannot be established;\n+- the observer is subscribed only while Bridge Lab is mounted; with no subscribers, diagnostic events are not delivered or retained;\n+- listener exceptions are isolated and cannot interrupt Native callback dispatch or Promise settlement.\n+\n+The added automated tests cover request correlation, both Android callback aliases, callback counts and stage classification, unmatched callbacks, timeout events, component log rendering and Playwright browser smoke.\n+\n+**Validation boundary:** these tests use stub Native hosts and prove the H5 diagnostic mechanism only. They do not prove that the current App build emits a callback, that the real Native callbackId matches, or that Android/iOS scan business flows work. A real App WebView scan is still required after the change reaches the test environment.\ndiff --git a/docs/workbench/tasks/H027-bridge-lab.md b/docs/workbench/tasks/H027-bridge-lab.md\nindex 36eae8e..32bbb5d 100644\n--- a/docs/workbench/tasks/H027-bridge-lab.md\n+++ b/docs/workbench/tasks/H027-bridge-lab.md\n@@ -12,6 +12,7 @@\n - Android / iOS 宿主协议可通过隔离的 Raw Probe 临时探测；\n - 不为每个新增 Native 方法继续手写一套固定按钮；\n - 调试信息可观察，但敏感返回默认脱敏；\n+- 异步 Native callback 可按 callbackId 追踪实际入口、回调次数、中间态/终态与超时；扫码原文默认脱敏；\n - Lab 只服务 dev / preview / test 联调，不进入 production 正式业务导航或 production bundle。\n \n ## 当前事实\n@@ -29,6 +30,14 @@\n - 已确认的 capability 通过同一 production runtime/facade 链调用；\n - 浏览器或宿主缺失时明确显示 unsupported，不 fake success。\n \n+### Async callback trace\n+\n+- 记录 Native 方法实际调用、object/method、callbackId、候选 callback endpoints 与安全的参数摘要；\n+- 记录每次 Native callback 实际抵达的 endpoint、callbackId 是否命中 pending、回调次数、payload 类型及 capability 阶段判定；\n+- 记录 callback timeout；未匹配 callbackId 也要可见，并继续保持既有 fallback 行为；\n+- callback observer 仅在 Bridge Lab 挂载时订阅；没有 observer 时 transport 不向订阅者分发或保留诊断事件；\n+- 扫码 payload 保留 envelope 状态字段用于诊断，但默认遮蔽二维码正文；显式 reveal 仍受现有调试动作控制��\n+\n ### Android Raw Probe\n \n 支持动态输入：\n@@ -74,6 +83,7 @@\n - iOS Raw Probe 复用 H026 transport，并按 callback name 持久化 single-flight channel 状态；\n - timeout 后 channel 保持 fail-closed，显式 reset 才恢复；\n - Lab 日志记录开始、耗时、结果与错误，并默认脱敏；\n+- Registered async callback 增加被动诊断事件：Native 调用、callbackId、实际回调入口、回调计数、阶段判定、未匹配回调与超时；扫码正文默认遮蔽；\n - production 使用编译期 route gate + lazy import，CI 额外扫描 dist，避免 Bridge Lab / Raw Probe 落入 production bundle；\n - 新增 Vitest / React component / Playwright browser smoke 覆盖。\n \n@@ -87,6 +97,7 @@\n - [x] 敏感返回默认脱敏，raw 查看需要显式动作；\n - [x] 普通浏览器无宿主对象时明确 unsupported；\n - [x] Lab 不进入 production 正式业务导航，并由 bundle gate 验证；\n+- [ ] Registered async callback trace 可关联 request / callbackId / 实际 endpoint / callback 次数 / intermediate-terminal / timeout；扫码正文默认脱敏；\n - [ ] 当前 App WebView 对本轮 Lab 再跑一次真人 smoke：registered `getLoginToken` + 至少一次 Android Raw Probe；\n - [ ] 用户验收后才标记 Accepted。\n \ndiff --git a/scripts/verify-h015-jsbridge.mjs b/scripts/verify-h015-jsbridge.mjs\nindex 3a51702..8fb0d5f 100644\n--- a/scripts/verify-h015-jsbridge.mjs\n+++ b/scripts/verify-h015-jsbridge.mjs\n@@ -42,6 +42,8 @@ function collectFormalH5PageFiles(directory, pageRoot = path.normalize('src/page\n     }\n \n     if (!/\\.[jt]sx?$/.test(entry.name)) return []\n+    // Test fixtures exercise fake Native globals; only shipped page modules are subject to H015.\n+    if (/\\.test\\.[jt]sx?$/.test(entry.name)) return []\n     if (path.normalize(directory) === pageRoot && NATIVE_REFERENCE_TOP_LEVEL_PAGES.has(entry.name)) {\n       return []\n     }\ndiff --git a/src/pages/BridgeLab.test.tsx b/src/pages/BridgeLab.test.tsx\nindex 717e845..f7b8213 100644\n--- a/src/pages/BridgeLab.test.tsx\n+++ b/src/pages/BridgeLab.test.tsx\n@@ -164,6 +164,56 @@ describe('Bridge Lab page', () => {\n     expect(screen.queryByText(/bridge-lab-scan/)).toBeNull()\n   })\n \n+  it('shows scan callback correlation, actual endpoint, both stages, and redacts the QR text', async () => {\n+    usePlatform('android')\n+    let request: { scanType: string; callbackId: string } | undefined\n+    labWindow.androidBridge = {\n+      scanCode(payload: unknown) {\n+        request = JSON.parse(payload as string) as { scanType: string; callbackId: string }\n+      },\n+    }\n+\n+    render(<BridgeLab />)\n+    fireEvent.click(document.querySelector('[data-capability-name=\"scanCode\"]') as HTMLButtonElement)\n+    fireEvent.click(screen.getByRole('button', { name: '调用 scanCode' }))\n+\n+    await waitFor(() => {\n+      expect(request).toBeDefined()\n+      const invocationLog = document.querySelector('[data-bridge-lab-logs]')?.textContent ?? ''\n+      expect(invocationLog).toContain('Native invoke')\n+      expect(invocationLog).toContain('androidBridge')\n+      expect(invocationLog).toContain('scanCode')\n+    })\n+\n+    const logs = document.querySelector('[data-bridge-lab-logs]') as HTMLElement\n+    expect(logs.textContent).toContain(request!.callbackId)\n+    expect(logs.textContent).toContain('\"scanType\": \"all\"')\n+    expect(logs.textContent).toContain('window.nativeBridgeCallback')\n+    expect(logs.textContent).toContain('window.androidBridgeCallback')\n+\n+    labWindow.androidBridgeCallback?.(request!.callbackId, {\n+      code: 0,\n+      message: 'ok',\n+      data: { text: 'buddy-qr-private-value', scanType: 'qr' },\n+    })\n+\n+    await waitFor(() => expect(logs.textContent).toContain('intermediate'))\n+    expect(logs.textContent).toContain('\"text\": \"[REDACTED]\"')\n+    expect(logs.textContent).not.toContain('buddy-qr-private-value')\n+\n+    labWindow.androidBridgeCallback?.(request!.callbackId, {\n+      code: 0,\n+      message: 'ok',\n+      data: { text: 'buddy-qr-private-value', scanType: 'qr' },\n+    })\n+\n+    await waitFor(() => {\n+      expect(logs.textContent).toContain('terminal')\n+      expect(logs.textContent).toContain('callbackCount')\n+    })\n+    expect(logs.textContent).not.toContain('buddy-qr-private-value')\n+  })\n+\n   it('can invoke a registered H032 capability with editable JSON input', async () => {\n     usePlatform('android')\n     labWindow.androidBridge = {\ndiff --git a/src/pages/BridgeLab.tsx b/src/pages/BridgeLab.tsx\nindex 3684419..da12714 100644\n--- a/src/pages/BridgeLab.tsx\n+++ b/src/pages/BridgeLab.tsx\n@@ -1,4 +1,4 @@\n-import { useEffect, useMemo, useRef, useState } from 'react'\n+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\n import {\n   Bug,\n   Eye,\n@@ -15,6 +15,7 @@ import {\n   getNativeBridgeCapabilityCatalog,\n   getNativeBridgeDiagnostics,\n   invokeRegisteredNativeCapabilityForDebug,\n+  subscribeNativeBridgeTransportDebugEvents,\n   type NativeCapabilityName,\n } from '../services/nativeBridge'\n import {\n@@ -34,7 +35,7 @@ type BridgeLabCallbackWindow = Window & {\n   androidBridgeCallback?: (callbackId: string, payload: unknown) => void\n   testFunc?: (params: unknown) => string\n }\n-type LogLevel = 'call' | 'result' | 'callback' | 'error'\n+type LogLevel = 'call' | 'result' | 'callback' | 'error' | 'timeout'\n \n type LabLog = {\n   id: string\n@@ -126,6 +127,79 @@ function errorValue(error: unknown): unknown {\n   return error\n }\n \n+function summarizeNativeArguments(args: readonly unknown[]) {\n+  return args.map((argument) => {\n+    if (typeof argument !== 'string') {\n+      return { type: argument === null ? 'null' : Array.isArray(argument) ? 'array' : typeof argument }\n+    }\n+    try {\n+      const parsed: unknown = JSON.parse(argument)\n+      if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {\n+        const record = parsed as Record<string, unknown>\n+        return {\n+          type: 'JSON string',\n+          keys: Object.keys(record),\n+          ...(typeof record.callbackId === 'string' ? { callbackId: record.callbackId } : {}),\n+          ...(typeof record.scanType === 'string' ? { scanType: record.scanType } : {}),\n+        }\n+      }\n+    } catch {\n+      // Plain strings are represented by length only; their contents may be sensitive.\n+    }\n+    return { type: 'string', length: argument.length }\n+  })\n+}\n+\n+function getNativePayloadType(payload: unknown): string {\n+  if (typeof payload === 'string') {\n+    try {\n+      JSON.parse(payload)\n+      return 'JSON string'\n+    } catch {\n+      return 'string'\n+    }\n+  }\n+  if (payload === null) return 'null'\n+  if (Array.isArray(payload)) return 'array'\n+  return typeof payload\n+}\n+\n+function redactNativeCallbackPayload(\n+  payload: unknown,\n+  methodName: string | undefined,\n+  forceSensitive: boolean,\n+) {\n+  if (methodName !== 'scanCode') return redactBridgeValue(payload, forceSensitive)\n+\n+  let normalized = payload\n+  if (typeof normalized === 'string') {\n+    try {\n+      normalized = JSON.parse(normalized)\n+    } catch {\n+      return { value: '[REDACTED]', redacted: true }\n+    }\n+  }\n+  const redaction = redactBridgeValue(normalized)\n+  if (redaction.value === null || typeof redaction.value !== 'object' || Array.isArray(redaction.value)) {\n+    return { value: '[REDACTED]', redacted: true }\n+  }\n+\n+  const record = redaction.value as Record<string, unknown>\n+  if (typeof record.code === 'string' && record.code.length > 0) {\n+    record.code = '[REDACTED]'\n+    redaction.redacted = true\n+  }\n+  const data = record.data\n+  if (data !== null && typeof data === 'object' && !Array.isArray(data)) {\n+    const dataRecord = data as Record<string, unknown>\n+    if (typeof dataRecord.text === 'string') {\n+      dataRecord.text = '[REDACTED]'\n+      redaction.redacted = true\n+    }\n+  }\n+  return redaction\n+}\n+\n function inputClassName() {\n   return 'h-10 w-full rounded-control border border-border bg-surface px-3 text-sm text-text-primary outline-none transition focus:border-reward-strong'\n }\n@@ -222,21 +296,23 @@ export default function BridgeLab() {\n     }\n   }\n \n-  const appendLog = (\n+  const appendLog = useCallback((\n     level: LogLevel,\n     title: string,\n     options: {\n       value?: unknown\n       forceSensitive?: boolean\n+      redaction?: { value: unknown; redacted: boolean }\n       durationMs?: number\n     } = {},\n   ) => {\n     sequence.current += 1\n     const id = `${Date.now()}-${sequence.current}`\n     const redaction =\n-      options.value === undefined\n+      options.redaction ??\n+      (options.value === undefined\n         ? { value: undefined, redacted: false }\n-        : redactBridgeValue(options.value, options.forceSensitive)\n+        : redactBridgeValue(options.value, options.forceSensitive))\n \n     setLogs((current) =>\n       [\n@@ -253,7 +329,70 @@ export default function BridgeLab() {\n         ...current,\n       ].slice(0, 50),\n     )\n-  }\n+  }, [])\n+\n+  useEffect(() => {\n+    if (!runtimePolicy.bridgeLabEnabled) return\n+\n+    return subscribeNativeBridgeTransportDebugEvents((event) => {\n+      if (event.type === 'request') {\n+        appendLog('call', `Native invoke · ${event.objectName}.${event.methodName}`, {\n+          value: {\n+            callbackId: event.callbackId,\n+            callbackEndpoints: event.callbackNames.map((name) => `window.${name}`),\n+            timeoutMs: event.timeoutMs,\n+            args: summarizeNativeArguments(event.args),\n+          },\n+        })\n+        return\n+      }\n+\n+      if (event.type === 'callback') {\n+        const capability = event.methodName\n+          ? platformCatalog.find((item) => item.name === event.methodName)\n+          : undefined\n+        const endpoint = event.callbackName ? `window.${event.callbackName}` : 'method return'\n+        const stage = !event.pendingMatched\n+          ? 'unmatched callbackId'\n+          : event.terminal === false\n+            ? 'intermediate'\n+            : event.terminal === true\n+              ? 'terminal'\n+              : 'stage unknown'\n+        const payloadRedaction = redactNativeCallbackPayload(\n+          event.payload,\n+          event.methodName,\n+          !event.pendingMatched || capability?.sensitiveResult === true,\n+        )\n+        const details = {\n+          callbackId: event.callbackId,\n+          endpoint,\n+          source: event.source,\n+          pendingMatched: event.pendingMatched,\n+          ...(event.callbackCount === undefined ? {} : { callbackCount: event.callbackCount }),\n+          ...(event.terminal === undefined ? {} : { terminal: event.terminal }),\n+          ...(event.classificationError ? { classificationError: event.classificationError } : {}),\n+          payloadType: getNativePayloadType(event.payload),\n+        }\n+        appendLog(\n+          'callback',\n+          `Native callback · ${endpoint} · ${stage}${event.callbackCount === undefined ? '' : ` · #${event.callbackCount}`}`,\n+          {\n+            value: { ...details, payload: event.payload },\n+            redaction: {\n+              value: { ...details, payload: payloadRedaction.value },\n+              redacted: payloadRedaction.redacted,\n+            },\n+          },\n+        )\n+        return\n+      }\n+\n+      appendLog('timeout', `Native callback timeout · ${event.objectName}.${event.methodName}`, {\n+        value: { callbackId: event.callbackId, timeoutMs: event.timeoutMs },\n+      })\n+    })\n+  }, [appendLog, platformCatalog])\n \n   const runRegisteredCapability = async () => {\n     if (!currentCapability) return\n@@ -270,7 +409,7 @@ export default function BridgeLab() {\n       }\n     }\n \n-    appendLog('call', `capability · ${currentCapability.name}`)\n+    appendLog('call', `H5 requested · capability · ${currentCapability.name}`)\n     const startedAt = performance.now()\n     try {\n       const result = await invokeRegisteredNativeCapabilityForDebug(\n@@ -484,7 +623,7 @@ export default function BridgeLab() {\n                   input JSON\n                 </label>\n                 <p className=\"mb-2 mt-1 text-[11px] leading-4 text-text-tertiary\">\n-                  异步 capability 的 callbackId 由 H5 Runtime 自动生成并注入，无需手填。\n+                  异步 capability 的 callbackId 由 H5 Runtime 自动生成并注入；日志会记录实际调用、回调入口、次数与阶段。扫码正文默认脱敏。\n                 </p>\n                 <textarea\n                   id=\"capability-input\"\n@@ -739,7 +878,7 @@ export default function BridgeLab() {\n               <h2 className=\"text-sm font-semibold\">Native → H5 callback endpoints</h2>\n               <p className=\"mt-1 text-xs leading-5 text-text-tertiary\">\n                 异步能力统一目标为 nativeBridgeCallback；Android 当前已验证的 androidBridgeCallback\n-                作为兼容入口接入同一个 callbackId pending channel。\n+                作为兼容入口接入同一个 callbackId pending channel。实际到达的入口与 callbackId 会自动写入 Lab logs。\n               </p>\n             </div>\n             <div className=\"mt-3 space-y-2 rounded-control bg-surface-subtle p-3 text-xs leading-5 text-text-secondary\">\ndiff --git a/src/services/nativeBridge.ts b/src/services/nativeBridge.ts\nindex 18ba72c..b6dda21 100644\n--- a/src/services/nativeBridge.ts\n+++ b/src/services/nativeBridge.ts\n@@ -23,6 +23,8 @@ import type {\n } from './nativeBridge/types'\n \n export { NativeBridgeError } from './nativeBridge/errors'\n+export { subscribeNativeBridgeTransportDebugEvents } from './nativeBridgeTransport'\n+export type { NativeBridgeTransportDebugEvent } from './nativeBridgeTransport'\n export {\n   getNativeBridgeCapabilityCatalog,\n   getNativeBridgeDiagnostics,\ndiff --git a/src/services/nativeBridgeTransport.callback.test.ts b/src/services/nativeBridgeTransport.callback.test.ts\nindex 916b01a..1c0c059 100644\n--- a/src/services/nativeBridgeTransport.callback.test.ts\n+++ b/src/services/nativeBridgeTransport.callback.test.ts\n@@ -3,6 +3,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'\n import {\n   createCallbackInjectedObjectTransport,\n   INJECTED_CALLBACK_TIMEOUT_MS,\n+  subscribeNativeBridgeTransportDebugEvents,\n+  type NativeBridgeTransportDebugEvent,\n   type NativeTransportResolution,\n   type NativeTransportWindow,\n } from './nativeBridgeTransport'\n@@ -126,6 +128,102 @@ describe('Android injected callback dispatcher', () => {\n     await expect(promise).resolves.toBe('device-finished')\n   })\n \n+  it('emits request correlation and callback-stage diagnostics without changing settlement', async () => {\n+    const events: NativeBridgeTransportDebugEvent[] = []\n+    const unsubscribe = subscribeNativeBridgeTransportDebugEvents((event) => events.push(event))\n+    const requests: Array<{ scanType: string; callbackId: string }> = []\n+    hostWindow.androidBridge = {\n+      scanCode(payload: unknown) {\n+        requests.push(JSON.parse(payload as string) as { scanType: string; callbackId: string })\n+      },\n+    }\n+    const transport = createCallbackInjectedObjectTransport<'all', string>({\n+      objectName: 'androidBridge',\n+      methodName: 'scanCode',\n+      callbackName: 'nativeBridgeCallback',\n+      callbackAliases: ['androidBridgeCallback'],\n+      timeoutMs: 1_000,\n+      serializeArgs: (scanType, callbackId) => [JSON.stringify({ scanType, callbackId })],\n+      isTerminalPayload: (_payload, callbackCount) => callbackCount > 1,\n+      parseResult: (payload) => (payload as { result: string }).result,\n+    })\n+\n+    try {\n+      const promise = invoke(transport.resolve(hostWindow, 'all'))\n+      const request = requests[0]\n+      expect(request).toBeDefined()\n+      expect(events[0]).toMatchObject({\n+        type: 'request',\n+        objectName: 'androidBridge',\n+        methodName: 'scanCode',\n+        callbackId: request.callbackId,\n+        callbackNames: ['nativeBridgeCallback', 'androidBridgeCallback'],\n+        timeoutMs: 1_000,\n+        args: [expect.stringContaining(request.callbackId)],\n+      })\n+\n+      hostWindow.androidBridgeCallback?.(request.callbackId, { result: 'scan-recognized' })\n+      expect(events[1]).toMatchObject({\n+        type: 'callback',\n+        objectName: 'androidBridge',\n+        methodName: 'scanCode',\n+        callbackId: request.callbackId,\n+        callbackName: 'androidBridgeCallback',\n+        source: 'callback',\n+        callbackCount: 1,\n+        pendingMatched: true,\n+        terminal: false,\n+      })\n+\n+      hostWindow.nativeBridgeCallback?.(request.callbackId, { result: 'device-finished' })\n+      expect(events[2]).toMatchObject({\n+        type: 'callback',\n+        callbackId: request.callbackId,\n+        callbackName: 'nativeBridgeCallback',\n+        source: 'callback',\n+        callbackCount: 2,\n+        pendingMatched: true,\n+        terminal: true,\n+      })\n+      await expect(promise).resolves.toBe('device-finished')\n+\n+      hostWindow.androidBridgeCallback?.('unmatched-callback-id', { error: 'unknown' })\n+      expect(events[3]).toMatchObject({\n+        type: 'callback',\n+        callbackId: 'unmatched-callback-id',\n+        callbackName: 'androidBridgeCallback',\n+        pendingMatched: false,\n+      })\n+      expect(events).toHaveLength(4)\n+    } finally {\n+      unsubscribe()\n+    }\n+  })\n+\n+  it('emits a correlated timeout event and still rejects the pending invocation', async () => {\n+    vi.useFakeTimers()\n+    const events: NativeBridgeTransportDebugEvent[] = []\n+    const unsubscribe = subscribeNativeBridgeTransportDebugEvents((event) => events.push(event))\n+    hostWindow.androidBridge = { scanCode() {} }\n+    const transport = createProbeTransport('scanCode', 25)\n+\n+    try {\n+      const promise = invoke(transport.resolve(hostWindow, 'waiting'))\n+      const rejection = expect(promise).rejects.toMatchObject({ code: 'callback-timeout' })\n+      await vi.advanceTimersByTimeAsync(25)\n+      await rejection\n+      expect(events).toContainEqual(expect.objectContaining({\n+        type: 'timeout',\n+        objectName: 'androidBridge',\n+        methodName: 'scanCode',\n+        timeoutMs: 25,\n+        callbackId: expect.any(String),\n+      }))\n+    } finally {\n+      unsubscribe()\n+    }\n+  })\n+\n   it('resolves out-of-order callbacks by callbackId instead of invocation order', async () => {\n     const requests: CallbackRequest[] = []\n     hostWindow.androidBridge = {\ndiff --git a/src/services/nativeBridgeTransport.ts b/src/services/nativeBridgeTransport.ts\nindex b80362d..5370299 100644\n--- a/src/services/nativeBridgeTransport.ts\n+++ b/src/services/nativeBridgeTransport.ts\n@@ -211,6 +211,59 @@ export type InjectedCallbackName =\n   | 'nativeBridgeCallback'\n   | 'androidBridgeCallback'\n \n+export type NativeBridgeTransportDebugEvent =\n+  | {\n+      type: 'request'\n+      objectName: string\n+      methodName: string\n+      callbackId: string\n+      callbackNames: readonly InjectedCallbackName[]\n+      timeoutMs: number\n+      args: readonly unknown[]\n+    }\n+  | {\n+      type: 'callback'\n+      objectName?: string\n+      methodName?: string\n+      callbackId: string\n+      callbackName: InjectedCallbackName | null\n+      source: 'callback' | 'method-return'\n+      callbackCount?: number\n+      pendingMatched: boolean\n+      terminal?: boolean\n+      classificationError?: string\n+      payload: unknown\n+    }\n+  | {\n+      type: 'timeout'\n+      objectName: string\n+      methodName: string\n+      callbackId: string\n+      timeoutMs: number\n+    }\n+\n+type NativeBridgeTransportDebugListener = (event: NativeBridgeTransportDebugEvent) => void\n+const nativeBridgeTransportDebugListeners = new Set<NativeBridgeTransportDebugListener>()\n+\n+/** Passive Bridge Lab observer. Events are delivered only to active debug listeners. */\n+export function subscribeNativeBridgeTransportDebugEvents(\n+  listener: NativeBridgeTransportDebugListener,\n+): () => void {\n+  nativeBridgeTransportDebugListeners.add(listener)\n+  return () => nativeBridgeTransportDebugListeners.delete(listener)\n+}\n+\n+function emitNativeBridgeTransportDebugEvent(event: NativeBridgeTransportDebugEvent): void {\n+  if (nativeBridgeTransportDebugListeners.size === 0) return\n+  for (const listener of nativeBridgeTransportDebugListeners) {\n+    try {\n+      listener(event)\n+    } catch {\n+      // Diagnostics must never interfere with Native callback delivery or Promise settlement.\n+    }\n+  }\n+}\n+\n export interface CallbackInjectedObjectTransportConfig<TInput, TResult>\n   extends Omit<InjectedObjectTransportConfig<TInput, TResult>, 'serializeArgs'> {\n   serializeArgs: (input: TInput, callbackId: string) => readonly unknown[]\n@@ -232,7 +285,7 @@ export interface CallbackInjectedObjectTransportConfig<TInput, TResult>\n }\n \n type InjectedCallbackPending = {\n-  handle: (payload: unknown) => void\n+  handle: (payload: unknown, callbackName: InjectedCallbackName) => void\n }\n \n type InjectedCallbackDispatcher = (callbackId: string, payload: unknown) => void\n@@ -265,9 +318,17 @@ function installInjectedCallbackEndpoint(\n     dispatcher = (callbackId, payload) => {\n       const request = channel.pending.get(callbackId)\n       if (request) {\n-        request.handle(payload)\n+        request.handle(payload, callbackName)\n         return\n       }\n+      emitNativeBridgeTransportDebugEvent({\n+        type: 'callback',\n+        callbackId,\n+        callbackName,\n+        source: 'callback',\n+        pendingMatched: false,\n+        payload,\n+      })\n       channel.fallbacks.get(callbackName)?.call(hostWindow, callbackId, payload)\n     }\n     channel.dispatchers.set(callbackName, dispatcher)\n@@ -348,6 +409,7 @@ export function createCallbackInjectedObjectTransport<TInput, TResult>(\n           }\n \n           const callbackName = config.callbackName ?? 'nativeBridgeCallback'\n+          const callbackNames = [...new Set([callbackName, ...(config.callbackAliases ?? [])])]\n           const channel = getInjectedCallbackChannel(\n             hostWindow,\n             callbackName,\n@@ -377,13 +439,29 @@ export function createCallbackInjectedObjectTransport<TInput, TResult>(\n               cleanup()\n               work()\n             }\n-            const handlePayload = (payload: unknown) => {\n+            const handlePayload = (\n+              payload: unknown,\n+              source: 'callback' | 'method-return',\n+              endpoint: InjectedCallbackName | null,\n+            ) => {\n               callbackCount += 1\n \n               let isTerminal = true\n               try {\n                 isTerminal = config.isTerminalPayload?.(payload, callbackCount) ?? true\n               } catch (error) {\n+                emitNativeBridgeTransportDebugEvent({\n+                  type: 'callback',\n+                  objectName: config.objectName,\n+                  methodName: config.methodName,\n+                  callbackId,\n+                  callbackName: endpoint,\n+                  source,\n+                  callbackCount,\n+                  pendingMatched: true,\n+                  classificationError: error instanceof Error ? error.message : String(error),\n+                  payload,\n+                })\n                 finish(() =>\n                   reject(\n                     error instanceof NativeTransportError\n@@ -398,6 +476,19 @@ export function createCallbackInjectedObjectTransport<TInput, TResult>(\n                 return\n               }\n \n+              emitNativeBridgeTransportDebugEvent({\n+                type: 'callback',\n+                objectName: config.objectName,\n+                methodName: config.methodName,\n+                callbackId,\n+                callbackName: endpoint,\n+                source,\n+                callbackCount,\n+                pendingMatched: true,\n+                terminal: isTerminal,\n+                payload,\n+              })\n+\n               if (!isTerminal) return\n \n               finish(() => {\n@@ -409,8 +500,17 @@ export function createCallbackInjectedObjectTransport<TInput, TResult>(\n               })\n             }\n \n-            channel.pending.set(callbackId, { handle: handlePayload })\n+            channel.pending.set(callbackId, {\n+              handle: (payload, endpoint) => handlePayload(payload, 'callback', endpoint),\n+            })\n             const timer = window.setTimeout(() => {\n+              emitNativeBridgeTransportDebugEvent({\n+                type: 'timeout',\n+                objectName: config.objectName,\n+                methodName: config.methodName,\n+                callbackId,\n+                timeoutMs,\n+              })\n               finish(() =>\n                 reject(\n                   new NativeTransportError(\n@@ -423,8 +523,17 @@ export function createCallbackInjectedObjectTransport<TInput, TResult>(\n \n             try {\n               const args = config.serializeArgs(input, callbackId)\n+              emitNativeBridgeTransportDebugEvent({\n+                type: 'request',\n+                objectName: config.objectName,\n+                methodName: config.methodName,\n+                callbackId,\n+                callbackNames,\n+                timeoutMs,\n+                args,\n+              })\n               const rawResult = method.call(bridge, ...args)\n-              if (rawResult !== undefined) handlePayload(rawResult)\n+              if (rawResult !== undefined) handlePayload(rawResult, 'method-return', null)\n             } catch (error) {\n               finish(() =>\n                 reject(\ndiff --git a/tests/e2e/bridge-lab.spec.ts b/tests/e2e/bridge-lab.spec.ts\nindex 6f416f7..7bcc433 100644\n--- a/tests/e2e/bridge-lab.spec.ts\n+++ b/tests/e2e/bridge-lab.spec.ts\n@@ -59,6 +59,46 @@ test.describe('H027 Bridge Lab', () => {\n     await expect(logs).toContainText('\"success\": true')\n   })\n \n+  test('shows correlated Android scan callback stages without exposing QR text by default', async ({ page }) => {\n+    await page.addInitScript(() => {\n+      const host = window as unknown as {\n+        androidBridge?: { scanCode(payload: unknown): void }\n+        androidBridgeCallback?: (callbackId: string, payload: unknown) => void\n+      }\n+      host.androidBridge = {\n+        scanCode(payload) {\n+          const request = JSON.parse(payload as string) as { callbackId: string }\n+          setTimeout(() => {\n+            host.androidBridgeCallback?.(request.callbackId, {\n+              code: 0,\n+              message: 'ok',\n+              data: { text: 'e2e-buddy-qr-private', scanType: 'qr' },\n+            })\n+          }, 0)\n+          setTimeout(() => {\n+            host.androidBridgeCallback?.(request.callbackId, {\n+              code: 0,\n+              message: 'ok',\n+              data: { text: 'e2e-buddy-qr-private', scanType: 'qr' },\n+            })\n+          }, 20)\n+        },\n+      }\n+    })\n+\n+    await page.goto('/__debug/bridge-lab?osType=android', { waitUntil: 'domcontentloaded' })\n+    await page.locator('[data-capability-name=\"scanCode\"]').click()\n+    await page.getByRole('button', { name: '调用 scanCode' }).click()\n+\n+    const logs = page.locator('[data-bridge-lab-logs]')\n+    await expect(logs).toContainText('Native invoke · androidBridge.scanCode')\n+    await expect(logs).toContainText('androidBridgeCallback')\n+    await expect(logs).toContainText('intermediate')\n+    await expect(logs).toContainText('terminal')\n+    await expect(logs).toContainText('[REDACTED]')\n+    await expect(logs).not.toContainText('e2e-buddy-qr-private')\n+  })\n+\n   test('Native can still call the legacy window.testFunc endpoint', async ({ page }) => {\n     await page.goto('/__debug/bridge-lab?osType=android', { waitUntil: 'domcontentloaded' })",
          "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": 70,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-06T08:13:37.127Z",
                  "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": 1226,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-06T08:13:37.133Z",
                  "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": 1319,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-06T08:13:37.253Z",
                  "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": 1043,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-06T08:13:38.786Z",
                  "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": 1104,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-06T08:13:38.878Z",
                  "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": 1000,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-06T08:13:39.846Z",
                  "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": 999,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-06T08:13:40.004Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-11-06T08:13:36.135Z",
    "duration": 4927.275000000001,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}