{
  "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/b8654cabde48a2108e9881fce70a9a7c40364d43",
        "commitHash": "b8654cabde48a2108e9881fce70a9a7c40364d43",
        "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/302",
        "prTitle": "release(test): promote QR poster branding and OSS production fix",
        "prBaseHash": "8cb08ff87a78d2d15382ec61b86fa055aa44158e",
        "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/42735353685"
      },
      "gitCommit": {
        "shortHash": "b8654ca",
        "hash": "b8654cabde48a2108e9881fce70a9a7c40364d43",
        "subject": "Merge b6331355e3b019792fd95028bcab3176c88408ae into 8cb08ff87a78d2d15382ec61b86fa055aa44158e",
        "body": "Merge b6331355e3b019792fd95028bcab3176c88408ae into 8cb08ff87a78d2d15382ec61b86fa055aa44158e\n",
        "author": {
          "name": "SINGING-AKIBA",
          "email": "980325684@qq.com",
          "time": 1794273757000
        },
        "committer": {
          "name": "GitHub",
          "email": "noreply@github.com",
          "time": 1794273757000
        },
        "branch": "HEAD"
      },
      "gitDiff": "diff --git a/scripts/oss-prod-release.test.mjs b/scripts/oss-prod-release.test.mjs\nindex a40d849..410448a 100644\n--- a/scripts/oss-prod-release.test.mjs\n+++ b/scripts/oss-prod-release.test.mjs\n@@ -4,7 +4,7 @@ import {\n   LAST, HISTORY, CATALOG, MANIFEST, KEEP, makeCandidate,\n   publish, rollback, pruneHistory, retentionPlan, safeFile, safeId,\n } from './oss-prod-release-core.mjs'\n-import { OssProdStore } from './oss-prod-store.mjs'\n+import { OssProdStore, putArgsForKey } from './oss-prod-store.mjs'\n \n class MemoryStore {\n   constructor() {\n@@ -267,3 +267,17 @@ test('OSS production driver defaults to read only and rejects all wrong deletion\n   const noRemove = new OssProdStore({ allowDeletes: true })\n   await assert.rejects(noRemove.remove(CATALOG), /Deletion forbidden/)\n })\n+\n+test('OSS mutable production pointers use explicit force overwrite without changing immutable copies', () => {\n+  for (const key of [LAST + 'index.html', LAST + 'build-meta.json',\n+    LAST + MANIFEST, CATALOG]) {\n+    assert.deepEqual(putArgsForKey(key, '/tmp/object', 'no-cache'), [\n+      'cp', '/tmp/object', 'oss://kbs-sdl/' + key, '-f', '--cache-control', 'no-cache',\n+    ])\n+  }\n+  for (const key of [LAST + 'assets/index-aBcD1234.js',\n+    HISTORY + 'r101-a1/index.html', HISTORY + 'r101-a1/build-meta.json']) {\n+    const args = putArgsForKey(key, '/tmp/object', 'no-cache')\n+    assert.equal(args.includes('-f'), false, key + ' must not be force-written')\n+  }\n+})\ndiff --git a/scripts/oss-prod-store.mjs b/scripts/oss-prod-store.mjs\nindex 659654d..635b347 100644\n--- a/scripts/oss-prod-store.mjs\n+++ b/scripts/oss-prod-store.mjs\n@@ -32,6 +32,22 @@ function localTemp(fn) {\n   try { return fn(join(dir, 'object')) }\n   finally { rmSync(dir, { recursive: true, force: true }) }\n }\n+\n+/**\n+ * Mutable entry/manifest/catalog keys require -f to replace an existing OSS\n+ * object. The nonproduction uploader already uses this flag; without it,\n+ * ossutil cp may report success while build-meta.json remains stale.\n+ * History snapshots and fingerprinted assets are never forcibly overwritten.\n+ */\n+export function putArgsForKey(key, path, cache) {\n+  const replaceable = key === CATALOG ||\n+    key === LAST + 'index.html' ||\n+    key === LAST + 'build-meta.json' ||\n+    key === LAST + 'release-manifest.json'\n+  return ['cp', path, BUCKET + key,\n+    ...(replaceable ? ['-f'] : []), '--cache-control', cache]\n+}\n+\n export class OssProdStore {\n   constructor({ allowWrites = false, allowSwitch = false, allowDeletes = false } = {}) {\n     this.allowWrites = allowWrites\n@@ -69,7 +85,7 @@ export class OssProdStore {\n           : hashedAsset\n             ? 'public, max-age=31536000, immutable'\n             : 'no-cache'\n-      execOss('cp', path, BUCKET + key, '--cache-control', cache)\n+      execOss(...putArgsForKey(key, path, cache))\n \n     })\n   }\ndiff --git a/src/lib/buddyQrPoster.test.ts b/src/lib/buddyQrPoster.test.ts\nindex a540580..4a0f441 100644\n--- a/src/lib/buddyQrPoster.test.ts\n+++ b/src/lib/buddyQrPoster.test.ts\n@@ -53,6 +53,9 @@ describe('buddy QR image and poster encoding', () => {\n     const poster = await createBuddyPoster(png, { demo: true })\n     expect(requestedSrc).toBe(png)\n     expect(context.drawImage).toHaveBeenCalledWith(expect.any(MockImage), 160, 335, 640, 640)\n+    expect(context.fillText).toHaveBeenCalledWith('极地种子 · 洗头搭子', 480, 175, 870)\n+    expect(context.fillText).toHaveBeenCalledWith('请在卡博士 App 内使用「极地种子扫一扫」', 480, 1045, 870)\n+    expect(context.fillText.mock.calls.some(([text]) => String(text).includes('诗得丽'))).toBe(false)\n     expect(context.fillText).toHaveBeenCalledWith(expect.stringContaining('演示二维码'), 480, 1166, 870)\n     expect(poster).toEqual({\n       imageType: 'base64',\ndiff --git a/src/lib/buddyQrPoster.ts b/src/lib/buddyQrPoster.ts\nindex b75304a..171be31 100644\n--- a/src/lib/buddyQrPoster.ts\n+++ b/src/lib/buddyQrPoster.ts\n@@ -61,13 +61,13 @@ export async function createBuddyPoster(\n   ctx.fillStyle = '#ffffff'\n   ctx.fillRect(60, 60, 840, 1160)\n \n-  drawCentered(ctx, '诗得丽 · 洗头搭子', 175, 'bold 46px sans-serif', '#174749')\n+  drawCentered(ctx, '极地种子 · 洗头搭子', 175, 'bold 46px sans-serif', '#174749')\n   drawCentered(ctx, '快来成为我的洗头搭子吧～', 253, '34px sans-serif', '#345d5c')\n \n   // 白底 + 静区全部在同一张 QR 原图内；勿在二维码上添加水印。\n   ctx.drawImage(qr, 160, 335, 640, 640)\n \n-  drawCentered(ctx, '请在卡博士 App 内使用「诗得丽扫一扫」', 1045, '29px sans-serif', '#345d5c')\n+  drawCentered(ctx, '请在卡博士 App 内使用「极地种子扫一扫」', 1045, '29px sans-serif', '#345d5c')\n   drawCentered(ctx, '扫码后确认，才能成为洗头搭子', 1100, '24px sans-serif', '#758a86')\n   if (options.demo) {\n     drawCentered(ctx, '演示二维码 · 无法建立真实关系', 1166, 'bold 26px sans-serif', '#ba5d35')",
      "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/b8654cabde48a2108e9881fce70a9a7c40364d43",
            "commitHash": "b8654cabde48a2108e9881fce70a9a7c40364d43",
            "prHref": "https://github.com/sanchuang-dev/dr-card-web/pull/302",
            "prTitle": "release(test): promote QR poster branding and OSS production fix",
            "prBaseHash": "8cb08ff87a78d2d15382ec61b86fa055aa44158e",
            "buildHref": "https://github.com/sanchuang-dev/dr-card-web/actions/runs/42735353685"
          },
          "gitCommit": {
            "shortHash": "b8654ca",
            "hash": "b8654cabde48a2108e9881fce70a9a7c40364d43",
            "subject": "Merge b6331355e3b019792fd95028bcab3176c88408ae into 8cb08ff87a78d2d15382ec61b86fa055aa44158e",
            "body": "Merge b6331355e3b019792fd95028bcab3176c88408ae into 8cb08ff87a78d2d15382ec61b86fa055aa44158e\n",
            "author": {
              "name": "SINGING-AKIBA",
              "email": "980325684@qq.com",
              "time": 1794273757000
            },
            "committer": {
              "name": "GitHub",
              "email": "noreply@github.com",
              "time": 1794273757000
            },
            "branch": "HEAD"
          },
          "gitDiff": "diff --git a/scripts/oss-prod-release.test.mjs b/scripts/oss-prod-release.test.mjs\nindex a40d849..410448a 100644\n--- a/scripts/oss-prod-release.test.mjs\n+++ b/scripts/oss-prod-release.test.mjs\n@@ -4,7 +4,7 @@ import {\n   LAST, HISTORY, CATALOG, MANIFEST, KEEP, makeCandidate,\n   publish, rollback, pruneHistory, retentionPlan, safeFile, safeId,\n } from './oss-prod-release-core.mjs'\n-import { OssProdStore } from './oss-prod-store.mjs'\n+import { OssProdStore, putArgsForKey } from './oss-prod-store.mjs'\n \n class MemoryStore {\n   constructor() {\n@@ -267,3 +267,17 @@ test('OSS production driver defaults to read only and rejects all wrong deletion\n   const noRemove = new OssProdStore({ allowDeletes: true })\n   await assert.rejects(noRemove.remove(CATALOG), /Deletion forbidden/)\n })\n+\n+test('OSS mutable production pointers use explicit force overwrite without changing immutable copies', () => {\n+  for (const key of [LAST + 'index.html', LAST + 'build-meta.json',\n+    LAST + MANIFEST, CATALOG]) {\n+    assert.deepEqual(putArgsForKey(key, '/tmp/object', 'no-cache'), [\n+      'cp', '/tmp/object', 'oss://kbs-sdl/' + key, '-f', '--cache-control', 'no-cache',\n+    ])\n+  }\n+  for (const key of [LAST + 'assets/index-aBcD1234.js',\n+    HISTORY + 'r101-a1/index.html', HISTORY + 'r101-a1/build-meta.json']) {\n+    const args = putArgsForKey(key, '/tmp/object', 'no-cache')\n+    assert.equal(args.includes('-f'), false, key + ' must not be force-written')\n+  }\n+})\ndiff --git a/scripts/oss-prod-store.mjs b/scripts/oss-prod-store.mjs\nindex 659654d..635b347 100644\n--- a/scripts/oss-prod-store.mjs\n+++ b/scripts/oss-prod-store.mjs\n@@ -32,6 +32,22 @@ function localTemp(fn) {\n   try { return fn(join(dir, 'object')) }\n   finally { rmSync(dir, { recursive: true, force: true }) }\n }\n+\n+/**\n+ * Mutable entry/manifest/catalog keys require -f to replace an existing OSS\n+ * object. The nonproduction uploader already uses this flag; without it,\n+ * ossutil cp may report success while build-meta.json remains stale.\n+ * History snapshots and fingerprinted assets are never forcibly overwritten.\n+ */\n+export function putArgsForKey(key, path, cache) {\n+  const replaceable = key === CATALOG ||\n+    key === LAST + 'index.html' ||\n+    key === LAST + 'build-meta.json' ||\n+    key === LAST + 'release-manifest.json'\n+  return ['cp', path, BUCKET + key,\n+    ...(replaceable ? ['-f'] : []), '--cache-control', cache]\n+}\n+\n export class OssProdStore {\n   constructor({ allowWrites = false, allowSwitch = false, allowDeletes = false } = {}) {\n     this.allowWrites = allowWrites\n@@ -69,7 +85,7 @@ export class OssProdStore {\n           : hashedAsset\n             ? 'public, max-age=31536000, immutable'\n             : 'no-cache'\n-      execOss('cp', path, BUCKET + key, '--cache-control', cache)\n+      execOss(...putArgsForKey(key, path, cache))\n \n     })\n   }\ndiff --git a/src/lib/buddyQrPoster.test.ts b/src/lib/buddyQrPoster.test.ts\nindex a540580..4a0f441 100644\n--- a/src/lib/buddyQrPoster.test.ts\n+++ b/src/lib/buddyQrPoster.test.ts\n@@ -53,6 +53,9 @@ describe('buddy QR image and poster encoding', () => {\n     const poster = await createBuddyPoster(png, { demo: true })\n     expect(requestedSrc).toBe(png)\n     expect(context.drawImage).toHaveBeenCalledWith(expect.any(MockImage), 160, 335, 640, 640)\n+    expect(context.fillText).toHaveBeenCalledWith('极地种子 · 洗头搭子', 480, 175, 870)\n+    expect(context.fillText).toHaveBeenCalledWith('请在卡博士 App 内使用「极地种子扫一扫」', 480, 1045, 870)\n+    expect(context.fillText.mock.calls.some(([text]) => String(text).includes('诗得丽'))).toBe(false)\n     expect(context.fillText).toHaveBeenCalledWith(expect.stringContaining('演示二维码'), 480, 1166, 870)\n     expect(poster).toEqual({\n       imageType: 'base64',\ndiff --git a/src/lib/buddyQrPoster.ts b/src/lib/buddyQrPoster.ts\nindex b75304a..171be31 100644\n--- a/src/lib/buddyQrPoster.ts\n+++ b/src/lib/buddyQrPoster.ts\n@@ -61,13 +61,13 @@ export async function createBuddyPoster(\n   ctx.fillStyle = '#ffffff'\n   ctx.fillRect(60, 60, 840, 1160)\n \n-  drawCentered(ctx, '诗得丽 · 洗头搭子', 175, 'bold 46px sans-serif', '#174749')\n+  drawCentered(ctx, '极地种子 · 洗头搭子', 175, 'bold 46px sans-serif', '#174749')\n   drawCentered(ctx, '快来成为我的洗头搭子吧～', 253, '34px sans-serif', '#345d5c')\n \n   // 白底 + 静区全部在同一张 QR 原图内；勿在二维码上添加水印。\n   ctx.drawImage(qr, 160, 335, 640, 640)\n \n-  drawCentered(ctx, '请在卡博士 App 内使用「诗得丽扫一扫」', 1045, '29px sans-serif', '#345d5c')\n+  drawCentered(ctx, '请在卡博士 App 内使用「极地种子扫一扫」', 1045, '29px sans-serif', '#345d5c')\n   drawCentered(ctx, '扫码后确认，才能成为洗头搭子', 1100, '24px sans-serif', '#758a86')\n   if (options.demo) {\n     drawCentered(ctx, '演示二维码 · 无法建立真实关系', 1166, 'bold 26px sans-serif', '#ba5d35')",
          "actualWorkers": 2
        },
        "id": "chromium-mobile",
        "name": "chromium-mobile",
        "testDir": "/home/runner/work/dr-card-web/dr-card-web/tests/e2e",
        "testIgnore": [],
        "testMatch": [
          "**/*.@(spec|test).?(c|m)[jt]s?(x)"
        ],
        "timeout": 30000
      }
    ],
    "quiet": false,
    "reporter": [
      [
        "line",
        null
      ],
      [
        "html",
        {
          "outputFolder": "playwright-report",
          "open": "never"
        }
      ],
      [
        "json",
        {
          "outputFile": "test-results/playwright-results.json"
        }
      ]
    ],
    "reportSlowTests": {
      "max": 5,
      "threshold": 300000
    },
    "shard": null,
    "tags": [],
    "updateSnapshots": "missing",
    "updateSourceMethod": "patch",
    "version": "1.63.0",
    "workers": 2,
    "webServer": null
  },
  "suites": [
    {
      "title": "test-gate.spec.ts",
      "file": "test-gate.spec.ts",
      "column": 0,
      "line": 0,
      "specs": [
        {
          "title": "@test-gate build identity matches the production-like test policy",
          "ok": true,
          "tags": [
            "test-gate"
          ],
          "tests": [
            {
              "timeout": 30000,
              "annotations": [],
              "expectedStatus": "passed",
              "projectId": "chromium-mobile",
              "projectName": "chromium-mobile",
              "results": [
                {
                  "workerIndex": 0,
                  "parallelIndex": 0,
                  "status": "passed",
                  "duration": 53,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T01:23:46.641Z",
                  "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": 1120,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T01:23:46.651Z",
                  "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": 1129,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T01:23:46.770Z",
                  "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": 1155,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T01:23:49.449Z",
                  "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": 1237,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T01:23:49.462Z",
                  "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": 961,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T01:23:50.621Z",
                  "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": 971,
                  "errors": [],
                  "stdout": [],
                  "stderr": [],
                  "retry": 0,
                  "startTime": "2026-11-10T01:23:50.719Z",
                  "annotations": [],
                  "attachments": []
                }
              ],
              "status": "expected"
            }
          ],
          "id": "37c772ca39f4c8e5dd7a-8caf237c4d42513fff2f",
          "file": "test-gate.spec.ts",
          "line": 80,
          "column": 1
        }
      ]
    }
  ],
  "errors": [],
  "stats": {
    "startTime": "2026-11-10T01:23:45.755Z",
    "duration": 5998.088,
    "expected": 7,
    "skipped": 0,
    "unexpected": 0,
    "flaky": 0
  }
}