{
  "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/be8ba2423bdb8db651b7bfc02515d96260779e24",
        "commitHash": "be8ba2423bdb8db651b7bfc02515d96260779e24",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/215",
        "prTitle": "release(test): 晋级 #213 HTTP 认证凭证注入修复",
        "prBaseHash": "82661bb129ce89966563e4d224d71ee8c58df36e",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/38354507765"
      },
      "gitCommit": {
        "shortHash": "be8ba24",
        "hash": "be8ba2423bdb8db651b7bfc02515d96260779e24",
        "subject": "Merge 245ffe221e20cc4c3bc2f64b4b31c385f18f364d into 82661bb129ce89966563e4d224d71ee8c58df36e",
        "body": "Merge 245ffe221e20cc4c3bc2f64b4b31c385f18f364d into 82661bb129ce89966563e4d224d71ee8c58df36e\n",
        "author": {
          "name": "Tomz Dang",
          "email": "dangjingtao@163.com",
          "time": 1791871328000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1791871328000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/src/services/auth/session.notice-auth.test.ts b/src/services/auth/session.notice-auth.test.ts\nnew file mode 100644\nindex 0000000..75243a0\n--- /dev/null\n+++ b/src/services/auth/session.notice-auth.test.ts\n@@ -0,0 +1,107 @@\n+import { beforeEach, describe, expect, it, vi } from 'vitest'\n+import type { AxiosAdapter } from 'axios'\n+\n+const probes = vi.hoisted(() => ({\n+  getLoginToken: vi.fn(),\n+  requests: [] as Array<{ url: string; authorization: string | undefined }>,\n+  issuedToken: 'access-A',\n+  unread401Once: false,\n+}))\n+\n+vi.mock('../../app/config/runtime', () => ({\n+  runtimePolicy: { apiBaseUrl: 'https://api.example.test', dataMode: 'api' },\n+}))\n+\n+vi.mock('../nativeBridge', () => ({\n+  getLoginToken: probes.getLoginToken,\n+}))\n+\n+// Keep the real Axios interceptors and notice service. Substitute only the transport.\n+vi.mock('../http/httpClient', async importOriginal => {\n+  const actual = await importOriginal<typeof import('../http/httpClient')>()\n+  const adapter: AxiosAdapter = async (config) => {\n+    const url = config.url ?? ''\n+    probes.requests.push({\n+      url,\n+      authorization: config.headers.get('Authorization')?.toString(),\n+    })\n+    let data: unknown = { ok: true }\n+    if (url === '/api/oauth/login') {\n+      data = { code: 0, data: { userInfo: { id: 9 }, accessToken: probes.issuedToken } }\n+    } else if (url === '/api/notices/unread-count') {\n+      if (probes.unread401Once) {\n+        probes.unread401Once = false\n+        data = { code: 401, message: '请先登录' }\n+      } else {\n+        data = { code: 0, data: { total: 2, items: [] } }\n+      }\n+    }\n+    return { data, status: 200, statusText: 'OK', headers: {}, config }\n+  }\n+  return {\n+    ...actual,\n+    createHttpClient: (options: Parameters<typeof actual.createHttpClient>[0]) =>\n+      actual.createHttpClient({ ...options, adapter }),\n+    httpClient: actual.createHttpClient({ baseURL: 'https://api.example.test', adapter }),\n+  }\n+})\n+\n+import { authenticate, clearAuthSession, setAuthFlowEnabled } from './session'\n+import { fetchNoticeUnreadCounts } from '../notices'\n+import { httpClient } from '../http'\n+\n+describe('#213 notice auth header follows the session, not the current route', () => {\n+  beforeEach(() => {\n+    clearAuthSession()\n+    setAuthFlowEnabled(false)\n+    probes.requests.length = 0\n+    probes.issuedToken = 'access-A'\n+    probes.unread401Once = false\n+    probes.getLoginToken.mockReset().mockResolvedValue({ token: 'native-token', salt: 'native-salt' })\n+  })\n+\n+  it('attaches the stored Bearer token to the real unread-count service even on a deferred route', async () => {\n+    await authenticate()\n+    setAuthFlowEnabled(false) // MobileLayout does this for /mall and other deferred routes.\n+\n+    await expect(fetchNoticeUnreadCounts()).resolves.toMatchObject({ total: 2 })\n+    expect(probes.requests.filter(request => request.url === '/api/notices/unread-count'))\n+      .toEqual([{ url: '/api/notices/unread-count', authorization: 'Bearer access-A' }])\n+  })\n+\n+  it('sends no invented credentials when there is no session', async () => {\n+    await expect(fetchNoticeUnreadCounts()).resolves.toMatchObject({ total: 2 })\n+    expect(probes.requests[0]).toEqual({\n+      url: '/api/notices/unread-count',\n+      authorization: undefined,\n+    })\n+  })\n+\n+  it('excludes Bearer from OAuth skipAuth, explicit skipAuth and cross-origin requests', async () => {\n+    await authenticate()\n+    setAuthFlowEnabled(true)\n+    await httpClient.request({ method: 'GET', url: '/api/notices/unread-count', skipAuth: true })\n+    await httpClient.request({ method: 'GET', url: 'https://other.example.test/resource' })\n+\n+    expect(probes.requests).toEqual([\n+      { url: '/api/oauth/login', authorization: undefined },\n+      { url: '/api/notices/unread-count', authorization: undefined },\n+      { url: 'https://other.example.test/resource', authorization: undefined },\n+    ])\n+  })\n+\n+  it('preserves a single 401 reauthentication and retries with the renewed token', async () => {\n+    await authenticate()\n+    setAuthFlowEnabled(true)\n+    probes.issuedToken = 'access-B'\n+    probes.unread401Once = true\n+\n+    await expect(fetchNoticeUnreadCounts()).resolves.toMatchObject({ total: 2 })\n+    expect(probes.requests.filter(request => request.url === '/api/notices/unread-count'))\n+      .toEqual([\n+        { url: '/api/notices/unread-count', authorization: 'Bearer access-A' },\n+        { url: '/api/notices/unread-count', authorization: 'Bearer access-B' },\n+      ])\n+    expect(probes.requests.filter(request => request.url === '/api/oauth/login')).toHaveLength(2)\n+  })\n+})\ndiff --git a/src/services/auth/session.ts b/src/services/auth/session.ts\nindex db73016..260115d 100644\n--- a/src/services/auth/session.ts\n+++ b/src/services/auth/session.ts\n@@ -180,7 +180,8 @@ export async function refreshChatAuthAfterUnauthorized(): Promise<void> {\n }\n \n setHttpAuthHeadersProvider(() => {\n-  if (!authFlowEnabled) return undefined\n+  // Route-level authFlowEnabled controls reauthentication/expiry UI, not credential attachment.\n+  // Any same-origin API request with an existing session must carry its Bearer token.\n   const session = readSession()\n   return session ? { Authorization: `Bearer ${session.accessToken}` } : undefined\n })",
      "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/be8ba2423bdb8db651b7bfc02515d96260779e24",
            "commitHash": "be8ba2423bdb8db651b7bfc02515d96260779e24",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/215",
            "prTitle": "release(test): 晋级 #213 HTTP 认证凭证注入修复",
            "prBaseHash": "82661bb129ce89966563e4d224d71ee8c58df36e",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/38354507765"
          },
          "gitCommit": {
            "shortHash": "be8ba24",
            "hash": "be8ba2423bdb8db651b7bfc02515d96260779e24",
            "subject": "Merge 245ffe221e20cc4c3bc2f64b4b31c385f18f364d into 82661bb129ce89966563e4d224d71ee8c58df36e",
            "body": "Merge 245ffe221e20cc4c3bc2f64b4b31c385f18f364d into 82661bb129ce89966563e4d224d71ee8c58df36e\n",
            "author": {
              "name": "Tomz Dang",
              "email": "dangjingtao@163.com",
              "time": 1791871328000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1791871328000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/src/services/auth/session.notice-auth.test.ts b/src/services/auth/session.notice-auth.test.ts\nnew file mode 100644\nindex 0000000..75243a0\n--- /dev/null\n+++ b/src/services/auth/session.notice-auth.test.ts\n@@ -0,0 +1,107 @@\n+import { beforeEach, describe, expect, it, vi } from 'vitest'\n+import type { AxiosAdapter } from 'axios'\n+\n+const probes = vi.hoisted(() => ({\n+  getLoginToken: vi.fn(),\n+  requests: [] as Array<{ url: string; authorization: string | undefined }>,\n+  issuedToken: 'access-A',\n+  unread401Once: false,\n+}))\n+\n+vi.mock('../../app/config/runtime', () => ({\n+  runtimePolicy: { apiBaseUrl: 'https://api.example.test', dataMode: 'api' },\n+}))\n+\n+vi.mock('../nativeBridge', () => ({\n+  getLoginToken: probes.getLoginToken,\n+}))\n+\n+// Keep the real Axios interceptors and notice service. Substitute only the transport.\n+vi.mock('../http/httpClient', async importOriginal => {\n+  const actual = await importOriginal<typeof import('../http/httpClient')>()\n+  const adapter: AxiosAdapter = async (config) => {\n+    const url = config.url ?? ''\n+    probes.requests.push({\n+      url,\n+      authorization: config.headers.get('Authorization')?.toString(),\n+    })\n+    let data: unknown = { ok: true }\n+    if (url === '/api/oauth/login') {\n+      data = { code: 0, data: { userInfo: { id: 9 }, accessToken: probes.issuedToken } }\n+    } else if (url === '/api/notices/unread-count') {\n+      if (probes.unread401Once) {\n+        probes.unread401Once = false\n+        data = { code: 401, message: '请先登录' }\n+      } else {\n+        data = { code: 0, data: { total: 2, items: [] } }\n+      }\n+    }\n+    return { data, status: 200, statusText: 'OK', headers: {}, config }\n+  }\n+  return {\n+    ...actual,\n+    createHttpClient: (options: Parameters<typeof actual.createHttpClient>[0]) =>\n+      actual.createHttpClient({ ...options, adapter }),\n+    httpClient: actual.createHttpClient({ baseURL: 'https://api.example.test', adapter }),\n+  }\n+})\n+\n+import { authenticate, clearAuthSession, setAuthFlowEnabled } from './session'\n+import { fetchNoticeUnreadCounts } from '../notices'\n+import { httpClient } from '../http'\n+\n+describe('#213 notice auth header follows the session, not the current route', () => {\n+  beforeEach(() => {\n+    clearAuthSession()\n+    setAuthFlowEnabled(false)\n+    probes.requests.length = 0\n+    probes.issuedToken = 'access-A'\n+    probes.unread401Once = false\n+    probes.getLoginToken.mockReset().mockResolvedValue({ token: 'native-token', salt: 'native-salt' })\n+  })\n+\n+  it('attaches the stored Bearer token to the real unread-count service even on a deferred route', async () => {\n+    await authenticate()\n+    setAuthFlowEnabled(false) // MobileLayout does this for /mall and other deferred routes.\n+\n+    await expect(fetchNoticeUnreadCounts()).resolves.toMatchObject({ total: 2 })\n+    expect(probes.requests.filter(request => request.url === '/api/notices/unread-count'))\n+      .toEqual([{ url: '/api/notices/unread-count', authorization: 'Bearer access-A' }])\n+  })\n+\n+  it('sends no invented credentials when there is no session', async () => {\n+    await expect(fetchNoticeUnreadCounts()).resolves.toMatchObject({ total: 2 })\n+    expect(probes.requests[0]).toEqual({\n+      url: '/api/notices/unread-count',\n+      authorization: undefined,\n+    })\n+  })\n+\n+  it('excludes Bearer from OAuth skipAuth, explicit skipAuth and cross-origin requests', async () => {\n+    await authenticate()\n+    setAuthFlowEnabled(true)\n+    await httpClient.request({ method: 'GET', url: '/api/notices/unread-count', skipAuth: true })\n+    await httpClient.request({ method: 'GET', url: 'https://other.example.test/resource' })\n+\n+    expect(probes.requests).toEqual([\n+      { url: '/api/oauth/login', authorization: undefined },\n+      { url: '/api/notices/unread-count', authorization: undefined },\n+      { url: 'https://other.example.test/resource', authorization: undefined },\n+    ])\n+  })\n+\n+  it('preserves a single 401 reauthentication and retries with the renewed token', async () => {\n+    await authenticate()\n+    setAuthFlowEnabled(true)\n+    probes.issuedToken = 'access-B'\n+    probes.unread401Once = true\n+\n+    await expect(fetchNoticeUnreadCounts()).resolves.toMatchObject({ total: 2 })\n+    expect(probes.requests.filter(request => request.url === '/api/notices/unread-count'))\n+      .toEqual([\n+        { url: '/api/notices/unread-count', authorization: 'Bearer access-A' },\n+        { url: '/api/notices/unread-count', authorization: 'Bearer access-B' },\n+      ])\n+    expect(probes.requests.filter(request => request.url === '/api/oauth/login')).toHaveLength(2)\n+  })\n+})\ndiff --git a/src/services/auth/session.ts b/src/services/auth/session.ts\nindex db73016..260115d 100644\n--- a/src/services/auth/session.ts\n+++ b/src/services/auth/session.ts\n@@ -180,7 +180,8 @@ export async function refreshChatAuthAfterUnauthorized(): Promise<void> {\n }\n \n setHttpAuthHeadersProvider(() => {\n-  if (!authFlowEnabled) return undefined\n+  // Route-level authFlowEnabled controls reauthentication/expiry UI, not credential attachment.\n+  // Any same-origin API request with an existing session must carry its Bearer token.\n   const session = readSession()\n   return session ? { Authorization: `Bearer ${session.accessToken}` } : undefined\n })",
          "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": 62,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T06:03:14.730Z",
                  "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": 1023,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T06:03:14.737Z",
                  "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": 1038,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T06:03:14.863Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-b030d57053896c93a550",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /profile shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 909,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T06:03:16.072Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8605535a2807e17fcccc",
          "file": "test-gate.spec.ts",
          "line": 51,
          "column": 3
        },
        {
          "title": "@test-gate /settings shows the unsupported-host notice in a browser",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 1,
                  "parallelIndex": 1,
                  "status": "passed",
                  "duration": 874,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T06:03:16.086Z",
                  "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": 878,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T06:03:16.972Z",
                  "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": 941,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-10-13T06:03:17.000Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-10-13T06:03:13.936Z",
    "duration": 4055.492,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}