@danceiny/gotry 0.0.1-rc.18 → 0.0.1-rc.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -16
- package/README.zh-CN.md +37 -15
- package/bin/gotry-bootstrap.js +309 -30
- package/bin/gotry-inner.js +74 -23
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +7 -8
- package/cordis.gotry-patch.yml +22 -8
- package/data/stations-12306-verify.json +524 -0
- package/dist/capabilities/agent-reach.js +4 -4
- package/dist/capabilities/anything.js +62 -29
- package/dist/capabilities/channel-health.js +103 -0
- package/dist/capabilities/channel-registry.js +213 -0
- package/dist/capabilities/doctor.js +307 -0
- package/dist/capabilities/effect.js +178 -5
- package/dist/capabilities/flyai.js +13 -0
- package/dist/capabilities/hbcli.js +28 -2
- package/dist/capabilities/session/adapters/ctrip-hotel.js +207 -0
- package/dist/capabilities/session/adapters/rail-12306.js +285 -0
- package/dist/capabilities/session/extension-bridge.js +14 -1
- package/dist/capabilities/session/extension-channel.js +4 -2
- package/dist/capabilities/session/extension-distribution.js +1 -1
- package/dist/capabilities/session-search.js +311 -0
- package/dist/capabilities/visa-policy.js +126 -0
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +305 -0
- package/dist/scripts/agent-planning-turn-deadline-tests.js +281 -0
- package/dist/scripts/anything-tests.js +70 -21
- package/dist/scripts/benchmark-environment-bridge-e2e.js +139 -37
- package/dist/scripts/benchmark-environment-bridge-tests.js +1099 -306
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +26 -26
- package/dist/scripts/{booking-copilot-availability-policy-v2-tests.js → booking-copilot-availability-policy-tests.js} +115 -115
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +41 -1
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +75 -120
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +370 -61
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +2 -2
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +7 -3
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +15 -9
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +19 -11
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +16 -52
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +3953 -155
- package/dist/scripts/booking-copilot-server-proof-tests.js +58 -23
- package/dist/scripts/booking-copilot-startup-proof-tests.js +10 -119
- package/dist/scripts/booking-surface-contract-proof-tests.js +1154 -326
- package/dist/scripts/bootstrap-tests.js +113 -2
- package/dist/scripts/build-metrics-report.js +339 -0
- package/dist/scripts/channel-probe-tests.js +148 -0
- package/dist/scripts/channel-probe.js +252 -0
- package/dist/scripts/channel-registry-tests.js +262 -0
- package/dist/scripts/doctor-tests.js +175 -0
- package/dist/scripts/effect-tests.js +274 -1
- package/dist/scripts/extension-tests.js +129 -11
- package/dist/scripts/fact-gate-tests.js +91 -2
- package/dist/scripts/flyai-tests.js +17 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -3
- package/dist/scripts/hbcli-tests.js +38 -1
- package/dist/scripts/metrics-report-tests.js +288 -0
- package/dist/scripts/persona-surface-guard-tests.js +19 -0
- package/dist/scripts/session-tests.js +401 -1
- package/dist/scripts/smoke.js +100 -83
- package/dist/scripts/turn-handoff-collect-tests.js +216 -0
- package/dist/scripts/turn-handoff-collect.js +189 -0
- package/dist/scripts/turn-policy-tests.js +56 -0
- package/dist/scripts/typed-contract-canary.js +261 -0
- package/dist/scripts/visa-policy-tests.js +75 -0
- package/dist/scripts/wish-channel-gate-tests.js +111 -0
- package/dist/src/artifact-gate.js +91 -3
- package/dist/src/benchmark-agent-conformance.js +39 -16
- package/dist/src/benchmark-environment-bridge.js +316 -52
- package/dist/src/bookable-facts.js +62 -3
- package/dist/src/booking-surface/{availability-policy-v2.js → availability-policy.js} +89 -89
- package/dist/src/booking-surface/canonical-schema.js +8 -22
- package/dist/src/booking-surface/contracts.js +70 -19
- package/dist/src/booking-surface/dsh-planner.js +310 -177
- package/dist/src/booking-surface/dsh-plugin.js +3 -3
- package/dist/src/booking-surface/index.js +1 -5
- package/dist/src/booking-surface/profile.js +1 -1
- package/dist/src/booking-surface/runtime.js +1682 -247
- package/dist/src/booking-surface/server.js +371 -185
- package/dist/src/booking-surface/startup.js +17 -27
- package/dist/src/booking-surface/validation.js +270 -760
- package/dist/src/index.js +739 -150
- package/dist/src/turn-deadline.js +292 -0
- package/dist/src/turn-policy.js +146 -0
- package/dist/src/wish-pool.js +5 -0
- package/extension/background.js +23 -4
- package/extension/content-main.js +47 -9
- package/extension/manifest.json +27 -9
- package/package.json +7 -26
- package/schemas/booking.surface.schema.json +2593 -0
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +89 -38
- package/ts/capabilities/flyai.ts +21 -3
- package/ts/capabilities/hbcli.ts +53 -4
- package/ts/capabilities/session/action-cache.ts +1 -1
- package/ts/capabilities/session/adapters/ctrip-hotel.ts +238 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +293 -0
- package/ts/capabilities/session/extension-bridge.ts +32 -2
- package/ts/capabilities/session/extension-channel.ts +9 -8
- package/ts/capabilities/session/extension-distribution.ts +3 -1
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/read-guard.ts +1 -1
- package/ts/capabilities/session/wizard.ts +1 -1
- package/ts/capabilities/session-search.ts +323 -1
- package/ts/dsh-runtime/vendor/README.md +60 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/LICENSE +21 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +200 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +202 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/client/client.js +216 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/cordis.patch.yml +5 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/amap.js +371 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/nominatim.js +63 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/osrm.js +56 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/clients/photon.js +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-file.js +87 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config-route.js +126 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/config.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/index.js +109 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +27 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/geocode.js +127 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/poi.js +84 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/tools/routes.js +170 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/amap.d.ts +53 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/nominatim.d.ts +17 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/osrm.d.ts +15 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/clients/photon.d.ts +22 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-file.d.ts +33 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config-route.d.ts +20 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/config.d.ts +23 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/index.d.ts +18 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +14 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/geocode.d.ts +11 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/poi.d.ts +9 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/tools/routes.d.ts +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/types.d.ts +55 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types.js +16 -0
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +91 -0
- package/ts/package.json +9 -0
- package/ts/scripts/turn-handoff-collect.ts +178 -0
- package/ts/src/artifact-gate.ts +82 -2
- package/ts/src/benchmark-agent-conformance.ts +54 -15
- package/ts/src/benchmark-environment-bridge.ts +234 -46
- package/ts/src/bookable-facts.ts +109 -5
- package/ts/src/booking-saga.ts +1 -1
- package/ts/src/booking-surface/{availability-policy-v2.ts → availability-policy.ts} +124 -125
- package/ts/src/booking-surface/canonical-schema.js +8 -22
- package/ts/src/booking-surface/contracts.ts +278 -239
- package/ts/src/booking-surface/dsh-planner.ts +333 -194
- package/ts/src/booking-surface/dsh-plugin.js +3 -3
- package/ts/src/booking-surface/index.ts +1 -5
- package/ts/src/booking-surface/profile.ts +11 -11
- package/ts/src/booking-surface/runtime.ts +1351 -351
- package/ts/src/booking-surface/server.ts +282 -175
- package/ts/src/booking-surface/startup.ts +36 -48
- package/ts/src/booking-surface/validation.ts +194 -442
- package/ts/src/contracts.ts +1 -1
- package/ts/src/index.ts +492 -172
- package/ts/src/loop.ts +1 -1
- package/ts/src/state-ledger.ts +1 -1
- package/ts/src/turn-deadline.ts +365 -0
- package/ts/src/turn-policy.ts +154 -0
- package/ts/src/wish-pool.ts +17 -4
- package/dist/scripts/agent-planning-budget-e2e.js +0 -227
- package/dist/scripts/agent-planning-budget-tests.js +0 -173
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +0 -3935
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +0 -1174
- package/dist/src/booking-surface/contracts-v2.js +0 -89
- package/dist/src/booking-surface/runtime-v2.js +0 -1771
- package/dist/src/booking-surface/server-v2.js +0 -334
- package/dist/src/booking-surface/validation-v2.js +0 -319
- package/dist/src/tool-budget.js +0 -136
- package/schemas/booking.surface.v1.schema.json +0 -927
- package/schemas/booking.surface.v2.schema.json +0 -61
- package/ts/src/booking-surface/contracts-v2.ts +0 -118
- package/ts/src/booking-surface/runtime-v2.ts +0 -1466
- package/ts/src/booking-surface/server-v2.ts +0 -247
- package/ts/src/booking-surface/validation-v2.ts +0 -205
- package/ts/src/tool-budget.ts +0 -165
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
3
5
|
import { join } from 'node:path';
|
|
4
6
|
const repoRoot = join(import.meta.dirname, '..', '..');
|
|
5
7
|
const bootstrap = join(repoRoot, 'bin', 'gotry-bootstrap.js');
|
|
@@ -102,7 +104,116 @@ const c7b = runBootstrap([
|
|
|
102
104
|
assert.equal(c7b.code, 0, `非法 --extension-from 值应回落 bundled 且 exit 0,实际 ${c7b.code}\n${c7b.out}`);
|
|
103
105
|
assert.ok(!c7b.out.includes('下载通道'), '非法值不应进入 github 通道');
|
|
104
106
|
console.log('7. 扩展分发 github 通道(不可达基址即时降级 + 非法值回落 bundled)OK');
|
|
105
|
-
|
|
107
|
+
const c8 = runBootstrap([
|
|
108
|
+
'doctor'
|
|
109
|
+
], {});
|
|
110
|
+
assert.ok(c8.out.includes('[gotry-doctor]'), '应输出 [gotry-doctor] 标签');
|
|
111
|
+
assert.ok(c8.out.includes('Agent Reach'), '体检应含 agent-reach 项');
|
|
112
|
+
assert.ok(c8.out.includes('hbcli'), '体检应含 hbcli 项');
|
|
113
|
+
assert.ok(c8.out.includes('FLYAI_API_KEY'), '体检应含 flyai key 项(试用额度降级面)');
|
|
114
|
+
assert.ok(c8.out.includes('LLM key'), '体检应含 LLM key 让渡说明(doctor 不管 key)');
|
|
115
|
+
assert.ok(c8.out.includes('dsh-map-tools'), '体检应含 dsh-map-tools 项(#139;与 ts/capabilities/doctor.ts 两面成对)');
|
|
116
|
+
assert.ok(c8.out.includes('dsh-tool-ask-user'), '体检应含 dsh-tool-ask-user 项(与 ts/capabilities/doctor.ts 两面成对)');
|
|
117
|
+
assert.ok(c8.out.includes('doctor-report.md'), '应提示报告落盘路径');
|
|
118
|
+
assert.ok([
|
|
119
|
+
0,
|
|
120
|
+
1
|
|
121
|
+
].includes(c8.code), `doctor exit 应为 0(就绪)或 1(有缺失),实际 ${c8.code}`);
|
|
122
|
+
const reportPath = join(repoRoot, 'gotry-state', 'doctor-report.md');
|
|
123
|
+
const report = readFileSync(reportPath, 'utf-8');
|
|
124
|
+
assert.ok(report.includes('# GoTry 依赖体检报告'), '报告 markdown 应落盘可预览');
|
|
125
|
+
assert.ok(report.includes('npx gotry doctor --fix'), '报告应带补装指引');
|
|
126
|
+
console.log('8. doctor 子命令(体检清单 + LLM key 让渡 + 报告落盘)OK');
|
|
127
|
+
const calHome = mkdtempSync(join(tmpdir(), 'gotry-cal-test-'));
|
|
128
|
+
const calEnv = {
|
|
129
|
+
HOME: calHome
|
|
130
|
+
};
|
|
131
|
+
const c9a = runBootstrap([
|
|
132
|
+
'calendar',
|
|
133
|
+
'--status'
|
|
134
|
+
], calEnv);
|
|
135
|
+
assert.equal(c9a.code, 0, `calendar --status(默认态)应 exit 0\n${c9a.out}`);
|
|
136
|
+
assert.ok(c9a.out.includes('默认未挂载'), '默认态=未挂载');
|
|
137
|
+
assert.ok(c9a.out.includes(join(calHome, '.gotry', 'calendar.json')), '状态文件路径可见');
|
|
138
|
+
const c9b = runBootstrap([
|
|
139
|
+
'calendar'
|
|
140
|
+
], calEnv);
|
|
141
|
+
assert.equal(c9b.code, 0, `calendar(开启)应 exit 0\n${c9b.out}`);
|
|
142
|
+
assert.ok(c9b.out.includes('已开启挂载'), '开启态输出');
|
|
143
|
+
assert.ok(c9b.out.includes('cordis.patch.yml'), '未配置时给 profile 配置指引');
|
|
144
|
+
const c9c = runBootstrap([
|
|
145
|
+
'calendar',
|
|
146
|
+
'--status'
|
|
147
|
+
], calEnv);
|
|
148
|
+
assert.ok(c9c.out.includes('已挂载'), '开启后 --status 显示已挂载');
|
|
149
|
+
const c9d = runBootstrap([
|
|
150
|
+
'doctor'
|
|
151
|
+
], {
|
|
152
|
+
...calEnv,
|
|
153
|
+
GOTRY_SETUP_SKIP: '1'
|
|
154
|
+
});
|
|
155
|
+
assert.ok(c9d.out.includes('dsh-calendar'), 'doctor 清单含 calendar 项(setup 状态面)');
|
|
156
|
+
assert.ok(c9d.out.includes('已挂载但 calendar 未配置'), '开启未配置=doctor 可见');
|
|
157
|
+
const c9e = runBootstrap([
|
|
158
|
+
'calendar',
|
|
159
|
+
'--off'
|
|
160
|
+
], calEnv);
|
|
161
|
+
assert.equal(c9e.code, 0, `calendar --off 应 exit 0\n${c9e.out}`);
|
|
162
|
+
assert.ok(c9e.out.includes('恢复默认不挂载'), '关闭态输出');
|
|
163
|
+
const c9f = runBootstrap([
|
|
164
|
+
'calendar',
|
|
165
|
+
'--status'
|
|
166
|
+
], calEnv);
|
|
167
|
+
assert.ok(c9f.out.includes('默认未挂载'), '关闭后回到默认态');
|
|
168
|
+
console.log('9. calendar 子命令(setup 状态面 on/off/status + doctor 三态,HOME 隔离)OK');
|
|
169
|
+
{
|
|
170
|
+
const sumHome = mkdtempSync(join(tmpdir(), 'gotry-sum-test-'));
|
|
171
|
+
const r = spawnSync('node', [
|
|
172
|
+
bootstrap,
|
|
173
|
+
'doctor',
|
|
174
|
+
'--summary'
|
|
175
|
+
], {
|
|
176
|
+
encoding: 'utf-8',
|
|
177
|
+
timeout: 60_000,
|
|
178
|
+
env: {
|
|
179
|
+
...process.env,
|
|
180
|
+
HOME: sumHome
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
assert.equal(r.status, 0, `doctor --summary 应恒 exit 0(不挡启动),实际 ${r.status}\n${r.stderr}`);
|
|
184
|
+
assert.ok(!r.stdout.includes('报告已写'), 'summary 模式零写盘(不落 doctor-report.md)');
|
|
185
|
+
assert.ok(!r.stdout.includes('[gotry-doctor]'), 'summary 模式零 header(stdout 静默)');
|
|
186
|
+
assert.match(r.stderr, /\[gotry\] doctor: \d+ 项待处理/, '隔离 HOME 有缺失项 → 一行摘要进 stderr');
|
|
187
|
+
assert.match(r.stderr, /gotry_doctor/, '摘要带对话内指路');
|
|
188
|
+
assert.match(r.stderr, /扩展=缺/, '缺失项人话=缺(降级类=半可用)');
|
|
189
|
+
}console.log('10. 启动一次性 doctor 摘要(--summary:stderr 一行/零写盘/恒 exit 0)OK');
|
|
190
|
+
{
|
|
191
|
+
const sbHome = mkdtempSync(join(tmpdir(), 'gotry-sidebar-test-'));
|
|
192
|
+
const prevHome = process.env.HOME;
|
|
193
|
+
process.env.HOME = sbHome;
|
|
194
|
+
try {
|
|
195
|
+
const { setupSidebar } = await import(bootstrap);
|
|
196
|
+
const r = await setupSidebar(async ()=>{
|
|
197
|
+
const sbPkgDir = join(sbHome, '.dsh/profiles/web/node_modules/dsh-better-sidebar');
|
|
198
|
+
mkdirSync(sbPkgDir, {
|
|
199
|
+
recursive: true
|
|
200
|
+
});
|
|
201
|
+
writeFileSync(join(sbPkgDir, 'package.json'), JSON.stringify({
|
|
202
|
+
name: 'dsh-better-sidebar',
|
|
203
|
+
version: '0.18.0'
|
|
204
|
+
}));
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
error: 'exit 1'
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
assert.equal(r.ok, true, '安装器 exit 非 0 但状态已落盘 → 按落盘判成功(不误报失败)');
|
|
211
|
+
} finally{
|
|
212
|
+
if (prevHome === undefined) delete process.env.HOME;
|
|
213
|
+
else process.env.HOME = prevHome;
|
|
214
|
+
}
|
|
215
|
+
}console.log('11. setupSidebar 落盘状态复核(pnpm 忽略构建脚本 exit 1 不再误报)OK');
|
|
216
|
+
console.log('BOOTSTRAP TESTS: 11/11 OK(扩展就位 + 跳过开关 / wizard --dry-run / wizard 真实 / 扩展分发通道 / doctor 体检面 / calendar setup 状态面 / 显式跳过 + auto 跳过 + 单项跳过 / 启动摘要 / sidebar 落盘状态复核)');
|
|
106
217
|
|
|
107
218
|
|
|
108
219
|
//# sourceURL=ts/scripts/bootstrap-tests.ts
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
const BRIDGE_LATENCY_BUDGET_MS = 500;
|
|
4
|
+
const CHANNEL_WINDOW_DAYS = 30;
|
|
5
|
+
function stateFile(stateRoot, name) {
|
|
6
|
+
const root = stateRoot === '.' ? process.cwd() : stateRoot;
|
|
7
|
+
return join(root, 'gotry-state', name);
|
|
8
|
+
}
|
|
9
|
+
function readJsonl(path) {
|
|
10
|
+
if (!existsSync(path)) return [];
|
|
11
|
+
try {
|
|
12
|
+
const rows = [];
|
|
13
|
+
for (const line of readFileSync(path, 'utf-8').split('\n')){
|
|
14
|
+
const t = line.trim();
|
|
15
|
+
if (!t) continue;
|
|
16
|
+
try {
|
|
17
|
+
rows.push(JSON.parse(t));
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
return rows;
|
|
21
|
+
} catch {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function bounded(value, max = 120) {
|
|
26
|
+
if (!value) return undefined;
|
|
27
|
+
return value.length <= max ? value : `${value.slice(0, max - 1)}…`;
|
|
28
|
+
}
|
|
29
|
+
const BLOCKED_VERDICTS = new Set([
|
|
30
|
+
'error',
|
|
31
|
+
'needs-setup'
|
|
32
|
+
]);
|
|
33
|
+
export function aggregateFacts(rows) {
|
|
34
|
+
const latest = new Map();
|
|
35
|
+
let lines = 0;
|
|
36
|
+
for (const raw of rows){
|
|
37
|
+
const row = typeof raw === 'object' && raw !== null ? raw : {};
|
|
38
|
+
lines += 1;
|
|
39
|
+
const id = typeof row.fact_id === 'string' ? row.fact_id : '';
|
|
40
|
+
if (!id) continue;
|
|
41
|
+
latest.set(id, row);
|
|
42
|
+
}
|
|
43
|
+
const byKind = {};
|
|
44
|
+
for (const row of latest.values()){
|
|
45
|
+
const kind = typeof row.kind === 'string' && row.kind ? row.kind : 'unknown';
|
|
46
|
+
const verdict = typeof row.verdict === 'string' && row.verdict ? row.verdict : 'n/a';
|
|
47
|
+
const slot = byKind[kind] ??= {
|
|
48
|
+
total: 0,
|
|
49
|
+
verdicts: {},
|
|
50
|
+
blocked: 0
|
|
51
|
+
};
|
|
52
|
+
slot.total += 1;
|
|
53
|
+
slot.verdicts[verdict] = (slot.verdicts[verdict] ?? 0) + 1;
|
|
54
|
+
if (BLOCKED_VERDICTS.has(verdict)) slot.blocked += 1;
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
lines,
|
|
58
|
+
deduped: latest.size,
|
|
59
|
+
byKind
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function aggregateChannels(rows, opts = {}) {
|
|
63
|
+
const nowMs = (opts.now ?? new Date()).getTime();
|
|
64
|
+
const cutoff = nowMs - (opts.windowDays ?? CHANNEL_WINDOW_DAYS) * 86_400_000;
|
|
65
|
+
const counts = new Map();
|
|
66
|
+
const newest = new Map();
|
|
67
|
+
const lastState = new Map();
|
|
68
|
+
for (const raw of rows){
|
|
69
|
+
const row = typeof raw === 'object' && raw !== null ? raw : {};
|
|
70
|
+
if (typeof row.channel !== 'string' || !row.channel) continue;
|
|
71
|
+
if (typeof row.state === 'string' && [
|
|
72
|
+
'down',
|
|
73
|
+
'cooldown',
|
|
74
|
+
'ok'
|
|
75
|
+
].includes(row.state)) {
|
|
76
|
+
lastState.set(row.channel, row.state);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const raw of rows){
|
|
80
|
+
const row = typeof raw === 'object' && raw !== null ? raw : {};
|
|
81
|
+
if (typeof row.channel !== 'string' || !row.channel) continue;
|
|
82
|
+
if (row.state !== 'down' && row.state !== 'cooldown') continue;
|
|
83
|
+
if (lastState.get(row.channel) === 'ok') continue;
|
|
84
|
+
const at = typeof row.at === 'string' ? row.at : '';
|
|
85
|
+
const atMs = Date.parse(at);
|
|
86
|
+
if (Number.isFinite(atMs) && atMs < cutoff) continue;
|
|
87
|
+
counts.set(row.channel, (counts.get(row.channel) ?? 0) + 1);
|
|
88
|
+
const atMsSafe = Number.isFinite(atMs) ? atMs : 0;
|
|
89
|
+
const prev = newest.get(row.channel);
|
|
90
|
+
if (prev && prev.atMs >= atMsSafe) continue;
|
|
91
|
+
newest.set(row.channel, {
|
|
92
|
+
channel: row.channel,
|
|
93
|
+
state: row.state,
|
|
94
|
+
reason: typeof row.reason === 'string' ? row.reason : undefined,
|
|
95
|
+
at: at || undefined,
|
|
96
|
+
events: 0,
|
|
97
|
+
atMs: atMsSafe
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const latest = [
|
|
101
|
+
...newest.values()
|
|
102
|
+
].map(({ atMs: _atMs, ...row })=>({
|
|
103
|
+
...row,
|
|
104
|
+
events: counts.get(row.channel) ?? 0
|
|
105
|
+
}));
|
|
106
|
+
return {
|
|
107
|
+
windowDays: opts.windowDays ?? CHANNEL_WINDOW_DAYS,
|
|
108
|
+
latest
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export function aggregateIncidents(rows, opts = {}) {
|
|
112
|
+
const nowMs = (opts.now ?? new Date()).getTime();
|
|
113
|
+
const windowDays = opts.windowDays ?? 7;
|
|
114
|
+
const cutoff = nowMs - windowDays * 86_400_000;
|
|
115
|
+
const byKind = {};
|
|
116
|
+
const sources = new Map();
|
|
117
|
+
let total = 0;
|
|
118
|
+
for (const raw of rows){
|
|
119
|
+
const row = typeof raw === 'object' && raw !== null ? raw : {};
|
|
120
|
+
const ts = typeof row.ts === 'string' ? row.ts : '';
|
|
121
|
+
const atMs = Date.parse(ts);
|
|
122
|
+
if (Number.isFinite(atMs) && atMs < cutoff) continue;
|
|
123
|
+
const kind = typeof row.kind === 'string' && row.kind ? row.kind : 'unknown';
|
|
124
|
+
const slot = byKind[kind] ??= {
|
|
125
|
+
count: 0
|
|
126
|
+
};
|
|
127
|
+
slot.count += 1;
|
|
128
|
+
if (!slot.lastTs || Number.isFinite(atMs) && atMs > Date.parse(slot.lastTs)) {
|
|
129
|
+
slot.lastTs = ts || slot.lastTs;
|
|
130
|
+
slot.lastMessage = bounded(typeof row.message === 'string' ? row.message : undefined);
|
|
131
|
+
}
|
|
132
|
+
const source = typeof row.source === 'string' && row.source ? row.source : '(unknown)';
|
|
133
|
+
sources.set(source, (sources.get(source) ?? 0) + 1);
|
|
134
|
+
total += 1;
|
|
135
|
+
}
|
|
136
|
+
const topSources = [
|
|
137
|
+
...sources.entries()
|
|
138
|
+
].sort((a, b)=>b[1] - a[1]).slice(0, 5).map(([source, count])=>({
|
|
139
|
+
source,
|
|
140
|
+
count
|
|
141
|
+
}));
|
|
142
|
+
return {
|
|
143
|
+
windowDays,
|
|
144
|
+
totalInWindow: total,
|
|
145
|
+
byKind,
|
|
146
|
+
topSources
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
export function aggregateLatency(rows, opts = {}) {
|
|
150
|
+
const nowMs = (opts.now ?? new Date()).getTime();
|
|
151
|
+
const cutoff = nowMs - (opts.windowDays ?? 7) * 86_400_000;
|
|
152
|
+
const values = [];
|
|
153
|
+
const kinds = new Map();
|
|
154
|
+
for (const raw of rows){
|
|
155
|
+
const row = typeof raw === 'object' && raw !== null ? raw : {};
|
|
156
|
+
const ts = typeof row.ts === 'string' ? row.ts : '';
|
|
157
|
+
const atMs = Date.parse(ts);
|
|
158
|
+
if (Number.isFinite(atMs) && atMs < cutoff) continue;
|
|
159
|
+
const ms = typeof row.latencyMs === 'number' ? row.latencyMs : Number.NaN;
|
|
160
|
+
if (!Number.isFinite(ms)) continue;
|
|
161
|
+
values.push(ms);
|
|
162
|
+
const kind = typeof row.kind === 'string' && row.kind ? row.kind : 'unknown';
|
|
163
|
+
kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
|
|
164
|
+
}
|
|
165
|
+
const sorted = [
|
|
166
|
+
...values
|
|
167
|
+
].sort((a, b)=>a - b);
|
|
168
|
+
const pick = (q)=>sorted.length === 0 ? 0 : sorted[Math.min(sorted.length - 1, Math.ceil(q * sorted.length) - 1)];
|
|
169
|
+
return {
|
|
170
|
+
count: sorted.length,
|
|
171
|
+
p50: pick(0.5),
|
|
172
|
+
p95: pick(0.95),
|
|
173
|
+
max: sorted.length === 0 ? 0 : sorted[sorted.length - 1],
|
|
174
|
+
overBudget: sorted.filter((ms)=>ms > BRIDGE_LATENCY_BUDGET_MS).length,
|
|
175
|
+
budgetMs: BRIDGE_LATENCY_BUDGET_MS,
|
|
176
|
+
topKinds: [
|
|
177
|
+
...kinds.entries()
|
|
178
|
+
].sort((a, b)=>b[1] - a[1]).slice(0, 5).map(([kind, count])=>({
|
|
179
|
+
kind,
|
|
180
|
+
count
|
|
181
|
+
}))
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function presenceOf(path) {
|
|
185
|
+
try {
|
|
186
|
+
if (!existsSync(path)) return {
|
|
187
|
+
exists: false
|
|
188
|
+
};
|
|
189
|
+
const st = statSync(path);
|
|
190
|
+
return {
|
|
191
|
+
exists: true,
|
|
192
|
+
bytes: st.size,
|
|
193
|
+
mtime: st.mtime.toISOString()
|
|
194
|
+
};
|
|
195
|
+
} catch {
|
|
196
|
+
return {
|
|
197
|
+
exists: false
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export async function collectMetrics(stateRoot, opts = {}) {
|
|
202
|
+
const root = resolve(stateRoot === '.' ? process.cwd() : stateRoot);
|
|
203
|
+
return {
|
|
204
|
+
generatedAt: (opts.now ?? new Date()).toISOString(),
|
|
205
|
+
stateRoot: root,
|
|
206
|
+
facts: aggregateFacts(readJsonl(stateFile(stateRoot, 'bookable-facts.jsonl'))),
|
|
207
|
+
channels: aggregateChannels(readJsonl(stateFile(stateRoot, 'channel-health.jsonl')), {
|
|
208
|
+
now: opts.now,
|
|
209
|
+
windowDays: CHANNEL_WINDOW_DAYS
|
|
210
|
+
}),
|
|
211
|
+
incidents: aggregateIncidents(readJsonl(stateFile(stateRoot, 'incidents.jsonl')), {
|
|
212
|
+
now: opts.now,
|
|
213
|
+
windowDays: opts.days ?? 7
|
|
214
|
+
}),
|
|
215
|
+
latency: aggregateLatency(readJsonl(stateFile(stateRoot, 'bridge-latency.jsonl')), {
|
|
216
|
+
now: opts.now,
|
|
217
|
+
windowDays: opts.days ?? 7
|
|
218
|
+
}),
|
|
219
|
+
ledger: presenceOf(stateFile(stateRoot, 'gotry-state.db')),
|
|
220
|
+
doctorReport: presenceOf(stateFile(stateRoot, 'doctor-report.md'))
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function fmtRate(blocked, total) {
|
|
224
|
+
return total === 0 ? '—' : `${blocked}/${total}(${(blocked / total * 100).toFixed(0)}%)`;
|
|
225
|
+
}
|
|
226
|
+
function fmtBytes(bytes) {
|
|
227
|
+
if (bytes === undefined) return '—';
|
|
228
|
+
return bytes >= 1_048_576 ? `${(bytes / 1_048_576).toFixed(1)}MB` : `${bytes}B`;
|
|
229
|
+
}
|
|
230
|
+
export function renderMetricsReport(s) {
|
|
231
|
+
const L = [];
|
|
232
|
+
L.push(`# GoTry 质量指标报告(只读聚合,#138 第一切片)`);
|
|
233
|
+
L.push('');
|
|
234
|
+
L.push(`- 生成时间:${s.generatedAt}`);
|
|
235
|
+
L.push(`- stateRoot:\`${s.stateRoot}\`(全程只读,零 SQLite 打开)`);
|
|
236
|
+
L.push(`- 口径:工程面交付,不构成 M3 Exit 证据(Exit 证据归 issue #22)`);
|
|
237
|
+
L.push('');
|
|
238
|
+
L.push('## 事实闸 verdict 分布(bookable-facts.jsonl,同 fact_id 取最新)');
|
|
239
|
+
L.push('');
|
|
240
|
+
const kinds = Object.entries(s.facts.byKind);
|
|
241
|
+
if (kinds.length === 0) {
|
|
242
|
+
L.push('(空:无事实落盘)');
|
|
243
|
+
} else {
|
|
244
|
+
L.push('| kind | 去重后 | verdict 分布 | blocked(error+needs-setup) |');
|
|
245
|
+
L.push('|---|---|---|---|');
|
|
246
|
+
for (const [kind, st] of kinds){
|
|
247
|
+
const dist = Object.entries(st.verdicts).map(([v, n])=>`${v}:${n}`).join(' / ');
|
|
248
|
+
L.push(`| ${kind} | ${st.total} | ${dist} | ${fmtRate(st.blocked, st.total)} |`);
|
|
249
|
+
}
|
|
250
|
+
L.push('');
|
|
251
|
+
L.push(`原始行 ${s.facts.lines},去重后 ${s.facts.deduped}(重查重放幂等)。`);
|
|
252
|
+
}
|
|
253
|
+
L.push('');
|
|
254
|
+
L.push(`## 通道健康(channel-health.jsonl,${s.channels.windowDays} 天窗;hit 即清除,此处只见当前处于 down/cooldown 的通道;'ok' 恢复事件已超越)`);
|
|
255
|
+
L.push('');
|
|
256
|
+
if (s.channels.latest.length === 0) {
|
|
257
|
+
L.push('(空:窗口内无 down/cooldown 事件)');
|
|
258
|
+
} else {
|
|
259
|
+
L.push('| 通道 | 最新状态 | 原因 | 时间 | 窗口事件 |');
|
|
260
|
+
L.push('|---|---|---|---|---|');
|
|
261
|
+
for (const c of s.channels.latest){
|
|
262
|
+
L.push(`| ${c.channel} | ${c.state} | ${bounded(c.reason ?? '', 60) ?? '—'} | ${c.at ?? '—'} | ${c.events} |`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
L.push('');
|
|
266
|
+
L.push(`## 事故面(incidents.jsonl,${s.incidents.windowDays} 天窗)`);
|
|
267
|
+
L.push('');
|
|
268
|
+
const incidentKinds = Object.entries(s.incidents.byKind);
|
|
269
|
+
if (incidentKinds.length === 0) {
|
|
270
|
+
L.push('(空:窗口内无事故)');
|
|
271
|
+
} else {
|
|
272
|
+
L.push('| kind | 次数 | 最近一次 |');
|
|
273
|
+
L.push('|---|---|---|');
|
|
274
|
+
for (const [kind, st] of incidentKinds){
|
|
275
|
+
L.push(`| ${kind} | ${st.count} | ${st.lastTs ?? '—'}${st.lastMessage ? ` · ${st.lastMessage}` : ''} |`);
|
|
276
|
+
}
|
|
277
|
+
const src = s.incidents.topSources.map((t)=>`${t.source}×${t.count}`).join(', ');
|
|
278
|
+
if (src) {
|
|
279
|
+
L.push('');
|
|
280
|
+
L.push(`Top 来源:${src}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
L.push('');
|
|
284
|
+
L.push(`## 桥延迟(bridge-latency.jsonl,${s.incidents.windowDays} 天窗;预算 ${s.latency.budgetMs}ms,超限即复审锚点)`);
|
|
285
|
+
L.push('');
|
|
286
|
+
if (s.latency.count === 0) {
|
|
287
|
+
L.push('(空:窗口内无延迟记录)');
|
|
288
|
+
} else {
|
|
289
|
+
const kinds = s.latency.topKinds.map((t)=>`${t.kind}×${t.count}`).join(', ');
|
|
290
|
+
L.push(`样本 ${s.latency.count}:p50=${s.latency.p50}ms · p95=${s.latency.p95}ms · max=${s.latency.max}ms · 超预算(>${s.latency.budgetMs}ms)= **${s.latency.overBudget}**`);
|
|
291
|
+
if (kinds) L.push(`按 kind:${kinds}`);
|
|
292
|
+
}
|
|
293
|
+
L.push('');
|
|
294
|
+
L.push('## 账本与体检报告');
|
|
295
|
+
L.push('');
|
|
296
|
+
L.push(`- 账本 gotry-state.db:${s.ledger.exists ? `存在(${fmtBytes(s.ledger.bytes)},更新 ${s.ledger.mtime};明细经 \`npx gotry state\` 查看,指标面不打开 SQLite)` : '不存在(该 stateRoot 无账本)'}`);
|
|
297
|
+
L.push(`- doctor 报告 doctor-report.md:${s.doctorReport.exists ? `存在(更新 ${s.doctorReport.mtime})` : '不存在(跑 `npx gotry doctor` 生成)'}`);
|
|
298
|
+
L.push('');
|
|
299
|
+
L.push('## 评测三层入口(ADR-11;运行结果由 run-all-tests.sh / CI 承载,v1 不在此重跑)');
|
|
300
|
+
L.push('');
|
|
301
|
+
L.push('- 单元/合同/golden E2E:`./scripts/run-all-tests.sh`(全绿是任何提交前闸)');
|
|
302
|
+
L.push('- 合同层样例:`evaluation-contract-tests.ts`;golden E2E:`benchmark-environment-bridge-e2e.ts`;typed 契约 canary:`typed-contract-canary.ts`');
|
|
303
|
+
L.push('');
|
|
304
|
+
return L.join('\n');
|
|
305
|
+
}
|
|
306
|
+
export function parseArgs(argv) {
|
|
307
|
+
let stateRoot = '.';
|
|
308
|
+
let out;
|
|
309
|
+
let days = 7;
|
|
310
|
+
for(let i = 0; i < argv.length; i += 1){
|
|
311
|
+
const a = argv[i];
|
|
312
|
+
if (a === '--state-root') stateRoot = argv[++i] ?? stateRoot;
|
|
313
|
+
else if (a === '--out') out = argv[++i];
|
|
314
|
+
else if (a === '--days') days = Math.max(1, Number.parseInt(argv[++i] ?? '7', 10) || 7);
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
stateRoot: isAbsolute(stateRoot) ? stateRoot : resolve(stateRoot),
|
|
318
|
+
out,
|
|
319
|
+
days
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
export async function main(argv) {
|
|
323
|
+
const { stateRoot, out, days } = parseArgs(argv);
|
|
324
|
+
const snapshot = await collectMetrics(stateRoot, {
|
|
325
|
+
days
|
|
326
|
+
});
|
|
327
|
+
const markdown = renderMetricsReport(snapshot);
|
|
328
|
+
if (out) {
|
|
329
|
+
writeFileSync(out, markdown + '\n', 'utf-8');
|
|
330
|
+
process.stdout.write(`指标报告已写 ${out}(stateRoot 只读未动)\n`);
|
|
331
|
+
} else {
|
|
332
|
+
process.stdout.write(markdown + '\n');
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const invokedDirectly = process.argv[1] && import.meta.url === new URL(`file://${resolve(process.argv[1])}`).href;
|
|
336
|
+
if (invokedDirectly) await main(process.argv.slice(2));
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
//# sourceURL=ts/scripts/build-metrics-report.ts
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, writeFile, mkdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { evaluateProbeResults, defaultProbes, parseArgs, main } from './channel-probe.js';
|
|
6
|
+
import { readLatestChannelEvents, recordChannelEvent } from '../capabilities/channel-health.js';
|
|
7
|
+
import { aggregateChannels } from './build-metrics-report.js';
|
|
8
|
+
const tmp = await mkdtemp(join(tmpdir(), 'gotry-probe-test-'));
|
|
9
|
+
const probes = defaultProbes();
|
|
10
|
+
const hb = probes.find((p)=>p.channel === 'hbcli-hotel');
|
|
11
|
+
const om = probes.find((p)=>p.channel === 'open-meteo');
|
|
12
|
+
const fly = probes.find((p)=>p.channel === 'flyai');
|
|
13
|
+
const now = new Date('2026-09-07T12:00:00Z');
|
|
14
|
+
const evs = evaluateProbeResults([
|
|
15
|
+
{
|
|
16
|
+
probe: hb,
|
|
17
|
+
result: {
|
|
18
|
+
ok: false,
|
|
19
|
+
reason: 'auth-denied',
|
|
20
|
+
ms: 5
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
probe: om,
|
|
25
|
+
result: {
|
|
26
|
+
ok: true,
|
|
27
|
+
ms: 100
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
probe: fly,
|
|
32
|
+
result: {
|
|
33
|
+
ok: true,
|
|
34
|
+
ms: 0
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
], new Set([
|
|
38
|
+
'open-meteo'
|
|
39
|
+
]), now);
|
|
40
|
+
assert.equal(evs.length, 2, `fail→down + ok恢复→'ok' 共 2 条,实际 ${evs.length}`);
|
|
41
|
+
assert.deepEqual({
|
|
42
|
+
channel: evs[0].channel,
|
|
43
|
+
state: evs[0].state,
|
|
44
|
+
reason: evs[0].reason
|
|
45
|
+
}, {
|
|
46
|
+
channel: 'hbcli-hotel',
|
|
47
|
+
state: 'down',
|
|
48
|
+
reason: 'auth-denied'
|
|
49
|
+
}, 'fail 应产 down 事件带 reason');
|
|
50
|
+
assert.deepEqual({
|
|
51
|
+
channel: evs[1].channel,
|
|
52
|
+
state: evs[1].state
|
|
53
|
+
}, {
|
|
54
|
+
channel: 'open-meteo',
|
|
55
|
+
state: 'ok'
|
|
56
|
+
}, 'ok 且此前 down 应产 ok 恢复事件');
|
|
57
|
+
const noWrites = evaluateProbeResults([
|
|
58
|
+
{
|
|
59
|
+
probe: om,
|
|
60
|
+
result: {
|
|
61
|
+
ok: true,
|
|
62
|
+
ms: 90
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
], new Set(), now);
|
|
66
|
+
assert.equal(noWrites.length, 0, 'ok 且此前健康 → 零写放大');
|
|
67
|
+
const stateRoot = join(tmp, 'state');
|
|
68
|
+
await mkdir(join(stateRoot, 'gotry-state'), {
|
|
69
|
+
recursive: true
|
|
70
|
+
});
|
|
71
|
+
const jsonl = join(stateRoot, 'gotry-state', 'channel-health.jsonl');
|
|
72
|
+
await writeFile(jsonl, [
|
|
73
|
+
JSON.stringify({
|
|
74
|
+
channel: 'opensky',
|
|
75
|
+
state: 'down',
|
|
76
|
+
reason: 'http-503',
|
|
77
|
+
at: '2026-09-07T10:00:00.000Z'
|
|
78
|
+
}),
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
channel: 'opensky',
|
|
81
|
+
state: 'ok',
|
|
82
|
+
at: '2026-09-07T11:00:00.000Z'
|
|
83
|
+
}),
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
channel: 'hbcli-hotel',
|
|
86
|
+
state: 'down',
|
|
87
|
+
reason: 'auth-denied',
|
|
88
|
+
at: '2026-09-07T10:30:00.000Z'
|
|
89
|
+
}),
|
|
90
|
+
'not-json-garbage',
|
|
91
|
+
''
|
|
92
|
+
].join('\n'), 'utf-8');
|
|
93
|
+
const latest = await readLatestChannelEvents(stateRoot, {
|
|
94
|
+
now: Date.parse('2026-09-07T12:00:00Z')
|
|
95
|
+
});
|
|
96
|
+
assert.equal(latest.get('opensky')?.state, 'ok', "'ok' 应超越同通道更早的 down");
|
|
97
|
+
assert.equal(latest.get('hbcli-hotel')?.state, 'down', '未恢复通道保持 down');
|
|
98
|
+
assert.equal(latest.size, 2, '坏行跳过,latest 只含两通道');
|
|
99
|
+
const openskyEv = latest.get('opensky');
|
|
100
|
+
assert.ok(openskyEv && openskyEv.state !== 'down', 'doctor 的 down 判定应不再命中已恢复通道');
|
|
101
|
+
const rows = (await readFile(jsonl, 'utf-8')).split('\n').filter((l)=>l.trim()).flatMap((l)=>{
|
|
102
|
+
try {
|
|
103
|
+
return [
|
|
104
|
+
JSON.parse(l)
|
|
105
|
+
];
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
const stats = aggregateChannels(rows, {
|
|
111
|
+
now: new Date('2026-09-07T12:00:00Z'),
|
|
112
|
+
windowDays: 30
|
|
113
|
+
});
|
|
114
|
+
const openskyStat = stats.latest.find((c)=>c.channel === 'opensky');
|
|
115
|
+
assert.equal(openskyStat, undefined, 'ok 超越的通道不应出现在当前 down/cooldown newest 面');
|
|
116
|
+
const hbStat = stats.latest.find((c)=>c.channel === 'hbcli-hotel');
|
|
117
|
+
assert.ok(hbStat && hbStat.state === 'down', '未恢复通道仍在 newest 面');
|
|
118
|
+
const listed = defaultProbes();
|
|
119
|
+
assert.ok(listed.find((p)=>p.channel === 'flyai')?.probeable === false, 'flyai 默认不探(共享额度)');
|
|
120
|
+
assert.ok(listed.filter((p)=>p.channel.startsWith('session:')).every((p)=>!p.probeable), 'session:* 全部 skip');
|
|
121
|
+
assert.deepEqual(parseArgs([
|
|
122
|
+
'--state-root',
|
|
123
|
+
'/tmp/x',
|
|
124
|
+
'--only',
|
|
125
|
+
'a, b',
|
|
126
|
+
'--dry-run'
|
|
127
|
+
]), {
|
|
128
|
+
stateRoot: '/tmp/x',
|
|
129
|
+
only: [
|
|
130
|
+
'a',
|
|
131
|
+
'b'
|
|
132
|
+
],
|
|
133
|
+
dryRun: true,
|
|
134
|
+
list: false
|
|
135
|
+
}, 'parseArgs 解析');
|
|
136
|
+
const rcList = await main([
|
|
137
|
+
'--list'
|
|
138
|
+
]);
|
|
139
|
+
assert.equal(rcList, 0, '--list exit 0');
|
|
140
|
+
const rcBad = await main([
|
|
141
|
+
'--only',
|
|
142
|
+
'no-such-channel'
|
|
143
|
+
]);
|
|
144
|
+
assert.equal(rcBad, 2, '--only 未匹配 exit 2');
|
|
145
|
+
console.log('CHANNEL PROBE TESTS: 5/5 OK(evaluate 纯函数 / latest-wins 恢复 / doctor 口径 / metrics 超越 / CLI 面)');
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
//# sourceURL=ts/scripts/channel-probe-tests.ts
|