From 6eb201b05c29c37d9f50155e06f6285442ff1d7a Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:43:23 +0700 Subject: [PATCH 1/3] test: verify merged upstream with isolated CLI process smoke --- UPSTREAM_SYNC.md | 16 + .../2026-10-22/host-origin-probe.json | 34 + .../validation/2026-10-22/report-browser.json | 22 + .../validation/2026-10-22/upstream-smoke.html | 286 ++++++++ .../validation/2026-10-22/upstream-smoke.json | 664 ++++++++++++++++++ tools/README.md | 34 + tools/smoke-cli.mjs | 138 ++++ tools/smoke-report.mjs | 15 + 8 files changed, 1209 insertions(+) create mode 100644 docs/validation/2026-10-22/host-origin-probe.json create mode 100644 docs/validation/2026-10-22/report-browser.json create mode 100644 docs/validation/2026-10-22/upstream-smoke.html create mode 100644 docs/validation/2026-10-22/upstream-smoke.json create mode 100644 tools/README.md create mode 100644 tools/smoke-cli.mjs create mode 100644 tools/smoke-report.mjs diff --git a/UPSTREAM_SYNC.md b/UPSTREAM_SYNC.md index 94d4e56..aacc13e 100644 --- a/UPSTREAM_SYNC.md +++ b/UPSTREAM_SYNC.md @@ -30,6 +30,22 @@ These upstream areas are excluded (TS/npm-specific): ## Sync Log +### 2026-10-22 - Post-merge runtime acceptance (in progress) + +- Owner: Codex, branch `codex/smoke-upstream-20261022`. +- Revision under test: merged main `422774fe3ba3d94c3e9be2e7082945ed54222770`. +- Outcome: verify the actual standalone CLI process over HTTP and a browser. +- Scope: DOSRouter test harness and evidence; isolated localhost gateway and + synthetic state only. No provider charge, credentials or remote rollout. +- Acceptance: binary revision/hash, startup catalog refresh, explicit model + and free aliases, fallback/outcomes, streaming, spend admission, local trust, + clean shutdown and browser readback with machine-readable evidence. +- Environment discovery: GitHub environments list is empty; the current + process has no DOSROUTER_UPSTREAM, DOSROUTER_PORT or DOSROUTER_API_KEY. + Historical npm/Docker plans do not describe this standalone Go runtime. +- Next: build the clean merged revision, run process-level smoke scenarios, + preserve results and close any reproduced acceptance failure within scope. + ### 2026-10-22 - Sync through 0.12.297 source Owner: Codex, branch `codex/sync-clawrouter-20261022`. Compared the previous diff --git a/docs/validation/2026-10-22/host-origin-probe.json b/docs/validation/2026-10-22/host-origin-probe.json new file mode 100644 index 0000000..17ef5d0 --- /dev/null +++ b/docs/validation/2026-10-22/host-origin-probe.json @@ -0,0 +1,34 @@ +{ + "checkedAt": "2026-10-22T14:32:04.813Z", + "revision": "422774fe3ba3d94c3e9be2e7082945ed54222770", + "binarySha256": "5bd95979b1176002133def84734e3b98cd6d70cb107d58627ffb00cede9f7a27", + "transport": "node:http sends the literal Host header", + "originalDriverIssue": "Node fetch ignored the overridden Host header and sent the loopback host; its 429 was a driver error, not a guard failure.", + "results": [ + { + "headers": { + "Origin": "https://attacker.invalid" + }, + "status": 403 + }, + { + "headers": { + "Origin": "null" + }, + "status": 403 + }, + { + "headers": { + "Host": "attacker.invalid" + }, + "status": 403 + }, + { + "headers": { + "Sec-Fetch-Site": "cross-site" + }, + "status": 403 + } + ], + "upstreamSends": 0 +} diff --git a/docs/validation/2026-10-22/report-browser.json b/docs/validation/2026-10-22/report-browser.json new file mode 100644 index 0000000..779c9b0 --- /dev/null +++ b/docs/validation/2026-10-22/report-browser.json @@ -0,0 +1,22 @@ +{ + "driver": "MCP Playwright", + "checkedAt": "2026-10-22T14:41:27Z", + "desktop": { + "language": "vi", + "passCount": "18/18", + "rows": 18, + "failures": 0, + "background": "rgb(16, 16, 20)", + "red": "rgb(255, 76, 87)", + "jsonStatus": 200, + "jsonChecks": 18, + "jsonAllPass": true, + "overflow": false + }, + "mobile": { + "width": 390, + "scrollWidth": 375, + "overflow": false + }, + "note": "Report and linked JSON inspected through temporary localhost HTTP. Link checked using browser fetch after native click timed out." +} diff --git a/docs/validation/2026-10-22/upstream-smoke.html b/docs/validation/2026-10-22/upstream-smoke.html new file mode 100644 index 0000000..8939b36 --- /dev/null +++ b/docs/validation/2026-10-22/upstream-smoke.html @@ -0,0 +1,286 @@ +DOSRouter - Xác minh runtime +
DOS / ROUTER

Xác minh bản chạy sau đồng bộ upstream

Binary CLI thật, giao tiếp HTTP với gateway mô phỏng. Đây là kết quả cục bộ, chưa xác nhận provider live hoặc triển khai từ xa.

+
18/18Kiểm tra đạt
ĐạtPlaywright
Đã dừngTiến trình thử nghiệm
+

Bản được xác minh

Commit: 422774fe3ba3d94c3e9be2e7082945ed54222770

SHA-256 binary: 5bd95979b1176002133def84734e3b98cd6d70cb107d58627ffb00cede9f7a27

VCS revision khớp commit và vcs.modified=false. Mỗi tiến trình dùng thư mục người dùng riêng, không dùng API key thật.

+

Kết quả kiểm tra

Kịch bảnKết quảBằng chứng
Health và revision của tiến trình CLIĐạt
Chi tiết
{
+  "gateway": "http://127.0.0.1:57085",
+  "journalEntries": 0,
+  "journalSessions": 0,
+  "sessions": 0,
+  "spendControl": {
+    "perRequest": {
+      "limit": 0.05,
+      "spent": 0,
+      "remaining": 0.05
+    },
+    "session": {
+      "limit": 10,
+      "spent": 0,
+      "remaining": 10
+    }
+  },
+  "status": "ok",
+  "version": "422774f"
+}
Catalog mới tự nạp khi khởi độngĐạt
Chi tiết
{
+  "reads": 1,
+  "modelVisible": true
+}
Catalog lỗi giữ bản đóng góiĐạt
Chi tiết
{
+  "bundledFallback": true
+}
Model reasoning mới chuyển tiếp đúng metadataĐạt
Chi tiết
{
+  "status": 200,
+  "trace": [
+    {
+      "gateway": "stable",
+      "tag": "reasoning",
+      "model": "fixture/reasoner",
+      "sequence": 1,
+      "status": 200,
+      "reasoningNormalized": true
+    }
+  ]
+}
Alias miễn phí không thành model trả phíĐạt
Chi tiết
[
+  {
+    "gateway": "stable",
+    "tag": "free-alias",
+    "model": "free/nemotron-3.5-lightning",
+    "sequence": 1,
+    "status": 200,
+    "reasoningNormalized": true
+  }
+]
Model lỗi lặp lại được chuyển xuống cuốiĐạt
Chi tiết
[
+  {
+    "gateway": "stable",
+    "tag": "repeat-failure-0",
+    "model": "google/gemini-2.5-flash",
+    "sequence": 1,
+    "status": 502,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "repeat-failure-0",
+    "model": "google/gemini-3-flash-preview",
+    "sequence": 2,
+    "status": 200,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "repeat-failure-1",
+    "model": "google/gemini-2.5-flash",
+    "sequence": 1,
+    "status": 502,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "repeat-failure-1",
+    "model": "google/gemini-3-flash-preview",
+    "sequence": 2,
+    "status": 200,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "repeat-failure-2",
+    "model": "google/gemini-2.5-flash",
+    "sequence": 1,
+    "status": 502,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "repeat-failure-2",
+    "model": "google/gemini-3-flash-preview",
+    "sequence": 2,
+    "status": 200,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "repeat-failure-3",
+    "model": "google/gemini-3-flash-preview",
+    "sequence": 1,
+    "status": 200,
+    "reasoningNormalized": true
+  }
+]
Phản hồi rỗng tiếp tục tới model có kết quảĐạt
Chi tiết
[
+  {
+    "gateway": "stable",
+    "tag": "empty-chain",
+    "model": "google/gemini-3.5-flash",
+    "sequence": 1,
+    "status": 200,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "empty-chain",
+    "model": "google/gemini-3.6-flash",
+    "sequence": 2,
+    "status": 200,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "empty-chain",
+    "model": "anthropic/claude-haiku-4.5",
+    "sequence": 3,
+    "status": 200,
+    "reasoningNormalized": true
+  }
+]
Chuỗi lỗi dừng sau tối đa 5 modelĐạt
Chi tiết
[
+  {
+    "gateway": "stable",
+    "tag": "all-fail",
+    "model": "google/gemini-3.5-flash",
+    "sequence": 1,
+    "status": 502,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "all-fail",
+    "model": "google/gemini-3.6-flash",
+    "sequence": 2,
+    "status": 502,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "all-fail",
+    "model": "anthropic/claude-haiku-4.5",
+    "sequence": 3,
+    "status": 502,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "all-fail",
+    "model": "zai/glm-5.3",
+    "sequence": 4,
+    "status": 502,
+    "reasoningNormalized": true
+  },
+  {
+    "gateway": "stable",
+    "tag": "all-fail",
+    "model": "google/gemini-3.5-flash-lite",
+    "sequence": 5,
+    "status": 502,
+    "reasoningNormalized": true
+  }
+]
Model chỉ định không bị thay bằng fallbackĐạt
Chi tiết
[
+  {
+    "gateway": "stable",
+    "tag": "explicit-pin-fails",
+    "model": "openai/gpt-4o-mini",
+    "sequence": 1,
+    "status": 502,
+    "reasoningNormalized": true
+  }
+]
Mất phản hồi không làm gửi lại requestĐạt
Chi tiết
[
+  {
+    "gateway": "stable",
+    "tag": "lost-response",
+    "model": "google/gemini-3-flash-preview",
+    "sequence": 1,
+    "status": "socket-closed",
+    "reasoningNormalized": true
+  }
+]
SSE giữ nội dung và dấu kết thúcĐạt
Chi tiết
{
+  "status": 200,
+  "done": true
+}
Giá cao bị chặn trước khi gửiĐạt
Chi tiết
{
+  "status": 429,
+  "upstreamSends": 0
+}
Giá cố định bằng 0 không vượt spend limitĐạt
Chi tiết
{
+  "status": 429,
+  "upstreamSends": 0
+}
Host và Origin không tin cậy bị từ chốiĐạt
Chi tiết
{
+  "initialDriverFailure": {
+    "error": "Expected values to be strictly equal:\n\n429 !== 403\n"
+  },
+  "recheck": {
+    "checkedAt": "2026-10-22T14:32:04.813Z",
+    "revision": "422774fe3ba3d94c3e9be2e7082945ed54222770",
+    "binarySha256": "5bd95979b1176002133def84734e3b98cd6d70cb107d58627ffb00cede9f7a27",
+    "transport": "node:http sends the literal Host header",
+    "originalDriverIssue": "Node fetch ignored the overridden Host header and sent the loopback host; its 429 was a driver error, not a guard failure.",
+    "results": [
+      {
+        "headers": {
+          "Origin": "https://attacker.invalid"
+        },
+        "status": 403
+      },
+      {
+        "headers": {
+          "Origin": "null"
+        },
+        "status": 403
+      },
+      {
+        "headers": {
+          "Host": "attacker.invalid"
+        },
+        "status": 403
+      },
+      {
+        "headers": {
+          "Sec-Fetch-Site": "cross-site"
+        },
+        "status": 403
+      }
+    ],
+    "upstreamSends": 0
+  },
+  "evidenceFile": "host-origin-probe.json"
+}
Prompt CJK dài được nâng tier theo số tokenĐạt
Chi tiết
{
+  "tier": "COMPLEX",
+  "reason": "Input exceeds 100000 tokens"
+}
Reservation đồng thời và settlement trả lại ngân sáchĐạt
Chi tiết
{
+  "first": 200,
+  "whileInFlight": 429,
+  "afterSettlement": 200
+}
Refresh 5 phút và phục hồi catalogĐạt
Chi tiết
{
+  "intervals": [
+    {
+      "gateway": "stable",
+      "intervalMs": 300011
+    },
+    {
+      "gateway": "recovery",
+      "intervalMs": 300012
+    }
+  ],
+  "lastGoodRetained": true,
+  "recoveredFromInvalidStartup": true
+}
Accounting chỉ ghi vào thư mục thử nghiệmĐạt
Chi tiết
{
+  "isolatedProfiles": 3,
+  "historyCounts": [
+    20,
+    0,
+    2
+  ],
+  "syntheticOnly": true
+}
+

Browser và đóng tiến trình

Playwright xác minh health, catalog và chat từ cùng origin; điều hướng cross-site nhận HTTP 403. Các tiến trình CLI thử nghiệm đã thoát và các cổng của chúng không còn truy cập được.

Bằng chứng chi tiết
{
+  "browser": {
+    "chatStatus": 200,
+    "sameOriginHealthStatus": "ok",
+    "chatModel": "fixture/reasoner",
+    "requestFetchSite": "same-origin",
+    "driver": "MCP Playwright",
+    "catalogDiscovered": true,
+    "healthVersion": "422774f",
+    "crossSiteNavigationStatusObserved": 403
+  },
+  "shutdown": {
+    "allOwnedProcessesExited": true,
+    "portsClosed": true
+  }
+}
+

Hiệu chỉnh công cụ kiểm tra

Lần đầu, Node fetch bỏ qua Host giả mạo nên phép thử nhận 429. Gửi lại bằng HTTP trực tiếp giữ nguyên Host cho kết quả 403 ở cả bốn tình huống, không gửi request tới gateway. Bằng chứng lỗi ban đầu và lần kiểm tra lại đều được giữ trong JSON.

+

Giới hạn của kết luận

Repo chưa có môi trường staging từ xa được cấu hình. Không gọi model live, không có giao dịch thanh toán và không thay đổi dịch vụ đang phục vụ. Phí trong fixture là dữ liệu giả để kiểm tra accounting.

Bằng chứng JSON

\ No newline at end of file diff --git a/docs/validation/2026-10-22/upstream-smoke.json b/docs/validation/2026-10-22/upstream-smoke.json new file mode 100644 index 0000000..9886cbc --- /dev/null +++ b/docs/validation/2026-10-22/upstream-smoke.json @@ -0,0 +1,664 @@ +{ + "schema": 1, + "startedAt": "2026-10-22T14:28:38.294Z", + "revision": "422774fe3ba3d94c3e9be2e7082945ed54222770", + "binarySha256": "5bd95979b1176002133def84734e3b98cd6d70cb107d58627ffb00cede9f7a27", + "buildInfo": [ + "D:\\Projects\\DOSRouter\\.codex\\worktrees\\sync-clawrouter-20261022\\.smoke-artifacts\\dosrouter-smoke.exe: go1.26.10", + "\tpath\tgithub.com/DOS/DOSRouter/cmd/dosrouter", + "\tmod\tgithub.com/DOS/DOSRouter\tv0.13.0-go.0.20261022124230-422774fe3ba3\t", + "\tdep\tgolang.org/x/crypto\tv0.50.0\th1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=", + "\tdep\tgolang.org/x/sys\tv0.43.0\th1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=", + "\tbuild\t-buildmode=exe", + "\tbuild\t-compiler=gc", + "\tbuild\t-trimpath=true", + "\tbuild\tDefaultGODEBUG=cryptocustomrand=1,httpservecontentmaxranges=1,tlssecpmlkem=0,urlstrictcolons=0", + "\tbuild\tCGO_ENABLED=0", + "\tbuild\tGOARCH=amd64", + "\tbuild\tGOOS=windows", + "\tbuild\tGOAMD64=v1", + "\tbuild\tvcs=git", + "\tbuild\tvcs.revision=422774fe3ba3d94c3e9be2e7082945ed54222770", + "\tbuild\tvcs.time=2026-10-22T12:42:30Z", + "\tbuild\tvcs.modified=false" + ], + "scope": "Actual CLI processes; synthetic localhost gateway; no live provider or payment.", + "checks": [ + { + "id": "health", + "label": "Health và revision của tiến trình CLI", + "pass": true, + "durationMs": 12, + "details": { + "gateway": "http://127.0.0.1:57085", + "journalEntries": 0, + "journalSessions": 0, + "sessions": 0, + "spendControl": { + "perRequest": { + "limit": 0.05, + "spent": 0, + "remaining": 0.05 + }, + "session": { + "limit": 10, + "spent": 0, + "remaining": 10 + } + }, + "status": "ok", + "version": "422774f" + } + }, + { + "id": "startup-catalog", + "label": "Catalog mới tự nạp khi khởi động", + "pass": true, + "durationMs": 11, + "details": { + "reads": 1, + "modelVisible": true + } + }, + { + "id": "invalid-startup", + "label": "Catalog lỗi giữ bản đóng gói", + "pass": true, + "durationMs": 15, + "details": { + "bundledFallback": true + } + }, + { + "id": "reasoning", + "label": "Model reasoning mới chuyển tiếp đúng metadata", + "pass": true, + "durationMs": 19, + "details": { + "status": 200, + "trace": [ + { + "gateway": "stable", + "tag": "reasoning", + "model": "fixture/reasoner", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + } + ] + } + }, + { + "id": "free-alias", + "label": "Alias miễn phí không thành model trả phí", + "pass": true, + "durationMs": 11, + "details": [ + { + "gateway": "stable", + "tag": "free-alias", + "model": "free/nemotron-3.5-lightning", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + } + ] + }, + { + "id": "outcome-memory", + "label": "Model lỗi lặp lại được chuyển xuống cuối", + "pass": true, + "durationMs": 59, + "details": [ + { + "gateway": "stable", + "tag": "repeat-failure-0", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-0", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-1", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-1", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-2", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-2", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-3", + "model": "google/gemini-3-flash-preview", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + } + ] + }, + { + "id": "empty-chain", + "label": "Phản hồi rỗng tiếp tục tới model có kết quả", + "pass": true, + "durationMs": 18, + "details": [ + { + "gateway": "stable", + "tag": "empty-chain", + "model": "google/gemini-3.5-flash", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "empty-chain", + "model": "google/gemini-3.6-flash", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "empty-chain", + "model": "anthropic/claude-haiku-4.5", + "sequence": 3, + "status": 200, + "reasoningNormalized": true + } + ] + }, + { + "id": "attempt-cap", + "label": "Chuỗi lỗi dừng sau tối đa 5 model", + "pass": true, + "durationMs": 17, + "details": [ + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.6-flash", + "sequence": 2, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "anthropic/claude-haiku-4.5", + "sequence": 3, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "zai/glm-5.3", + "sequence": 4, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.5-flash-lite", + "sequence": 5, + "status": 502, + "reasoningNormalized": true + } + ] + }, + { + "id": "explicit-pin", + "label": "Model chỉ định không bị thay bằng fallback", + "pass": true, + "durationMs": 22, + "details": [ + { + "gateway": "stable", + "tag": "explicit-pin-fails", + "model": "openai/gpt-4o-mini", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + } + ] + }, + { + "id": "lost-send", + "label": "Mất phản hồi không làm gửi lại request", + "pass": true, + "durationMs": 15, + "details": [ + { + "gateway": "stable", + "tag": "lost-response", + "model": "google/gemini-3-flash-preview", + "sequence": 1, + "status": "socket-closed", + "reasoningNormalized": true + } + ] + }, + { + "id": "stream", + "label": "SSE giữ nội dung và dấu kết thúc", + "pass": true, + "durationMs": 13, + "details": { + "status": 200, + "done": true + } + }, + { + "id": "expensive-admission", + "label": "Giá cao bị chặn trước khi gửi", + "pass": true, + "durationMs": 11, + "details": { + "status": 429, + "upstreamSends": 0 + } + }, + { + "id": "zero-flat", + "label": "Giá cố định bằng 0 không vượt spend limit", + "pass": true, + "durationMs": 13, + "details": { + "status": 429, + "upstreamSends": 0 + } + }, + { + "id": "local-trust", + "label": "Host và Origin không tin cậy bị từ chối", + "pass": true, + "durationMs": 31, + "details": { + "initialDriverFailure": { + "error": "Expected values to be strictly equal:\n\n429 !== 403\n" + }, + "recheck": { + "checkedAt": "2026-10-22T14:32:04.813Z", + "revision": "422774fe3ba3d94c3e9be2e7082945ed54222770", + "binarySha256": "5bd95979b1176002133def84734e3b98cd6d70cb107d58627ffb00cede9f7a27", + "transport": "node:http sends the literal Host header", + "originalDriverIssue": "Node fetch ignored the overridden Host header and sent the loopback host; its 429 was a driver error, not a guard failure.", + "results": [ + { + "headers": { + "Origin": "https://attacker.invalid" + }, + "status": 403 + }, + { + "headers": { + "Origin": "null" + }, + "status": 403 + }, + { + "headers": { + "Host": "attacker.invalid" + }, + "status": 403 + }, + { + "headers": { + "Sec-Fetch-Site": "cross-site" + }, + "status": 403 + } + ], + "upstreamSends": 0 + }, + "evidenceFile": "host-origin-probe.json" + } + }, + { + "id": "cjk", + "label": "Prompt CJK dài được nâng tier theo số token", + "pass": true, + "durationMs": 47, + "details": { + "tier": "COMPLEX", + "reason": "Input exceeds 100000 tokens" + } + }, + { + "id": "concurrency", + "label": "Reservation đồng thời và settlement trả lại ngân sách", + "pass": true, + "durationMs": 34, + "details": { + "first": 200, + "whileInFlight": 429, + "afterSettlement": 200 + } + }, + { + "id": "scheduled-refresh", + "label": "Refresh 5 phút và phục hồi catalog", + "pass": true, + "durationMs": 299443, + "details": { + "intervals": [ + { + "gateway": "stable", + "intervalMs": 300011 + }, + { + "gateway": "recovery", + "intervalMs": 300012 + } + ], + "lastGoodRetained": true, + "recoveredFromInvalidStartup": true + } + }, + { + "id": "isolated-state", + "label": "Accounting chỉ ghi vào thư mục thử nghiệm", + "pass": true, + "durationMs": 1, + "details": { + "isolatedProfiles": 3, + "historyCounts": [ + 20, + 0, + 2 + ], + "syntheticOnly": true + } + } + ], + "trace": [ + { + "gateway": "stable", + "tag": "reasoning", + "model": "fixture/reasoner", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "free-alias", + "model": "free/nemotron-3.5-lightning", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-0", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-0", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-1", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-1", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-2", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-2", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-3", + "model": "google/gemini-3-flash-preview", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "empty-chain", + "model": "google/gemini-3.5-flash", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "empty-chain", + "model": "google/gemini-3.6-flash", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "empty-chain", + "model": "anthropic/claude-haiku-4.5", + "sequence": 3, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.6-flash", + "sequence": 2, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "anthropic/claude-haiku-4.5", + "sequence": 3, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "zai/glm-5.3", + "sequence": 4, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.5-flash-lite", + "sequence": 5, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "explicit-pin-fails", + "model": "openai/gpt-4o-mini", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "lost-response", + "model": "google/gemini-3-flash-preview", + "sequence": 1, + "status": "socket-closed", + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "stream", + "model": "fixture/reasoner", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "concurrency", + "tag": "concurrency-first", + "model": "fixture/reasoner", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "concurrency", + "tag": "concurrency-third", + "model": "fixture/reasoner", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "browser-same-origin", + "model": "fixture/reasoner", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + } + ], + "catalogReads": [ + { + "gateway": "stable", + "count": 1, + "at": "2026-10-22T14:28:38.690Z", + "mode": "valid" + }, + { + "gateway": "recovery", + "count": 1, + "at": "2026-10-22T14:28:38.822Z", + "mode": "invalid" + }, + { + "gateway": "concurrency", + "count": 1, + "at": "2026-10-22T14:28:38.943Z", + "mode": "valid" + }, + { + "gateway": "stable", + "count": 2, + "at": "2026-10-22T14:33:38.701Z", + "mode": "invalid" + }, + { + "gateway": "recovery", + "count": 2, + "at": "2026-10-22T14:33:38.834Z", + "mode": "valid" + }, + { + "gateway": "concurrency", + "count": 2, + "at": "2026-10-22T14:33:38.955Z", + "mode": "valid" + } + ], + "browser": { + "chatStatus": 200, + "sameOriginHealthStatus": "ok", + "chatModel": "fixture/reasoner", + "requestFetchSite": "same-origin", + "driver": "MCP Playwright", + "catalogDiscovered": true, + "healthVersion": "422774f", + "crossSiteNavigationStatusObserved": 403 + }, + "shutdown": { + "allOwnedProcessesExited": true, + "portsClosed": true + }, + "apiFinishedAt": "2026-10-22T14:33:38.847Z", + "finishedAt": "2026-10-22T14:38:08.308Z", + "harnessCorrections": [ + { + "check": "local-trust", + "reason": "Node fetch ignored the overridden Host header and sent the loopback host; its 429 was a driver error, not a guard failure.", + "recheckedAt": "2026-10-22T14:32:04.813Z", + "sourceFix": "Use node:http to send the literal Host header. Runtime binary unchanged." + } + ] +} diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..6b1e347 --- /dev/null +++ b/tools/README.md @@ -0,0 +1,34 @@ +# CLI process acceptance + +`smoke-cli.mjs` runs the real Windows CLI against a synthetic HTTP gateway +bound to `127.0.0.1`. It requires Node.js 26, the Go toolchain, Git and a +clean-source CLI binary with Go VCS metadata. No packages are installed. + +Build the binary with `-buildvcs=true -trimpath` and set +`github.com/DOS/DOSRouter/proxy.Version` to the first seven characters of +the source revision. Supply its absolute path through `--binary` and a +disposable evidence directory through `--output`. `--revision` can pin +an earlier source commit; otherwise the harness requires the current HEAD. +The embedded revision must match and `vcs.modified` must be false. + +The harness creates three isolated USERPROFILE directories below the ignored +`.smoke-artifacts` directory. It clears API-key/proxy overrides and uses only +synthetic spending state. It never contacts a real provider. It is Windows-only +because USERPROFILE isolation would not protect a Unix user's HOME. + +The suite takes just over five minutes because it observes the real catalog +refresh timer. It writes `upstream-smoke.json` and a Vietnamese HTML report. +Without `--serve`, it stops its own children and exits nonzero for a failed +scenario or incomplete shutdown. Browser acceptance remains explicitly pending. + +With `--serve`, the process prints localhost fixture, proxy and report URLs and +stays available for independent browser testing. Use the browser's same-origin +fetch for health, models and chat; cross-site navigation must return 403. Only +after those observations, POST the observed proof to the fixture's `/__finish` +endpoint. This records that proof and stops the CLI children. POST `/__stop` to +close the remaining fixture. Ctrl+C also stops owned children. These control +endpoints exist only in this synthetic test fixture, never in the product. + +Reports must distinguish synthetic process acceptance from live provider or +deployment evidence. Review corrections to the test driver separately and keep +the original failed observation alongside any same-binary recheck. diff --git a/tools/smoke-cli.mjs b/tools/smoke-cli.mjs new file mode 100644 index 0000000..c60463e --- /dev/null +++ b/tools/smoke-cli.mjs @@ -0,0 +1,138 @@ +import { renderReport } from './smoke-report.mjs'; +// Process acceptance against synthetic localhost services, with isolated state. +import assert from 'node:assert/strict'; +import { createHash, randomUUID } from 'node:crypto'; +import { execFileSync, spawn } from 'node:child_process'; +import { once } from 'node:events'; +import fs from 'node:fs/promises'; +import http from 'node:http'; +import net from 'node:net'; +import path from 'node:path'; + +assert.equal(process.platform, 'win32', 'This harness isolates USERPROFILE and is Windows-only'); +const args = process.argv.slice(2); +const option = name => args[args.indexOf(name) + 1]; +assert(args.includes('--binary') && args.includes('--output'), '--binary and --output are required'); +const binary = path.resolve(option('--binary')); +const output = path.resolve(option('--output')); +const runtime = path.resolve('.smoke-artifacts', `run-${randomUUID()}`); +await fs.mkdir(runtime, { recursive: true }); +await fs.mkdir(output, { recursive: true }); +const revision = execFileSync('git', ['rev-parse', '--verify', `${args.includes('--revision') ? option('--revision') : 'HEAD'}^{commit}`], { encoding: 'utf8' }).trim(); +const buildInfo = execFileSync('go', ['version', '-m', binary], { encoding: 'utf8' }); +assert(buildInfo.includes(`vcs.revision=${revision}`), 'binary and checkout revisions differ'); +assert(buildInfo.includes('vcs.modified=false'), 'binary was not built from clean source'); +const evidence = { schema: 1, startedAt: new Date().toISOString(), revision, + binarySha256: createHash('sha256').update(await fs.readFile(binary)).digest('hex'), + buildInfo: buildInfo.split(/\r?\n/).filter(Boolean), + scope: 'Actual CLI processes; synthetic localhost gateway; no live provider or payment.', + checks: [], trace: [], catalogReads: [], browser: null, shutdown: null }; +const modes = { stable: 'valid', recovery: 'invalid', concurrency: 'valid' }; +const reads = { stable: 0, recovery: 0, concurrency: 0 }; +const caseCounts = new Map(), tickWaiters = new Map(), children = []; +let releaseHold, markHold, server; +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); +const exited = child => child.exitCode !== null || child.signalCode !== null; + +const report = () => renderReport(evidence); + +async function save() { + await fs.writeFile(path.join(output,'upstream-smoke.json'),JSON.stringify(evidence,null,2)+'\n'); + await fs.writeFile(path.join(output,'upstream-smoke.html'),report()); +} +async function check(id,label,fn) { + const at=Date.now(); + try { const details=await fn();evidence.checks.push({id,label,pass:true,durationMs:Date.now()-at,details}); } + catch(error) { evidence.checks.push({id,label,pass:false,durationMs:Date.now()-at,details:{error:error.message}}); } + await save(); console.log(JSON.stringify({check:id,pass:evidence.checks.at(-1).pass})); +} +const row=(id,pricing,categories=['chat'],billing_mode='paid')=>({id,name:id,categories,billing_mode,pricing,context_window:128000,max_output:32000}); +function complete(w,model,tag,stream=false) { + w.setHeader('X-DOS-Cost-USD',model.startsWith('free/')?'0':'0.001'); + w.setHeader('X-DOS-Request-Id',`fixture-${tag}`); + if(stream) { + w.setHeader('Content-Type','text/event-stream'); + w.end(`data: ${JSON.stringify({model,choices:[{index:0,delta:{content:'fixture-stream-ok'},finish_reason:null}]})}\n\ndata: ${JSON.stringify({model,choices:[{index:0,delta:{},finish_reason:'stop'}],usage:{prompt_tokens:10,completion_tokens:5}})}\n\ndata: [DONE]\n\n`); + } else { w.setHeader('Content-Type','application/json');w.end(JSON.stringify({model,choices:[{index:0,message:{role:'assistant',content:'fixture-ok'},finish_reason:'stop'}],usage:{prompt_tokens:10,completion_tokens:5}})); } +} +async function stopChildren() { + for(const child of children) if(!exited(child)){child.kill();await once(child,'exit');} + const closed=await Promise.all(children.map(async c=>{try{await fetch(`http://127.0.0.1:${c.port}/health`,{signal:AbortSignal.timeout(500)});return false;}catch{return true;}})); + evidence.shutdown={allOwnedProcessesExited:children.every(exited),portsClosed:closed.every(Boolean)};await save(); +} +process.on('uncaughtException',async error=>{console.error(error.message);await stopChildren();server?.close();process.exit(1);}); +server=http.createServer(async(r,w)=>{ + try { + const url=new URL(r.url,'http://127.0.0.1'); + if(url.pathname==='/'||url.pathname==='/report'){w.setHeader('Content-Type','text/html; charset=utf-8');w.end(report());return;} + if(url.pathname==='/evidence.json'||url.pathname==='/upstream-smoke.json'){w.setHeader('Content-Type','application/json');w.end(JSON.stringify(evidence));return;} + if(url.pathname==='/__finish'&&r.method==='POST'){let body='';for await(const chunk of r)body+=chunk;evidence.browser=JSON.parse(body);await stopChildren();evidence.finishedAt=new Date().toISOString();await save();w.end('ok');return;} + if(url.pathname==='/__stop'&&r.method==='POST'){await stopChildren();w.end('ok');server.close();return;} + const gateway=url.pathname.split('/')[1]; + if(!Object.hasOwn(modes,gateway)){w.writeHead(404);w.end();return;} + if(url.pathname.endsWith('/v1/models')){ + reads[gateway]++;evidence.catalogReads.push({gateway,count:reads[gateway],at:new Date().toISOString(),mode:modes[gateway]});w.setHeader('Content-Type','application/json'); + w.end(JSON.stringify(modes[gateway]==='invalid'?{data:[]}:{data:[row('fixture/reasoner',{input:1,output:1},['chat','reasoning']),row('fixture/expensive',{input:1,output:1000}),row('fixture/zero',{flat:0},['chat'],'flat')]})); + tickWaiters.get(`${gateway}:${reads[gateway]}`)?.();return; + } + if(!url.pathname.endsWith('/v1/chat/completions')){w.writeHead(404);w.end();return;} + let body='';for await(const chunk of r)body+=chunk;const req=JSON.parse(body),tag=req.metadata?.smoke_case??'browser'; + const count=(caseCounts.get(tag)??0)+1;caseCounts.set(tag,count); + const record={gateway,tag,model:req.model,sequence:count,status:200,reasoningNormalized:req.messages?.filter(m=>m.role==='assistant').every(m=>typeof m.reasoning_content==='string')??true};evidence.trace.push(record); + if(tag.startsWith('repeat-failure-')&&req.model==='google/gemini-2.5-flash'||tag==='all-fail'||tag==='explicit-pin-fails'){record.status=502;w.writeHead(502,{'X-DOS-Cost-USD':'0'});w.end('{"error":{"message":"synthetic model failure"}}');return;} + if(tag==='empty-chain'&&count<=2){w.setHeader('X-DOS-Cost-USD','0');w.end('{"choices":[{"message":{"content":"","tool_calls":[]},"finish_reason":"stop"}]}');return;} + if(tag==='lost-response'){record.status='socket-closed';r.socket.destroy();return;} + if(tag==='concurrency-first'){releaseHold=()=>complete(w,req.model,tag);markHold?.();return;} + complete(w,req.model,tag,req.stream); + }catch{if(!w.headersSent)w.writeHead(500);w.end('fixture failure');} +}); +await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); +const fixture=`http://127.0.0.1:${server.address().port}`; +async function freePort(){const s=net.createServer();await new Promise(resolve=>s.listen(0,'127.0.0.1',resolve));const port=s.address().port;await new Promise(resolve=>s.close(resolve));return port;} +async function start(name,limits){ + const port=await freePort(),profile=path.join(runtime,name);await fs.mkdir(path.join(profile,'.openclaw','DOS'),{recursive:true});await fs.writeFile(path.join(profile,'.openclaw','DOS','spending.json'),JSON.stringify({limits,history:[]})); + const env={...process.env,USERPROFILE:profile,DOSROUTER_API_KEY:'',HTTP_PROXY:'',HTTPS_PROXY:'',ALL_PROXY:''}; + const child=spawn(binary,['serve','--port',String(port),'--upstream',`${fixture}/${name}`],{env,windowsHide:true,stdio:['ignore','pipe','pipe']}); + child.port=port;child.profile=profile;child.stderrText='';child.stderr.on('data',d=>{child.stderrText+=d.toString();});child.stdout.resume();children.push(child); + const base=`http://127.0.0.1:${port}`; + for(let i=0;i<60;i++){try{const res=await fetch(`${base}/health`,{signal:AbortSignal.timeout(1000)});if(res.ok)return{child,base};}catch{}await sleep(100);}throw Error('CLI did not become ready'); +} +async function jsonRequest(base,route,payload,headers={}){ + const res=await fetch(base+route,{method:payload===undefined?'GET':'POST',headers:{'Content-Type':'application/json',...headers},body:payload===undefined?undefined:JSON.stringify(payload),signal:AbortSignal.timeout(10000)}); + const text=await res.text();let body;try{body=JSON.parse(text);}catch{body=text;}return{status:res.status,headers:Object.fromEntries(res.headers),body}; +} +async function rawRequest(base,route,payload,headers={}){return new Promise((resolve,reject)=>{const req=http.request(base+route,{method:'POST',headers:{'Content-Type':'application/json',...headers}},res=>{let body='';res.on('data',c=>{body+=c});res.on('end',()=>resolve({status:res.statusCode,body}));});req.setTimeout(10000,()=>req.destroy(new Error('raw HTTP request timed out')));req.on('error',reject);req.end(JSON.stringify(payload));});} +const chat=(base,tag,model='auto',extra={})=>jsonRequest(base,'/v1/chat/completions',{model,messages:[{role:'user',content:'hello'}],max_tokens:16,metadata:{smoke_case:tag},...extra},{'X-Session-Id':randomUUID(),Authorization:'Bearer synthetic-local-smoke'}); +const traceFor=tag=>evidence.trace.filter(r=>r.tag===tag); +async function visible(base,id){const res=await jsonRequest(base,'/v1/models');return res.body.data.some(m=>m.id===id);} +async function awaitVisible(base,id){for(let i=0;i<30;i++){if(await visible(base,id))return;await sleep(100);}throw Error(`${id} was not projected`);} +const stable=await start('stable',{perRequest:0.05,session:10});await awaitVisible(stable.base,'fixture/reasoner'); +const recovery=await start('recovery',{perRequest:0.05,session:10}); +const concurrency=await start('concurrency',{perRequest:1,session:0.015});await awaitVisible(concurrency.base,'fixture/reasoner'); +console.log(JSON.stringify({ready:true,fixture,proxy:stable.base,report:fixture+'/report',binarySha256:evidence.binarySha256,revision,pids:children.map(c=>c.pid)})); +await check('health','Health và revision của tiến trình CLI',async()=>{const res=await jsonRequest(stable.base,'/health?full=true');assert.equal(res.body.status,'ok');assert.equal(res.body.version,revision.slice(0,7));return res.body;}); +await check('startup-catalog','Catalog mới tự nạp khi khởi động',async()=>{assert(await visible(stable.base,'fixture/reasoner'));assert.equal(reads.stable,1);return{reads:reads.stable,modelVisible:true};}); +await check('invalid-startup','Catalog lỗi giữ bản đóng gói',async()=>{assert(!(await visible(recovery.base,'fixture/reasoner')));assert(await visible(recovery.base,'openai/gpt-6.1-sol'));return{bundledFallback:true};}); +await check('reasoning','Model reasoning mới chuyển tiếp đúng metadata',async()=>{const res=await chat(stable.base,'reasoning','fixture/reasoner',{messages:[{role:'user',content:'hi'},{role:'assistant',content:'hello'},{role:'user',content:'continue'}]});assert.equal(res.status,200);assert(traceFor('reasoning')[0].reasoningNormalized);return{status:res.status,trace:traceFor('reasoning')};}); +await check('free-alias','Alias miễn phí không thành model trả phí',async()=>{const res=await chat(stable.base,'free-alias','v4-flash');assert.equal(res.status,200);assert.equal(traceFor('free-alias')[0].model,'free/nemotron-3.5-lightning');return traceFor('free-alias');}); +await check('outcome-memory','Model lỗi lặp lại được chuyển xuống cuối',async()=>{for(let i=0;i<4;i++)assert.equal((await chat(stable.base,`repeat-failure-${i}`)).status,200);for(let i=0;i<3;i++)assert.equal(traceFor(`repeat-failure-${i}`)[0].model,'google/gemini-2.5-flash');assert.notEqual(traceFor('repeat-failure-3')[0].model,'google/gemini-2.5-flash');return evidence.trace.filter(t=>t.tag.startsWith('repeat-failure-'));}); +await check('empty-chain','Phản hồi rỗng tiếp tục tới model có kết quả',async()=>{const res=await chat(stable.base,'empty-chain','premium');assert.equal(res.status,200);assert.equal(traceFor('empty-chain').length,3);assert.equal(res.body.choices[0].message.content,'fixture-ok');return traceFor('empty-chain');}); +await check('attempt-cap','Chuỗi lỗi dừng sau tối đa 5 model',async()=>{const res=await chat(stable.base,'all-fail','premium');assert.equal(res.status,502);assert.equal(traceFor('all-fail').length,5);return traceFor('all-fail');}); +await check('explicit-pin','Model chỉ định không bị thay bằng fallback',async()=>{const res=await chat(stable.base,'explicit-pin-fails','openai/gpt-4o-mini');assert.equal(res.status,502);assert.equal(traceFor('explicit-pin-fails').length,1);return traceFor('explicit-pin-fails');}); +await check('lost-send','Mất phản hồi không làm gửi lại request',async()=>{const res=await chat(stable.base,'lost-response');assert.equal(res.status,502);assert.equal(traceFor('lost-response').length,1);return traceFor('lost-response');}); +await check('stream','SSE giữ nội dung và dấu kết thúc',async()=>{const res=await chat(stable.base,'stream','fixture/reasoner',{stream:true});assert.equal(res.status,200);assert(res.body.includes('fixture-stream-ok'));assert(res.body.includes('data: [DONE]'));return{status:res.status,done:true};}); +await check('expensive-admission','Giá cao bị chặn trước khi gửi',async()=>{const res=await chat(stable.base,'expensive','fixture/expensive',{max_tokens:4096});assert.equal(res.status,429);assert.equal(traceFor('expensive').length,0);return{status:res.status,upstreamSends:0};}); +await check('zero-flat','Giá cố định bằng 0 không vượt spend limit',async()=>{const res=await chat(stable.base,'zero-flat','fixture/zero');assert.equal(res.status,429);assert.equal(traceFor('zero-flat').length,0);return{status:res.status,upstreamSends:0};}); +await check('local-trust','Host và Origin không tin cậy bị từ chối',async()=>{const statuses=[],before=evidence.trace.length;for(const headers of[{Origin:'https://attacker.invalid'},{Origin:'null'},{Host:'attacker.invalid'},{'Sec-Fetch-Site':'cross-site'}]){const res=await rawRequest(stable.base,'/v1/chat/completions',{},headers);assert.equal(res.status,403);statuses.push(res.status);}assert.equal(evidence.trace.length,before);return{statuses,upstreamSends:0};}); +await check('cjk','Prompt CJK dài được nâng tier theo số token',async()=>{const res=await jsonRequest(stable.base,'/debug',{prompt:'中'.repeat(110000),max_tokens:16});assert.equal(res.status,200);assert.equal(res.body.decision.Tier,'COMPLEX');assert(res.body.decision.Reasoning.includes('Input exceeds'));return{tier:res.body.decision.Tier,reason:res.body.decision.Reasoning};}); +await check('concurrency','Reservation đồng thời và settlement trả lại ngân sách',async()=>{const held=new Promise(resolve=>{markHold=resolve;});const first=chat(concurrency.base,'concurrency-first','fixture/reasoner',{max_tokens:10000});await Promise.race([held,sleep(10000).then(()=>{throw Error('first request never arrived');})]);const second=await chat(concurrency.base,'concurrency-second','fixture/reasoner',{max_tokens:10000});assert.equal(second.status,429);assert.equal(traceFor('concurrency-second').length,0);releaseHold();assert.equal((await first).status,200);const third=await chat(concurrency.base,'concurrency-third','fixture/reasoner',{max_tokens:10000});assert.equal(third.status,200);return{first:200,whileInFlight:429,afterSettlement:200};}); +const stableTick=new Promise(resolve=>tickWaiters.set('stable:2',resolve)); +const recoveryTick=new Promise(resolve=>tickWaiters.set('recovery:2',resolve)); +modes.stable='invalid';modes.recovery='valid'; +console.log(JSON.stringify({waitingFor:'real five-minute catalog refresh',firstReads:evidence.catalogReads})); +await check('scheduled-refresh','Refresh 5 phút và phục hồi catalog',async()=>{await Promise.race([Promise.all([stableTick,recoveryTick]),sleep(330000).then(()=>{throw Error('scheduled refresh deadline exceeded');})]);await awaitVisible(recovery.base,'fixture/reasoner');assert(await visible(stable.base,'fixture/reasoner'));const intervals=['stable','recovery'].map(gateway=>{const entries=evidence.catalogReads.filter(r=>r.gateway===gateway);return{gateway,intervalMs:Date.parse(entries[1].at)-Date.parse(entries[0].at)};});for(const entry of intervals)assert(entry.intervalMs>=295000&&entry.intervalMs<=325000);return{intervals,lastGoodRetained:true,recoveredFromInvalidStartup:true};}); +await check('isolated-state','Accounting chỉ ghi vào thư mục thử nghiệm',async()=>{const states=await Promise.all(children.map(async c=>JSON.parse(await fs.readFile(path.join(c.profile,'.openclaw','DOS','spending.json'),'utf8'))));assert(states[0].history.length>0);assert.equal(states[2].history.length,2);return{isolatedProfiles:children.length,historyCounts:states.map(s=>s.history.length),syntheticOnly:true};}); +evidence.apiFinishedAt=new Date().toISOString();await save(); +console.log(JSON.stringify({apiFinished:true,passed:evidence.checks.filter(c=>c.pass).length,total:evidence.checks.length,fixture,proxy:stable.base,report:fixture+'/report'})); +if(!args.includes('--serve')){await stopChildren();evidence.finishedAt=new Date().toISOString();await save();server.close();process.exitCode=evidence.checks.every(c=>c.pass)&&evidence.shutdown.allOwnedProcessesExited&&evidence.shutdown.portsClosed?0:1;} +process.on('SIGINT',async()=>{await stopChildren();server.close();}); diff --git a/tools/smoke-report.mjs b/tools/smoke-report.mjs new file mode 100644 index 0000000..dcb1cf2 --- /dev/null +++ b/tools/smoke-report.mjs @@ -0,0 +1,15 @@ +const escape = v => String(v).replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c])); +export function renderReport(evidence) { + const passed = evidence.checks.filter(c => c.pass).length; + const browserPassed = evidence.browser?.sameOriginHealthStatus === 'ok' && evidence.browser?.chatStatus === 200 && evidence.browser?.catalogDiscovered === true && evidence.browser?.crossSiteNavigationStatusObserved === 403; + const shutdownPassed = evidence.shutdown?.allOwnedProcessesExited === true && evidence.shutdown?.portsClosed === true; + return `DOSRouter - Xác minh runtime +
DOS / ROUTER

Xác minh bản chạy sau đồng bộ upstream

Binary CLI thật, giao tiếp HTTP với gateway mô phỏng. Đây là kết quả cục bộ, chưa xác nhận provider live hoặc triển khai từ xa.

+
${passed}/${evidence.checks.length}Kiểm tra đạt
${browserPassed ? 'Đạt' : 'Chưa xác nhận'}Playwright
${shutdownPassed ? 'Đã dừng' : 'Chưa xác nhận'}Tiến trình thử nghiệm
+

Bản được xác minh

Commit: ${escape(evidence.revision)}

SHA-256 binary: ${escape(evidence.binarySha256)}

VCS revision khớp commit và vcs.modified=false. Mỗi tiến trình dùng thư mục người dùng riêng, không dùng API key thật.

+

Kết quả kiểm tra

${evidence.checks.map(c => ``).join('')}
Kịch bảnKết quảBằng chứng
${escape(c.label)}${c.pass ? 'Đạt':'Chưa đạt'}
Chi tiết
${escape(JSON.stringify(c.details,null,2))}
+

Browser và đóng tiến trình

${browserPassed ? 'Playwright xác minh health, catalog và chat từ cùng origin; điều hướng cross-site nhận HTTP 403.' : 'Browser chưa có đủ bằng chứng đạt.'} ${shutdownPassed ? 'Các tiến trình CLI thử nghiệm đã thoát và các cổng của chúng không còn truy cập được.' : 'Chưa xác nhận các tiến trình thử nghiệm đã đóng hoàn toàn.'}

Bằng chứng chi tiết
${escape(JSON.stringify({browser:evidence.browser,shutdown:evidence.shutdown},null,2))}
+ ${evidence.harnessCorrections?.length ? `

Hiệu chỉnh công cụ kiểm tra

Lần đầu, Node fetch bỏ qua Host giả mạo nên phép thử nhận 429. Gửi lại bằng HTTP trực tiếp giữ nguyên Host cho kết quả 403 ở cả bốn tình huống, không gửi request tới gateway. Bằng chứng lỗi ban đầu và lần kiểm tra lại đều được giữ trong JSON.

` : ''} +

Giới hạn của kết luận

Repo chưa có môi trường staging từ xa được cấu hình. Không gọi model live, không có giao dịch thanh toán và không thay đổi dịch vụ đang phục vụ. Phí trong fixture là dữ liệu giả để kiểm tra accounting.

Bằng chứng JSON

`; +} From adbff8e891851153e6d340d84595586a1427bc2c Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:49:17 +0700 Subject: [PATCH 2/3] test: harden smoke shutdown and reasoning assertions --- UPSTREAM_SYNC.md | 27 +- .../2026-10-22/harness-recheck.json | 783 ++++++++++++++++++ .../validation/2026-10-22/report-browser.json | 4 +- tools/smoke-cli.mjs | 25 +- 4 files changed, 824 insertions(+), 15 deletions(-) create mode 100644 docs/validation/2026-10-22/harness-recheck.json diff --git a/UPSTREAM_SYNC.md b/UPSTREAM_SYNC.md index aacc13e..ca48d05 100644 --- a/UPSTREAM_SYNC.md +++ b/UPSTREAM_SYNC.md @@ -30,7 +30,7 @@ These upstream areas are excluded (TS/npm-specific): ## Sync Log -### 2026-10-22 - Post-merge runtime acceptance (in progress) +### 2026-10-22 - Post-merge local runtime acceptance - Owner: Codex, branch `codex/smoke-upstream-20261022`. - Revision under test: merged main `422774fe3ba3d94c3e9be2e7082945ed54222770`. @@ -43,8 +43,29 @@ These upstream areas are excluded (TS/npm-specific): - Environment discovery: GitHub environments list is empty; the current process has no DOSROUTER_UPSTREAM, DOSROUTER_PORT or DOSROUTER_API_KEY. Historical npm/Docker plans do not describe this standalone Go runtime. -- Next: build the clean merged revision, run process-level smoke scenarios, - preserve results and close any reproduced acceptance failure within scope. +- Completed: clean-source Go 1.26.10 binary, SHA-256 + `5bd95979b1176002133def84734e3b98cd6d70cb107d58627ffb00cede9f7a27`; + all 18 process scenarios passed. The real refresh intervals were 300011 + and 300012 ms; invalid startup recovered and invalid updates retained the + last good catalog. MCP Playwright verified same-origin health, model list + and chat, plus cross-site HTTP 403. All three CLI processes exited and + their ports closed. Report rendering passed desktop and 390px checks. +- Driver correction: Node fetch ignored a forged Host override. The original + failed observation remains recorded; literal raw HTTP against the same + binary returned four HTTP 403 responses with zero gateway sends. The fixed + harness then passed a separate full 18-scenario run without corrections. +- Independent review found two harness gaps: late SIGINT registration and a + vacuously true reasoning assertion. Both were fixed. A focused final-source + probe passed 16 immediate scenarios, preserved the exact assistant message + and model, and confirmed owned-process shutdown on Ctrl+C during the timer + wait. It makes no claim for the interrupted refresh scenario. +- Evidence: [Vietnamese report](docs/validation/2026-10-22/upstream-smoke.html), + [initial run and correction](docs/validation/2026-10-22/upstream-smoke.json), + [full rerun and final-source probes](docs/validation/2026-10-22/harness-recheck.json) + and [report browser readback](docs/validation/2026-10-22/report-browser.json). +- Remaining boundary: remote staging, paid-provider behavior and deployment + remain unverified because no target environment is configured or authorized. + No product source change was needed for the process acceptance. ### 2026-10-22 - Sync through 0.12.297 source diff --git a/docs/validation/2026-10-22/harness-recheck.json b/docs/validation/2026-10-22/harness-recheck.json new file mode 100644 index 0000000..11c755c --- /dev/null +++ b/docs/validation/2026-10-22/harness-recheck.json @@ -0,0 +1,783 @@ +{ + "recordedAt": "2026-10-22T14:49:09.666Z", + "revision": "422774fe3ba3d94c3e9be2e7082945ed54222770", + "binarySha256": "5bd95979b1176002133def84734e3b98cd6d70cb107d58627ffb00cede9f7a27", + "fullSuite": { + "harnessCommit": "6eb201b05c29c37d9f50155e06f6285442ff1d7a", + "harnessSourceSha256": "c5a18ef8df443f753578bc556451c2991e122ba7d6f4b36300e0988922c4af29", + "startedAt": "2026-10-22T14:42:39.802Z", + "finishedAt": "2026-10-22T14:47:40.090Z", + "checks": [ + { + "id": "health", + "label": "Health và revision của tiến trình CLI", + "pass": true, + "durationMs": 3, + "details": { + "gateway": "http://127.0.0.1:59966", + "journalEntries": 0, + "journalSessions": 0, + "sessions": 0, + "spendControl": { + "perRequest": { + "limit": 0.05, + "spent": 0, + "remaining": 0.05 + }, + "session": { + "limit": 10, + "spent": 0, + "remaining": 10 + } + }, + "status": "ok", + "version": "422774f" + } + }, + { + "id": "startup-catalog", + "label": "Catalog mới tự nạp khi khởi động", + "pass": true, + "durationMs": 6, + "details": { + "reads": 1, + "modelVisible": true + } + }, + { + "id": "invalid-startup", + "label": "Catalog lỗi giữ bản đóng gói", + "pass": true, + "durationMs": 16, + "details": { + "bundledFallback": true + } + }, + { + "id": "reasoning", + "label": "Model reasoning mới chuyển tiếp đúng metadata", + "pass": true, + "durationMs": 8, + "details": { + "status": 200, + "trace": [ + { + "gateway": "stable", + "tag": "reasoning", + "model": "fixture/reasoner", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + } + ] + } + }, + { + "id": "free-alias", + "label": "Alias miễn phí không thành model trả phí", + "pass": true, + "durationMs": 8, + "details": [ + { + "gateway": "stable", + "tag": "free-alias", + "model": "free/nemotron-3.5-lightning", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + } + ] + }, + { + "id": "outcome-memory", + "label": "Model lỗi lặp lại được chuyển xuống cuối", + "pass": true, + "durationMs": 60, + "details": [ + { + "gateway": "stable", + "tag": "repeat-failure-0", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-0", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-1", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-1", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-2", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-2", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "repeat-failure-3", + "model": "google/gemini-3-flash-preview", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + } + ] + }, + { + "id": "empty-chain", + "label": "Phản hồi rỗng tiếp tục tới model có kết quả", + "pass": true, + "durationMs": 20, + "details": [ + { + "gateway": "stable", + "tag": "empty-chain", + "model": "google/gemini-3.5-flash", + "sequence": 1, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "empty-chain", + "model": "google/gemini-3.6-flash", + "sequence": 2, + "status": 200, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "empty-chain", + "model": "anthropic/claude-haiku-4.5", + "sequence": 3, + "status": 200, + "reasoningNormalized": true + } + ] + }, + { + "id": "attempt-cap", + "label": "Chuỗi lỗi dừng sau tối đa 5 model", + "pass": true, + "durationMs": 20, + "details": [ + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.5-flash", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.6-flash", + "sequence": 2, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "anthropic/claude-haiku-4.5", + "sequence": 3, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "zai/glm-5.3", + "sequence": 4, + "status": 502, + "reasoningNormalized": true + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.5-flash-lite", + "sequence": 5, + "status": 502, + "reasoningNormalized": true + } + ] + }, + { + "id": "explicit-pin", + "label": "Model chỉ định không bị thay bằng fallback", + "pass": true, + "durationMs": 17, + "details": [ + { + "gateway": "stable", + "tag": "explicit-pin-fails", + "model": "openai/gpt-4o-mini", + "sequence": 1, + "status": 502, + "reasoningNormalized": true + } + ] + }, + { + "id": "lost-send", + "label": "Mất phản hồi không làm gửi lại request", + "pass": true, + "durationMs": 15, + "details": [ + { + "gateway": "stable", + "tag": "lost-response", + "model": "google/gemini-3-flash-preview", + "sequence": 1, + "status": "socket-closed", + "reasoningNormalized": true + } + ] + }, + { + "id": "stream", + "label": "SSE giữ nội dung và dấu kết thúc", + "pass": true, + "durationMs": 15, + "details": { + "status": 200, + "done": true + } + }, + { + "id": "expensive-admission", + "label": "Giá cao bị chặn trước khi gửi", + "pass": true, + "durationMs": 10, + "details": { + "status": 429, + "upstreamSends": 0 + } + }, + { + "id": "zero-flat", + "label": "Giá cố định bằng 0 không vượt spend limit", + "pass": true, + "durationMs": 14, + "details": { + "status": 429, + "upstreamSends": 0 + } + }, + { + "id": "local-trust", + "label": "Host và Origin không tin cậy bị từ chối", + "pass": true, + "durationMs": 5, + "details": { + "statuses": [ + 403, + 403, + 403, + 403 + ], + "upstreamSends": 0 + } + }, + { + "id": "cjk", + "label": "Prompt CJK dài được nâng tier theo số token", + "pass": true, + "durationMs": 28, + "details": { + "tier": "COMPLEX", + "reason": "Input exceeds 100000 tokens" + } + }, + { + "id": "concurrency", + "label": "Reservation đồng thời và settlement trả lại ngân sách", + "pass": true, + "durationMs": 43, + "details": { + "first": 200, + "whileInFlight": 429, + "afterSettlement": 200 + } + }, + { + "id": "scheduled-refresh", + "label": "Refresh 5 phút và phục hồi catalog", + "pass": true, + "durationMs": 299512, + "details": { + "intervals": [ + { + "gateway": "stable", + "intervalMs": 300023 + }, + { + "gateway": "recovery", + "intervalMs": 300023 + } + ], + "lastGoodRetained": true, + "recoveredFromInvalidStartup": true + } + }, + { + "id": "isolated-state", + "label": "Accounting chỉ ghi vào thư mục thử nghiệm", + "pass": true, + "durationMs": 1, + "details": { + "isolatedProfiles": 3, + "historyCounts": [ + 20, + 0, + 2 + ], + "syntheticOnly": true + } + } + ], + "catalogReads": [ + { + "gateway": "stable", + "count": 1, + "at": "2026-10-22T14:42:39.877Z", + "mode": "valid" + }, + { + "gateway": "recovery", + "count": 1, + "at": "2026-10-22T14:42:40.026Z", + "mode": "invalid" + }, + { + "gateway": "concurrency", + "count": 1, + "at": "2026-10-22T14:42:40.153Z", + "mode": "valid" + }, + { + "gateway": "stable", + "count": 2, + "at": "2026-10-22T14:47:39.900Z", + "mode": "invalid" + }, + { + "gateway": "recovery", + "count": 2, + "at": "2026-10-22T14:47:40.049Z", + "mode": "valid" + } + ], + "shutdown": { + "allOwnedProcessesExited": true, + "portsClosed": true + } + }, + "focusedDelta": { + "harnessSourceSha256": "bc234550786661cb32a1f03b2fb443822761de9a68f6d4bbd833a3e5500c71ba", + "startedAt": "2026-10-22T14:47:44.995Z", + "checks": [ + { + "id": "health", + "label": "Health và revision của tiến trình CLI", + "pass": true, + "durationMs": 2, + "details": { + "gateway": "http://127.0.0.1:64752", + "journalEntries": 0, + "journalSessions": 0, + "sessions": 0, + "spendControl": { + "perRequest": { + "limit": 0.05, + "spent": 0, + "remaining": 0.05 + }, + "session": { + "limit": 10, + "spent": 0, + "remaining": 10 + } + }, + "status": "ok", + "version": "422774f" + } + }, + { + "id": "startup-catalog", + "label": "Catalog mới tự nạp khi khởi động", + "pass": true, + "durationMs": 6, + "details": { + "reads": 1, + "modelVisible": true + } + }, + { + "id": "invalid-startup", + "label": "Catalog lỗi giữ bản đóng gói", + "pass": true, + "durationMs": 17, + "details": { + "bundledFallback": true + } + }, + { + "id": "reasoning", + "label": "Model reasoning mới chuyển tiếp đúng metadata", + "pass": true, + "durationMs": 18, + "details": { + "status": 200, + "trace": [ + { + "gateway": "stable", + "tag": "reasoning", + "model": "fixture/reasoner", + "sequence": 1, + "status": 200, + "assistantCount": 1, + "assistantContents": [ + "hello" + ], + "reasoningNormalized": true + } + ] + } + }, + { + "id": "free-alias", + "label": "Alias miễn phí không thành model trả phí", + "pass": true, + "durationMs": 8, + "details": [ + { + "gateway": "stable", + "tag": "free-alias", + "model": "free/nemotron-3.5-lightning", + "sequence": 1, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "outcome-memory", + "label": "Model lỗi lặp lại được chuyển xuống cuối", + "pass": true, + "durationMs": 60, + "details": [ + { + "gateway": "stable", + "tag": "repeat-failure-0", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-0", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-1", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-1", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-2", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-2", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-3", + "model": "google/gemini-3-flash-preview", + "sequence": 1, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "empty-chain", + "label": "Phản hồi rỗng tiếp tục tới model có kết quả", + "pass": true, + "durationMs": 11, + "details": [ + { + "gateway": "stable", + "tag": "empty-chain", + "model": "google/gemini-3.5-flash", + "sequence": 1, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "empty-chain", + "model": "google/gemini-3.6-flash", + "sequence": 2, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "empty-chain", + "model": "anthropic/claude-haiku-4.5", + "sequence": 3, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "attempt-cap", + "label": "Chuỗi lỗi dừng sau tối đa 5 model", + "pass": true, + "durationMs": 29, + "details": [ + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.5-flash", + "sequence": 1, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.6-flash", + "sequence": 2, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "anthropic/claude-haiku-4.5", + "sequence": 3, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "zai/glm-5.3", + "sequence": 4, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.5-flash-lite", + "sequence": 5, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "explicit-pin", + "label": "Model chỉ định không bị thay bằng fallback", + "pass": true, + "durationMs": 16, + "details": [ + { + "gateway": "stable", + "tag": "explicit-pin-fails", + "model": "openai/gpt-4o-mini", + "sequence": 1, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "lost-send", + "label": "Mất phản hồi không làm gửi lại request", + "pass": true, + "durationMs": 15, + "details": [ + { + "gateway": "stable", + "tag": "lost-response", + "model": "google/gemini-3-flash-preview", + "sequence": 1, + "status": "socket-closed", + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "stream", + "label": "SSE giữ nội dung và dấu kết thúc", + "pass": true, + "durationMs": 14, + "details": { + "status": 200, + "done": true + } + }, + { + "id": "expensive-admission", + "label": "Giá cao bị chặn trước khi gửi", + "pass": true, + "durationMs": 9, + "details": { + "status": 429, + "upstreamSends": 0 + } + }, + { + "id": "zero-flat", + "label": "Giá cố định bằng 0 không vượt spend limit", + "pass": true, + "durationMs": 14, + "details": { + "status": 429, + "upstreamSends": 0 + } + }, + { + "id": "local-trust", + "label": "Host và Origin không tin cậy bị từ chối", + "pass": true, + "durationMs": 5, + "details": { + "statuses": [ + 403, + 403, + 403, + 403 + ], + "upstreamSends": 0 + } + }, + { + "id": "cjk", + "label": "Prompt CJK dài được nâng tier theo số token", + "pass": true, + "durationMs": 21, + "details": { + "tier": "COMPLEX", + "reason": "Input exceeds 100000 tokens" + } + }, + { + "id": "concurrency", + "label": "Reservation đồng thời và settlement trả lại ngân sách", + "pass": true, + "durationMs": 48, + "details": { + "first": 200, + "whileInFlight": 429, + "afterSettlement": 200 + } + } + ], + "shutdown": { + "allOwnedProcessesExited": true, + "portsClosed": true + }, + "interruption": { + "method": "Ctrl+C sent to owned PTY while waiting for the first scheduled refresh", + "terminalExitCode": 1, + "note": "PowerShell PTY reported exit 1 after interruption; successful cleanup is established separately by saved process and port evidence. No scheduled-refresh result is claimed for this interrupted probe." + } + } +} diff --git a/docs/validation/2026-10-22/report-browser.json b/docs/validation/2026-10-22/report-browser.json index 779c9b0..528fcae 100644 --- a/docs/validation/2026-10-22/report-browser.json +++ b/docs/validation/2026-10-22/report-browser.json @@ -1,6 +1,5 @@ { "driver": "MCP Playwright", - "checkedAt": "2026-10-22T14:41:27Z", "desktop": { "language": "vi", "passCount": "18/18", @@ -18,5 +17,6 @@ "scrollWidth": 375, "overflow": false }, - "note": "Report and linked JSON inspected through temporary localhost HTTP. Link checked using browser fetch after native click timed out." + "note": "Report and linked JSON inspected through temporary localhost HTTP. Link checked using browser fetch after native click timed out.", + "recordedAt": "2026-10-22T14:45:15.397Z" } diff --git a/tools/smoke-cli.mjs b/tools/smoke-cli.mjs index c60463e..c8f2357 100644 --- a/tools/smoke-cli.mjs +++ b/tools/smoke-cli.mjs @@ -3,7 +3,6 @@ import { renderReport } from './smoke-report.mjs'; import assert from 'node:assert/strict'; import { createHash, randomUUID } from 'node:crypto'; import { execFileSync, spawn } from 'node:child_process'; -import { once } from 'node:events'; import fs from 'node:fs/promises'; import http from 'node:http'; import net from 'node:net'; @@ -30,7 +29,7 @@ const evidence = { schema: 1, startedAt: new Date().toISOString(), revision, const modes = { stable: 'valid', recovery: 'invalid', concurrency: 'valid' }; const reads = { stable: 0, recovery: 0, concurrency: 0 }; const caseCounts = new Map(), tickWaiters = new Map(), children = []; -let releaseHold, markHold, server; +let releaseHold, markHold, server, cleanupPromise; const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); const exited = child => child.exitCode !== null || child.signalCode !== null; @@ -56,17 +55,23 @@ function complete(w,model,tag,stream=false) { } else { w.setHeader('Content-Type','application/json');w.end(JSON.stringify({model,choices:[{index:0,message:{role:'assistant',content:'fixture-ok'},finish_reason:'stop'}],usage:{prompt_tokens:10,completion_tokens:5}})); } } async function stopChildren() { - for(const child of children) if(!exited(child)){child.kill();await once(child,'exit');} - const closed=await Promise.all(children.map(async c=>{try{await fetch(`http://127.0.0.1:${c.port}/health`,{signal:AbortSignal.timeout(500)});return false;}catch{return true;}})); - evidence.shutdown={allOwnedProcessesExited:children.every(exited),portsClosed:closed.every(Boolean)};await save(); + if(cleanupPromise)return cleanupPromise; + cleanupPromise=(async()=>{ + const waitForExit=child=>new Promise(resolve=>{if(exited(child)){resolve();return;}const done=()=>{clearTimeout(timer);child.off('exit',done);resolve();};const timer=setTimeout(done,5000);child.once('exit',done);}); + await Promise.all(children.map(async child=>{if(!exited(child)){child.kill();await waitForExit(child);}if(!exited(child)){child.kill('SIGKILL');await waitForExit(child);}})); + const closed=await Promise.all(children.map(async c=>{try{await fetch(`http://127.0.0.1:${c.port}/health`,{signal:AbortSignal.timeout(500)});return false;}catch{return true;}})); + evidence.shutdown={allOwnedProcessesExited:children.every(exited),portsClosed:closed.every(Boolean)};await save(); + })(); + return cleanupPromise; } -process.on('uncaughtException',async error=>{console.error(error.message);await stopChildren();server?.close();process.exit(1);}); +process.on('uncaughtException',async error=>{console.error(error.message);try{await stopChildren();}finally{server?.close();process.exit(1);}}); +process.on('SIGINT',async()=>{try{await stopChildren();}finally{server?.close();process.exit(130);}}); server=http.createServer(async(r,w)=>{ try { const url=new URL(r.url,'http://127.0.0.1'); if(url.pathname==='/'||url.pathname==='/report'){w.setHeader('Content-Type','text/html; charset=utf-8');w.end(report());return;} if(url.pathname==='/evidence.json'||url.pathname==='/upstream-smoke.json'){w.setHeader('Content-Type','application/json');w.end(JSON.stringify(evidence));return;} - if(url.pathname==='/__finish'&&r.method==='POST'){let body='';for await(const chunk of r)body+=chunk;evidence.browser=JSON.parse(body);await stopChildren();evidence.finishedAt=new Date().toISOString();await save();w.end('ok');return;} + if(url.pathname==='/__finish'&&r.method==='POST'){if(!evidence.apiFinishedAt){w.writeHead(409);w.end('API checks still running');return;}let body='';for await(const chunk of r)body+=chunk;evidence.browser=JSON.parse(body);await stopChildren();evidence.finishedAt=new Date().toISOString();await save();w.end('ok');return;} if(url.pathname==='/__stop'&&r.method==='POST'){await stopChildren();w.end('ok');server.close();return;} const gateway=url.pathname.split('/')[1]; if(!Object.hasOwn(modes,gateway)){w.writeHead(404);w.end();return;} @@ -78,7 +83,8 @@ server=http.createServer(async(r,w)=>{ if(!url.pathname.endsWith('/v1/chat/completions')){w.writeHead(404);w.end();return;} let body='';for await(const chunk of r)body+=chunk;const req=JSON.parse(body),tag=req.metadata?.smoke_case??'browser'; const count=(caseCounts.get(tag)??0)+1;caseCounts.set(tag,count); - const record={gateway,tag,model:req.model,sequence:count,status:200,reasoningNormalized:req.messages?.filter(m=>m.role==='assistant').every(m=>typeof m.reasoning_content==='string')??true};evidence.trace.push(record); + const assistants=Array.isArray(req.messages)?req.messages.filter(m=>m.role==='assistant'):[]; + const record={gateway,tag,model:req.model,sequence:count,status:200,assistantCount:assistants.length,assistantContents:assistants.map(m=>m.content),reasoningNormalized:assistants.length>0&&assistants.every(m=>typeof m.reasoning_content==='string')};evidence.trace.push(record); if(tag.startsWith('repeat-failure-')&&req.model==='google/gemini-2.5-flash'||tag==='all-fail'||tag==='explicit-pin-fails'){record.status=502;w.writeHead(502,{'X-DOS-Cost-USD':'0'});w.end('{"error":{"message":"synthetic model failure"}}');return;} if(tag==='empty-chain'&&count<=2){w.setHeader('X-DOS-Cost-USD','0');w.end('{"choices":[{"message":{"content":"","tool_calls":[]},"finish_reason":"stop"}]}');return;} if(tag==='lost-response'){record.status='socket-closed';r.socket.destroy();return;} @@ -113,7 +119,7 @@ console.log(JSON.stringify({ready:true,fixture,proxy:stable.base,report:fixture+ await check('health','Health và revision của tiến trình CLI',async()=>{const res=await jsonRequest(stable.base,'/health?full=true');assert.equal(res.body.status,'ok');assert.equal(res.body.version,revision.slice(0,7));return res.body;}); await check('startup-catalog','Catalog mới tự nạp khi khởi động',async()=>{assert(await visible(stable.base,'fixture/reasoner'));assert.equal(reads.stable,1);return{reads:reads.stable,modelVisible:true};}); await check('invalid-startup','Catalog lỗi giữ bản đóng gói',async()=>{assert(!(await visible(recovery.base,'fixture/reasoner')));assert(await visible(recovery.base,'openai/gpt-6.1-sol'));return{bundledFallback:true};}); -await check('reasoning','Model reasoning mới chuyển tiếp đúng metadata',async()=>{const res=await chat(stable.base,'reasoning','fixture/reasoner',{messages:[{role:'user',content:'hi'},{role:'assistant',content:'hello'},{role:'user',content:'continue'}]});assert.equal(res.status,200);assert(traceFor('reasoning')[0].reasoningNormalized);return{status:res.status,trace:traceFor('reasoning')};}); +await check('reasoning','Model reasoning mới chuyển tiếp đúng metadata',async()=>{const res=await chat(stable.base,'reasoning','fixture/reasoner',{messages:[{role:'user',content:'hi'},{role:'assistant',content:'hello'},{role:'user',content:'continue'}]});assert.equal(res.status,200);const record=traceFor('reasoning')[0];assert.equal(record.model,'fixture/reasoner');assert.equal(record.assistantCount,1);assert.deepEqual(record.assistantContents,['hello']);assert(record.reasoningNormalized);return{status:res.status,trace:traceFor('reasoning')};}); await check('free-alias','Alias miễn phí không thành model trả phí',async()=>{const res=await chat(stable.base,'free-alias','v4-flash');assert.equal(res.status,200);assert.equal(traceFor('free-alias')[0].model,'free/nemotron-3.5-lightning');return traceFor('free-alias');}); await check('outcome-memory','Model lỗi lặp lại được chuyển xuống cuối',async()=>{for(let i=0;i<4;i++)assert.equal((await chat(stable.base,`repeat-failure-${i}`)).status,200);for(let i=0;i<3;i++)assert.equal(traceFor(`repeat-failure-${i}`)[0].model,'google/gemini-2.5-flash');assert.notEqual(traceFor('repeat-failure-3')[0].model,'google/gemini-2.5-flash');return evidence.trace.filter(t=>t.tag.startsWith('repeat-failure-'));}); await check('empty-chain','Phản hồi rỗng tiếp tục tới model có kết quả',async()=>{const res=await chat(stable.base,'empty-chain','premium');assert.equal(res.status,200);assert.equal(traceFor('empty-chain').length,3);assert.equal(res.body.choices[0].message.content,'fixture-ok');return traceFor('empty-chain');}); @@ -135,4 +141,3 @@ await check('isolated-state','Accounting chỉ ghi vào thư mục thử nghiệ evidence.apiFinishedAt=new Date().toISOString();await save(); console.log(JSON.stringify({apiFinished:true,passed:evidence.checks.filter(c=>c.pass).length,total:evidence.checks.length,fixture,proxy:stable.base,report:fixture+'/report'})); if(!args.includes('--serve')){await stopChildren();evidence.finishedAt=new Date().toISOString();await save();server.close();process.exitCode=evidence.checks.every(c=>c.pass)&&evidence.shutdown.allOwnedProcessesExited&&evidence.shutdown.portsClosed?0:1;} -process.on('SIGINT',async()=>{await stopChildren();server.close();}); From be07ad9352247a7bfffe83a401564dcb1fadc92a Mon Sep 17 00:00:00 2001 From: JOY <5027251+JOY@users.noreply.github.com> Date: Thu, 22 Oct 2026 21:56:51 +0700 Subject: [PATCH 3/3] test: correlate browser proof and settle failed concurrency probes --- UPSTREAM_SYNC.md | 8 + .../2026-10-22/harness-failure-probes.json | 388 ++++++++++++++++++ .../validation/2026-10-22/upstream-smoke.html | 4 +- .../validation/2026-10-22/upstream-smoke.json | 7 +- tools/README.md | 7 + tools/browser-proof.mjs | 22 + tools/smoke-cli.mjs | 5 +- tools/smoke-report.mjs | 3 +- 8 files changed, 438 insertions(+), 6 deletions(-) create mode 100644 docs/validation/2026-10-22/harness-failure-probes.json create mode 100644 tools/browser-proof.mjs diff --git a/UPSTREAM_SYNC.md b/UPSTREAM_SYNC.md index ca48d05..0aea569 100644 --- a/UPSTREAM_SYNC.md +++ b/UPSTREAM_SYNC.md @@ -59,9 +59,17 @@ These upstream areas are excluded (TS/npm-specific): probe passed 16 immediate scenarios, preserved the exact assistant message and model, and confirmed owned-process shutdown on Ctrl+C during the timer wait. It makes no claim for the interrupted refresh scenario. +- CodeRabbit's subsequent review added browser-proof correlation and cleanup + on a failed concurrency assertion. The finish endpoint now rejects missing + gateway-chat evidence or mismatched browser fields; the renderer checks the + correlation too. An injected unexpected concurrency response still releases + and settles the held request. A final-source process probe passed all 16 + immediate scenarios and interrupt shutdown. External MCP browser observations + remain operator attestations, with gateway chat independently correlated. - Evidence: [Vietnamese report](docs/validation/2026-10-22/upstream-smoke.html), [initial run and correction](docs/validation/2026-10-22/upstream-smoke.json), [full rerun and final-source probes](docs/validation/2026-10-22/harness-recheck.json) + [failure-path and final delta probes](docs/validation/2026-10-22/harness-failure-probes.json) and [report browser readback](docs/validation/2026-10-22/report-browser.json). - Remaining boundary: remote staging, paid-provider behavior and deployment remain unverified because no target environment is configured or authorized. diff --git a/docs/validation/2026-10-22/harness-failure-probes.json b/docs/validation/2026-10-22/harness-failure-probes.json new file mode 100644 index 0000000..58d315f --- /dev/null +++ b/docs/validation/2026-10-22/harness-failure-probes.json @@ -0,0 +1,388 @@ +{ + "recordedAt": "2026-10-22T14:55:40.794Z", + "browserWithoutGatewayChat": "rejected", + "browserWrongRevision": "rejected", + "rendererWithoutGatewayChat": "not passed", + "recordedBrowserChat": "accepted with gateway correlation", + "concurrencySecondUnexpected200": { + "heldReleasedOnce": true, + "firstSettled": true, + "assertionFailurePreserved": true + }, + "finalSourceProcessProbe": { + "recordedAt": "2026-10-22T14:56:38.234Z", + "revision": "422774fe3ba3d94c3e9be2e7082945ed54222770", + "binarySha256": "5bd95979b1176002133def84734e3b98cd6d70cb107d58627ffb00cede9f7a27", + "harnessSourceSha256": "0f12761cf69b952bd4de2012a096764add2d86fc20d7f33e87914f4e940a0a15", + "browserProofSourceSha256": "b325c2b5410ede738bec3df44a71013089043f6fb5ec556467fdd651350898aa", + "checks": [ + { + "id": "health", + "label": "Health và revision của tiến trình CLI", + "pass": true, + "durationMs": 1, + "details": { + "gateway": "http://127.0.0.1:63336", + "journalEntries": 0, + "journalSessions": 0, + "sessions": 0, + "spendControl": { + "perRequest": { + "limit": 0.05, + "spent": 0, + "remaining": 0.05 + }, + "session": { + "limit": 10, + "spent": 0, + "remaining": 10 + } + }, + "status": "ok", + "version": "422774f" + } + }, + { + "id": "startup-catalog", + "label": "Catalog mới tự nạp khi khởi động", + "pass": true, + "durationMs": 5, + "details": { + "reads": 1, + "modelVisible": true + } + }, + { + "id": "invalid-startup", + "label": "Catalog lỗi giữ bản đóng gói", + "pass": true, + "durationMs": 17, + "details": { + "bundledFallback": true + } + }, + { + "id": "reasoning", + "label": "Model reasoning mới chuyển tiếp đúng metadata", + "pass": true, + "durationMs": 17, + "details": { + "status": 200, + "trace": [ + { + "gateway": "stable", + "tag": "reasoning", + "model": "fixture/reasoner", + "sequence": 1, + "status": 200, + "assistantCount": 1, + "assistantContents": [ + "hello" + ], + "reasoningNormalized": true + } + ] + } + }, + { + "id": "free-alias", + "label": "Alias miễn phí không thành model trả phí", + "pass": true, + "durationMs": 12, + "details": [ + { + "gateway": "stable", + "tag": "free-alias", + "model": "free/nemotron-3.5-lightning", + "sequence": 1, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "outcome-memory", + "label": "Model lỗi lặp lại được chuyển xuống cuối", + "pass": true, + "durationMs": 47, + "details": [ + { + "gateway": "stable", + "tag": "repeat-failure-0", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-0", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-1", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-1", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-2", + "model": "google/gemini-2.5-flash", + "sequence": 1, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-2", + "model": "google/gemini-3-flash-preview", + "sequence": 2, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "repeat-failure-3", + "model": "google/gemini-3-flash-preview", + "sequence": 1, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "empty-chain", + "label": "Phản hồi rỗng tiếp tục tới model có kết quả", + "pass": true, + "durationMs": 15, + "details": [ + { + "gateway": "stable", + "tag": "empty-chain", + "model": "google/gemini-3.5-flash", + "sequence": 1, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "empty-chain", + "model": "google/gemini-3.6-flash", + "sequence": 2, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "empty-chain", + "model": "anthropic/claude-haiku-4.5", + "sequence": 3, + "status": 200, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "attempt-cap", + "label": "Chuỗi lỗi dừng sau tối đa 5 model", + "pass": true, + "durationMs": 20, + "details": [ + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.5-flash", + "sequence": 1, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.6-flash", + "sequence": 2, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "anthropic/claude-haiku-4.5", + "sequence": 3, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "zai/glm-5.3", + "sequence": 4, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + }, + { + "gateway": "stable", + "tag": "all-fail", + "model": "google/gemini-3.5-flash-lite", + "sequence": 5, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "explicit-pin", + "label": "Model chỉ định không bị thay bằng fallback", + "pass": true, + "durationMs": 15, + "details": [ + { + "gateway": "stable", + "tag": "explicit-pin-fails", + "model": "openai/gpt-4o-mini", + "sequence": 1, + "status": 502, + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "lost-send", + "label": "Mất phản hồi không làm gửi lại request", + "pass": true, + "durationMs": 14, + "details": [ + { + "gateway": "stable", + "tag": "lost-response", + "model": "google/gemini-3-flash-preview", + "sequence": 1, + "status": "socket-closed", + "assistantCount": 0, + "assistantContents": [], + "reasoningNormalized": false + } + ] + }, + { + "id": "stream", + "label": "SSE giữ nội dung và dấu kết thúc", + "pass": true, + "durationMs": 13, + "details": { + "status": 200, + "done": true + } + }, + { + "id": "expensive-admission", + "label": "Giá cao bị chặn trước khi gửi", + "pass": true, + "durationMs": 9, + "details": { + "status": 429, + "upstreamSends": 0 + } + }, + { + "id": "zero-flat", + "label": "Giá cố định bằng 0 không vượt spend limit", + "pass": true, + "durationMs": 13, + "details": { + "status": 429, + "upstreamSends": 0 + } + }, + { + "id": "local-trust", + "label": "Host và Origin không tin cậy bị từ chối", + "pass": true, + "durationMs": 6, + "details": { + "statuses": [ + 403, + 403, + 403, + 403 + ], + "upstreamSends": 0 + } + }, + { + "id": "cjk", + "label": "Prompt CJK dài được nâng tier theo số token", + "pass": true, + "durationMs": 22, + "details": { + "tier": "COMPLEX", + "reason": "Input exceeds 100000 tokens" + } + }, + { + "id": "concurrency", + "label": "Reservation đồng thời và settlement trả lại ngân sách", + "pass": true, + "durationMs": 46, + "details": { + "first": 200, + "whileInFlight": 429, + "afterSettlement": 200 + } + } + ], + "shutdown": { + "allOwnedProcessesExited": true, + "portsClosed": true + }, + "interruption": "Intentional Ctrl+C during refresh wait; terminal exit 1. Timer result not claimed." + } +} diff --git a/docs/validation/2026-10-22/upstream-smoke.html b/docs/validation/2026-10-22/upstream-smoke.html index 8939b36..edd188d 100644 --- a/docs/validation/2026-10-22/upstream-smoke.html +++ b/docs/validation/2026-10-22/upstream-smoke.html @@ -275,7 +275,9 @@ "driver": "MCP Playwright", "catalogDiscovered": true, "healthVersion": "422774f", - "crossSiteNavigationStatusObserved": 403 + "crossSiteNavigationStatusObserved": 403, + "gatewayCorrelationVerified": true, + "evidenceKind": "External MCP browser attestation, correlated with the local gateway trace" }, "shutdown": { "allOwnedProcessesExited": true, diff --git a/docs/validation/2026-10-22/upstream-smoke.json b/docs/validation/2026-10-22/upstream-smoke.json index 9886cbc..9b59af9 100644 --- a/docs/validation/2026-10-22/upstream-smoke.json +++ b/docs/validation/2026-10-22/upstream-smoke.json @@ -645,7 +645,9 @@ "driver": "MCP Playwright", "catalogDiscovered": true, "healthVersion": "422774f", - "crossSiteNavigationStatusObserved": 403 + "crossSiteNavigationStatusObserved": 403, + "gatewayCorrelationVerified": true, + "evidenceKind": "External MCP browser attestation, correlated with the local gateway trace" }, "shutdown": { "allOwnedProcessesExited": true, @@ -660,5 +662,6 @@ "recheckedAt": "2026-10-22T14:32:04.813Z", "sourceFix": "Use node:http to send the literal Host header. Runtime binary unchanged." } - ] + ], + "browserCorrelationRecordedAt": "2026-10-22T14:55:40.789Z" } diff --git a/tools/README.md b/tools/README.md index 6b1e347..43fb232 100644 --- a/tools/README.md +++ b/tools/README.md @@ -29,6 +29,13 @@ endpoint. This records that proof and stops the CLI children. POST `/__stop` to close the remaining fixture. Ctrl+C also stops owned children. These control endpoints exist only in this synthetic test fixture, never in the product. +The proof must identify MCP Playwright, the expected health revision, catalog, +same-origin chat model/status, fetch-site and cross-site status. Its chat must +match a `browser-same-origin` request in the independent gateway trace; otherwise +the finish endpoint returns 422 and the report cannot pass browser acceptance. +Health/catalog/browser-guard observations remain external driver attestations, +not facts that the upstream gateway can independently observe. + Reports must distinguish synthetic process acceptance from live provider or deployment evidence. Review corrections to the test driver separately and keep the original failed observation alongside any same-binary recheck. diff --git a/tools/browser-proof.mjs b/tools/browser-proof.mjs new file mode 100644 index 0000000..7ebed0c --- /dev/null +++ b/tools/browser-proof.mjs @@ -0,0 +1,22 @@ +import assert from 'node:assert/strict'; + +// Browser observations are supplied by the external MCP driver. Correlate its +// chat with the independent gateway trace before accepting that attestation. +export function validateBrowserProof(proof, evidence) { + assert.equal(proof.driver, 'MCP Playwright'); + assert.equal(proof.sameOriginHealthStatus, 'ok'); + assert.equal(proof.healthVersion, evidence.revision.slice(0, 7)); + assert.equal(proof.catalogDiscovered, true); + assert.equal(proof.chatStatus, 200); + assert.equal(proof.chatModel, 'fixture/reasoner'); + assert.equal(proof.requestFetchSite, 'same-origin'); + assert.equal(proof.crossSiteNavigationStatusObserved, 403); + assert(evidence.trace.some(r => r.gateway === 'stable' && r.tag === 'browser-same-origin' && r.model === proof.chatModel && r.status === proof.chatStatus), 'No matching browser chat reached the gateway'); + return { ...proof, gatewayCorrelationVerified: true, + evidenceKind: 'External MCP browser attestation, correlated with the local gateway trace' }; +} + +export function browserProofPassed(proof, evidence) { + if (proof?.gatewayCorrelationVerified !== true) return false; + try { validateBrowserProof(proof, evidence); return true; } catch { return false; } +} diff --git a/tools/smoke-cli.mjs b/tools/smoke-cli.mjs index c8f2357..7394a45 100644 --- a/tools/smoke-cli.mjs +++ b/tools/smoke-cli.mjs @@ -1,4 +1,5 @@ import { renderReport } from './smoke-report.mjs'; +import { validateBrowserProof } from './browser-proof.mjs'; // Process acceptance against synthetic localhost services, with isolated state. import assert from 'node:assert/strict'; import { createHash, randomUUID } from 'node:crypto'; @@ -71,7 +72,7 @@ server=http.createServer(async(r,w)=>{ const url=new URL(r.url,'http://127.0.0.1'); if(url.pathname==='/'||url.pathname==='/report'){w.setHeader('Content-Type','text/html; charset=utf-8');w.end(report());return;} if(url.pathname==='/evidence.json'||url.pathname==='/upstream-smoke.json'){w.setHeader('Content-Type','application/json');w.end(JSON.stringify(evidence));return;} - if(url.pathname==='/__finish'&&r.method==='POST'){if(!evidence.apiFinishedAt){w.writeHead(409);w.end('API checks still running');return;}let body='';for await(const chunk of r)body+=chunk;evidence.browser=JSON.parse(body);await stopChildren();evidence.finishedAt=new Date().toISOString();await save();w.end('ok');return;} + if(url.pathname==='/__finish'&&r.method==='POST'){if(!evidence.apiFinishedAt){w.writeHead(409);w.end('API checks still running');return;}let body='';for await(const chunk of r)body+=chunk;try{evidence.browser=validateBrowserProof(JSON.parse(body),evidence);}catch(error){w.writeHead(422);w.end(error.message);return;}await stopChildren();evidence.finishedAt=new Date().toISOString();await save();w.end('ok');return;} if(url.pathname==='/__stop'&&r.method==='POST'){await stopChildren();w.end('ok');server.close();return;} const gateway=url.pathname.split('/')[1]; if(!Object.hasOwn(modes,gateway)){w.writeHead(404);w.end();return;} @@ -131,7 +132,7 @@ await check('expensive-admission','Giá cao bị chặn trước khi gửi',asyn await check('zero-flat','Giá cố định bằng 0 không vượt spend limit',async()=>{const res=await chat(stable.base,'zero-flat','fixture/zero');assert.equal(res.status,429);assert.equal(traceFor('zero-flat').length,0);return{status:res.status,upstreamSends:0};}); await check('local-trust','Host và Origin không tin cậy bị từ chối',async()=>{const statuses=[],before=evidence.trace.length;for(const headers of[{Origin:'https://attacker.invalid'},{Origin:'null'},{Host:'attacker.invalid'},{'Sec-Fetch-Site':'cross-site'}]){const res=await rawRequest(stable.base,'/v1/chat/completions',{},headers);assert.equal(res.status,403);statuses.push(res.status);}assert.equal(evidence.trace.length,before);return{statuses,upstreamSends:0};}); await check('cjk','Prompt CJK dài được nâng tier theo số token',async()=>{const res=await jsonRequest(stable.base,'/debug',{prompt:'中'.repeat(110000),max_tokens:16});assert.equal(res.status,200);assert.equal(res.body.decision.Tier,'COMPLEX');assert(res.body.decision.Reasoning.includes('Input exceeds'));return{tier:res.body.decision.Tier,reason:res.body.decision.Reasoning};}); -await check('concurrency','Reservation đồng thời và settlement trả lại ngân sách',async()=>{const held=new Promise(resolve=>{markHold=resolve;});const first=chat(concurrency.base,'concurrency-first','fixture/reasoner',{max_tokens:10000});await Promise.race([held,sleep(10000).then(()=>{throw Error('first request never arrived');})]);const second=await chat(concurrency.base,'concurrency-second','fixture/reasoner',{max_tokens:10000});assert.equal(second.status,429);assert.equal(traceFor('concurrency-second').length,0);releaseHold();assert.equal((await first).status,200);const third=await chat(concurrency.base,'concurrency-third','fixture/reasoner',{max_tokens:10000});assert.equal(third.status,200);return{first:200,whileInFlight:429,afterSettlement:200};}); +await check('concurrency','Reservation đồng thời và settlement trả lại ngân sách',async()=>{const held=new Promise(resolve=>{markHold=resolve;});const first=chat(concurrency.base,'concurrency-first','fixture/reasoner',{max_tokens:10000});first.catch(()=>{});let released=false;const release=()=>{if(!released){released=true;releaseHold?.();}};try{await Promise.race([held,sleep(10000).then(()=>{throw Error('first request never arrived');})]);const second=await chat(concurrency.base,'concurrency-second','fixture/reasoner',{max_tokens:10000});assert.equal(second.status,429);assert.equal(traceFor('concurrency-second').length,0);release();assert.equal((await first).status,200);const third=await chat(concurrency.base,'concurrency-third','fixture/reasoner',{max_tokens:10000});assert.equal(third.status,200);return{first:200,whileInFlight:429,afterSettlement:200};}finally{release();await first.catch(()=>{});}}); const stableTick=new Promise(resolve=>tickWaiters.set('stable:2',resolve)); const recoveryTick=new Promise(resolve=>tickWaiters.set('recovery:2',resolve)); modes.stable='invalid';modes.recovery='valid'; diff --git a/tools/smoke-report.mjs b/tools/smoke-report.mjs index dcb1cf2..0510c5a 100644 --- a/tools/smoke-report.mjs +++ b/tools/smoke-report.mjs @@ -1,7 +1,8 @@ +import { browserProofPassed } from './browser-proof.mjs'; const escape = v => String(v).replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c])); export function renderReport(evidence) { const passed = evidence.checks.filter(c => c.pass).length; - const browserPassed = evidence.browser?.sameOriginHealthStatus === 'ok' && evidence.browser?.chatStatus === 200 && evidence.browser?.catalogDiscovered === true && evidence.browser?.crossSiteNavigationStatusObserved === 403; + const browserPassed = browserProofPassed(evidence.browser, evidence); const shutdownPassed = evidence.shutdown?.allOwnedProcessesExited === true && evidence.shutdown?.portsClosed === true; return `DOSRouter - Xác minh runtime