@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
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 +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -1,8 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 价格漂移监测合同测试(issue #49 长效机制,run-all §41)。
|
|
3
|
+
* 全确定性、零网络:
|
|
4
|
+
* - diff 算法守门:baseline 改/新/删均被检出;
|
|
5
|
+
* - offline 模式:无 baseline → skipped_no_baseline;有 baseline → unchanged(reason 提示 --fetch);
|
|
6
|
+
* - fetch 模式:fetch 失败 → skipped_no_network;parse 失败 → skipped_parse_error;baseline 不存在 →
|
|
7
|
+
* fetched_no_table_entry 且写 fixture;baseline 存在 + 完全相同 → unchanged;存在 + 不同 → drift;
|
|
8
|
+
* - 写入 baseline fixture 的 schema_version 守门;
|
|
9
|
+
* - PR 段落渲染:drift 含 model/field/from/to/direction 表格 + 纪律注脚。
|
|
10
|
+
* - diff 细节:字段 up/down/new/removed 四个方向全检。
|
|
11
|
+
*
|
|
12
|
+
* 网络例一律走注入的 fetcher / providers 路径,不真联网(真联网在 founder 手动 / CI 心跳)。
|
|
13
|
+
*/
|
|
1
14
|
import assert from 'node:assert/strict';
|
|
2
15
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
16
|
import { join } from 'node:path';
|
|
4
17
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { runPriceDriftWatch, parseOpenaiHtml } from './price-drift-watch.js';
|
|
18
|
+
import { runPriceDriftWatch, parseOpenaiHtml, } from './price-drift-watch.js';
|
|
6
19
|
let passed = 0;
|
|
7
20
|
function ok(label) {
|
|
8
21
|
passed += 1;
|
|
@@ -12,135 +25,113 @@ function tmpRoot() {
|
|
|
12
25
|
return mkdtempSync(join(tmpdir(), 'drift-'));
|
|
13
26
|
}
|
|
14
27
|
function makeBaseline(rows, asOf = '2026-08-30T00:00:00.000Z') {
|
|
15
|
-
return JSON.stringify({
|
|
16
|
-
schema_version: 'gotry_llm_price_baseline_v1',
|
|
17
|
-
as_of: asOf,
|
|
18
|
-
source_url: 'mock://test',
|
|
19
|
-
rows
|
|
20
|
-
});
|
|
28
|
+
return JSON.stringify({ schema_version: 'gotry_llm_price_baseline_v1', as_of: asOf, source_url: 'mock://test', rows });
|
|
21
29
|
}
|
|
30
|
+
/** 测试用注入 provider:baseline_path 指向 tmp 文件,parser 接受 mock HTML 字符串。 */
|
|
22
31
|
function makeMockProvider(id, baselinePath, _fetchReturn) {
|
|
23
|
-
const parser = (html)=>{
|
|
32
|
+
const parser = (html) => {
|
|
24
33
|
const rows = [];
|
|
25
|
-
for (const seg of html.split(';')){
|
|
34
|
+
for (const seg of html.split(';')) {
|
|
26
35
|
const [model, nums] = seg.split(':');
|
|
27
|
-
if (!model || !nums)
|
|
36
|
+
if (!model || !nums)
|
|
37
|
+
continue;
|
|
28
38
|
const [miss, hit, out] = nums.split(',').map(Number);
|
|
29
|
-
if (!Number.isFinite(miss) || !Number.isFinite(out))
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
input_cache_miss_usd_per_1m: miss,
|
|
33
|
-
input_cache_hit_usd_per_1m: Number.isFinite(hit) ? hit : null,
|
|
34
|
-
output_usd_per_1m: out
|
|
35
|
-
});
|
|
39
|
+
if (!Number.isFinite(miss) || !Number.isFinite(out))
|
|
40
|
+
continue;
|
|
41
|
+
rows.push({ model: model.trim(), input_cache_miss_usd_per_1m: miss, input_cache_hit_usd_per_1m: Number.isFinite(hit) ? hit : null, output_usd_per_1m: out });
|
|
36
42
|
}
|
|
37
43
|
return rows;
|
|
38
44
|
};
|
|
39
|
-
return [
|
|
40
|
-
{
|
|
45
|
+
return [{
|
|
41
46
|
id,
|
|
42
47
|
source_url: `mock://${id}`,
|
|
43
48
|
baseline_path: baselinePath,
|
|
44
|
-
parse: parser
|
|
45
|
-
}
|
|
46
|
-
];
|
|
49
|
+
parse: parser,
|
|
50
|
+
}];
|
|
47
51
|
}
|
|
48
52
|
function fetcherFromMap(map) {
|
|
49
|
-
return async (url)=>{
|
|
53
|
+
return async (url) => {
|
|
50
54
|
const v = map[url];
|
|
51
|
-
if (v instanceof Error)
|
|
52
|
-
|
|
55
|
+
if (v instanceof Error)
|
|
56
|
+
throw v;
|
|
57
|
+
if (typeof v === 'string')
|
|
58
|
+
return v;
|
|
53
59
|
throw new Error(`no mock for ${url}`);
|
|
54
60
|
};
|
|
55
61
|
}
|
|
62
|
+
// ---- 1. offline 无 baseline → skipped_no_baseline ----
|
|
56
63
|
async function testOfflineNoBaseline() {
|
|
57
64
|
const root = tmpRoot();
|
|
58
65
|
try {
|
|
59
66
|
const report = await runPriceDriftWatch({
|
|
60
67
|
mode: 'offline',
|
|
61
|
-
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
62
|
-
providers: makeMockProvider('mockprov', join(root, 'no-such-file.json'), '')
|
|
68
|
+
observedAt: () => new Date('2026-08-30T12:00:00Z'),
|
|
69
|
+
providers: makeMockProvider('mockprov', join(root, 'no-such-file.json'), ''),
|
|
63
70
|
});
|
|
64
71
|
assert.equal(report.schema_version, 'gotry_llm_price_drift_v1');
|
|
65
72
|
assert.equal(report.providers['mockprov'].status, 'skipped_no_baseline');
|
|
66
73
|
ok('offline 无 baseline → skipped_no_baseline');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
force: true
|
|
71
|
-
});
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
rmSync(root, { recursive: true, force: true });
|
|
72
77
|
}
|
|
73
78
|
}
|
|
79
|
+
// ---- 2. offline 有 baseline → unchanged(reason 提示 --fetch) ----
|
|
74
80
|
async function testOfflineWithBaseline() {
|
|
75
81
|
const root = tmpRoot();
|
|
76
82
|
try {
|
|
77
83
|
const bp = join(root, 'b.json');
|
|
78
84
|
writeFileSync(bp, makeBaseline([
|
|
79
|
-
{
|
|
80
|
-
model: 'm1',
|
|
81
|
-
input_cache_miss_usd_per_1m: 1.0,
|
|
82
|
-
input_cache_hit_usd_per_1m: 0.1,
|
|
83
|
-
output_usd_per_1m: 3.0
|
|
84
|
-
}
|
|
85
|
+
{ model: 'm1', input_cache_miss_usd_per_1m: 1.0, input_cache_hit_usd_per_1m: 0.1, output_usd_per_1m: 3.0 },
|
|
85
86
|
]));
|
|
86
87
|
const report = await runPriceDriftWatch({
|
|
87
88
|
mode: 'offline',
|
|
88
|
-
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
89
|
-
providers: makeMockProvider('mockprov', bp, '')
|
|
89
|
+
observedAt: () => new Date('2026-08-30T12:00:00Z'),
|
|
90
|
+
providers: makeMockProvider('mockprov', bp, ''),
|
|
90
91
|
});
|
|
91
92
|
assert.equal(report.providers['mockprov'].status, 'unchanged');
|
|
92
93
|
assert.match(report.providers['mockprov'].reason ?? '', /--fetch/, 'offline reason 应提示 --fetch');
|
|
93
94
|
ok('offline 有 baseline → unchanged + reason 提示 --fetch');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
force: true
|
|
98
|
-
});
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
rmSync(root, { recursive: true, force: true });
|
|
99
98
|
}
|
|
100
99
|
}
|
|
100
|
+
// ---- 3. fetch 模式:fet 失败 → skipped_no_network ----
|
|
101
101
|
async function testFetchFailure() {
|
|
102
102
|
const root = tmpRoot();
|
|
103
103
|
try {
|
|
104
104
|
const bp = join(root, 'b.json');
|
|
105
105
|
writeFileSync(bp, makeBaseline([
|
|
106
|
-
{
|
|
107
|
-
model: 'm1',
|
|
108
|
-
input_cache_miss_usd_per_1m: 1.0,
|
|
109
|
-
input_cache_hit_usd_per_1m: 0.1,
|
|
110
|
-
output_usd_per_1m: 3.0
|
|
111
|
-
}
|
|
106
|
+
{ model: 'm1', input_cache_miss_usd_per_1m: 1.0, input_cache_hit_usd_per_1m: 0.1, output_usd_per_1m: 3.0 },
|
|
112
107
|
]));
|
|
113
108
|
const report = await runPriceDriftWatch({
|
|
114
109
|
mode: 'fetch',
|
|
115
|
-
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
110
|
+
observedAt: () => new Date('2026-08-30T12:00:00Z'),
|
|
116
111
|
providers: makeMockProvider('mockprov', bp, ''),
|
|
117
|
-
fetcher: fetcherFromMap({
|
|
118
|
-
'mock://mockprov': new Error('ECONNREFUSED')
|
|
119
|
-
})
|
|
112
|
+
fetcher: fetcherFromMap({ 'mock://mockprov': new Error('ECONNREFUSED') }),
|
|
120
113
|
});
|
|
121
114
|
assert.equal(report.providers['mockprov'].status, 'skipped_no_network');
|
|
122
115
|
assert.match(report.providers['mockprov'].reason ?? '', /ECONNREFUSED/);
|
|
123
116
|
ok('fetch 失败 → skipped_no_network');
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
force: true
|
|
128
|
-
});
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
rmSync(root, { recursive: true, force: true });
|
|
129
120
|
}
|
|
130
121
|
}
|
|
122
|
+
// ---- 4. fetch 模式:首次 fetch 无 baseline → fetched_no_table_entry 且写 fixture ----
|
|
131
123
|
async function testFetchFirstTimeWritesBaseline() {
|
|
132
124
|
const root = tmpRoot();
|
|
133
125
|
try {
|
|
134
126
|
const bp = join(root, 'b.json');
|
|
135
127
|
const report = await runPriceDriftWatch({
|
|
136
128
|
mode: 'fetch',
|
|
137
|
-
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
129
|
+
observedAt: () => new Date('2026-08-30T12:00:00Z'),
|
|
138
130
|
providers: makeMockProvider('mockprov', bp, ''),
|
|
139
|
-
fetcher: fetcherFromMap({
|
|
140
|
-
'mock://mockprov': 'm1:1.0,0.1,3.0;m2:2.0,0.2,5.0'
|
|
141
|
-
})
|
|
131
|
+
fetcher: fetcherFromMap({ 'mock://mockprov': 'm1:1.0,0.1,3.0;m2:2.0,0.2,5.0' }),
|
|
142
132
|
});
|
|
143
133
|
assert.equal(report.providers['mockprov'].status, 'fetched_no_table_entry');
|
|
134
|
+
// 必须有 fixture 文件
|
|
144
135
|
const { existsSync, readFileSync } = await import('node:fs');
|
|
145
136
|
assert.ok(existsSync(bp), 'baseline fixture 必须被写');
|
|
146
137
|
const written = JSON.parse(readFileSync(bp, 'utf8'));
|
|
@@ -148,155 +139,115 @@ async function testFetchFirstTimeWritesBaseline() {
|
|
|
148
139
|
assert.ok(Array.isArray(written['rows']));
|
|
149
140
|
assert.equal(written['rows'].length, 2);
|
|
150
141
|
ok('fetch 首次 → fetched_no_table_entry + 写 fixture');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
force: true
|
|
155
|
-
});
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
rmSync(root, { recursive: true, force: true });
|
|
156
145
|
}
|
|
157
146
|
}
|
|
147
|
+
// ---- 5. fetch 模式:baseline + current 完全相同 → unchanged ----
|
|
158
148
|
async function testFetchUnchanged() {
|
|
159
149
|
const root = tmpRoot();
|
|
160
150
|
try {
|
|
161
151
|
const bp = join(root, 'b.json');
|
|
162
152
|
writeFileSync(bp, makeBaseline([
|
|
163
|
-
{
|
|
164
|
-
model: 'm1',
|
|
165
|
-
input_cache_miss_usd_per_1m: 1.0,
|
|
166
|
-
input_cache_hit_usd_per_1m: 0.1,
|
|
167
|
-
output_usd_per_1m: 3.0
|
|
168
|
-
}
|
|
153
|
+
{ model: 'm1', input_cache_miss_usd_per_1m: 1.0, input_cache_hit_usd_per_1m: 0.1, output_usd_per_1m: 3.0 },
|
|
169
154
|
]));
|
|
170
155
|
const report = await runPriceDriftWatch({
|
|
171
156
|
mode: 'fetch',
|
|
172
|
-
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
157
|
+
observedAt: () => new Date('2026-08-30T12:00:00Z'),
|
|
173
158
|
providers: makeMockProvider('mockprov', bp, ''),
|
|
174
|
-
fetcher: fetcherFromMap({
|
|
175
|
-
'mock://mockprov': 'm1:1.0,0.1,3.0'
|
|
176
|
-
})
|
|
159
|
+
fetcher: fetcherFromMap({ 'mock://mockprov': 'm1:1.0,0.1,3.0' }),
|
|
177
160
|
});
|
|
178
161
|
assert.equal(report.providers['mockprov'].status, 'unchanged');
|
|
179
162
|
ok('fetch 模式:baseline + current 同 → unchanged');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
force: true
|
|
184
|
-
});
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
rmSync(root, { recursive: true, force: true });
|
|
185
166
|
}
|
|
186
167
|
}
|
|
168
|
+
// ---- 6. fetch 模式:baseline + current 不同 → drift + 字段方向 ----
|
|
187
169
|
async function testFetchDrift() {
|
|
188
170
|
const root = tmpRoot();
|
|
189
171
|
try {
|
|
190
172
|
const bp = join(root, 'b.json');
|
|
191
173
|
writeFileSync(bp, makeBaseline([
|
|
192
|
-
{
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
output_usd_per_1m: 3.0
|
|
197
|
-
},
|
|
198
|
-
{
|
|
199
|
-
model: 'm-up',
|
|
200
|
-
input_cache_miss_usd_per_1m: 1.0,
|
|
201
|
-
input_cache_hit_usd_per_1m: 0.1,
|
|
202
|
-
output_usd_per_1m: 3.0
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
model: 'm-down',
|
|
206
|
-
input_cache_miss_usd_per_1m: 1.0,
|
|
207
|
-
input_cache_hit_usd_per_1m: 0.1,
|
|
208
|
-
output_usd_per_1m: 3.0
|
|
209
|
-
},
|
|
210
|
-
{
|
|
211
|
-
model: 'm-removed',
|
|
212
|
-
input_cache_miss_usd_per_1m: 1.0,
|
|
213
|
-
input_cache_hit_usd_per_1m: 0.1,
|
|
214
|
-
output_usd_per_1m: 3.0
|
|
215
|
-
}
|
|
174
|
+
{ model: 'm-keep', input_cache_miss_usd_per_1m: 1.0, input_cache_hit_usd_per_1m: 0.1, output_usd_per_1m: 3.0 },
|
|
175
|
+
{ model: 'm-up', input_cache_miss_usd_per_1m: 1.0, input_cache_hit_usd_per_1m: 0.1, output_usd_per_1m: 3.0 },
|
|
176
|
+
{ model: 'm-down', input_cache_miss_usd_per_1m: 1.0, input_cache_hit_usd_per_1m: 0.1, output_usd_per_1m: 3.0 },
|
|
177
|
+
{ model: 'm-removed', input_cache_miss_usd_per_1m: 1.0, input_cache_hit_usd_per_1m: 0.1, output_usd_per_1m: 3.0 },
|
|
216
178
|
]));
|
|
217
179
|
const report = await runPriceDriftWatch({
|
|
218
180
|
mode: 'fetch',
|
|
219
|
-
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
181
|
+
observedAt: () => new Date('2026-08-30T12:00:00Z'),
|
|
220
182
|
providers: makeMockProvider('mockprov', bp, ''),
|
|
221
183
|
fetcher: fetcherFromMap({
|
|
222
184
|
'mock://mockprov': [
|
|
223
185
|
'm-keep:1.0,0.1,3.0',
|
|
224
|
-
'm-up:1.5,0.1,3.0',
|
|
225
|
-
'm-down:0.8,0.1,3.0',
|
|
226
|
-
'm-new:2.0,0.2,4.0'
|
|
227
|
-
|
|
228
|
-
|
|
186
|
+
'm-up:1.5,0.1,3.0', // miss 1.0→1.5 up
|
|
187
|
+
'm-down:0.8,0.1,3.0', // miss 1.0→0.8 down
|
|
188
|
+
'm-new:2.0,0.2,4.0', // 全 new
|
|
189
|
+
// m-removed 不在 current,标记 removed
|
|
190
|
+
].join(';'),
|
|
191
|
+
}),
|
|
229
192
|
});
|
|
230
193
|
const r = report.providers['mockprov'];
|
|
231
194
|
assert.equal(r.status, 'drift');
|
|
232
195
|
const drift = r.drift ?? [];
|
|
233
|
-
|
|
196
|
+
// 期望:m-up input_cache_miss up, m-down input_cache_miss down, m-new input_cache_miss new, m-removed input_cache_miss removed
|
|
197
|
+
const fields = drift.map(d => `${d.model}|${d.field}|${d.direction}`).sort();
|
|
234
198
|
assert.deepEqual(fields, [
|
|
235
199
|
'm-down|input_cache_miss_usd_per_1m|down',
|
|
236
200
|
'm-new|input_cache_miss_usd_per_1m|new',
|
|
237
201
|
'm-removed|input_cache_miss_usd_per_1m|removed',
|
|
238
|
-
'm-up|input_cache_miss_usd_per_1m|up'
|
|
202
|
+
'm-up|input_cache_miss_usd_per_1m|up',
|
|
239
203
|
], 'drift 字段方向必须逐项正确');
|
|
240
204
|
ok('fetch drift:up/down/new/removed 四向全检');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
force: true
|
|
245
|
-
});
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
rmSync(root, { recursive: true, force: true });
|
|
246
208
|
}
|
|
247
209
|
}
|
|
210
|
+
// ---- 7. PR 段落渲染:drift 时含表格 + 纪律注脚 ----
|
|
248
211
|
async function testPrParagraph() {
|
|
249
212
|
const root = tmpRoot();
|
|
250
213
|
try {
|
|
251
214
|
const bp = join(root, 'b.json');
|
|
252
215
|
writeFileSync(bp, makeBaseline([
|
|
253
|
-
{
|
|
254
|
-
model: 'deepseek-v4-flash',
|
|
255
|
-
input_cache_miss_usd_per_1m: 1.32,
|
|
256
|
-
input_cache_hit_usd_per_1m: 0.014,
|
|
257
|
-
output_usd_per_1m: 3.0
|
|
258
|
-
}
|
|
216
|
+
{ model: 'deepseek-v4-flash', input_cache_miss_usd_per_1m: 1.32, input_cache_hit_usd_per_1m: 0.014, output_usd_per_1m: 3.0 },
|
|
259
217
|
]));
|
|
260
218
|
const report = await runPriceDriftWatch({
|
|
261
219
|
mode: 'fetch',
|
|
262
|
-
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
220
|
+
observedAt: () => new Date('2026-08-30T12:00:00Z'),
|
|
263
221
|
providers: makeMockProvider('mockprov', bp, ''),
|
|
264
|
-
fetcher: fetcherFromMap({
|
|
265
|
-
'mock://mockprov': 'deepseek-v4-flash:1.32,0.014,3.0'
|
|
266
|
-
})
|
|
222
|
+
fetcher: fetcherFromMap({ 'mock://mockprov': 'deepseek-v4-flash:1.32,0.014,3.0' }),
|
|
267
223
|
});
|
|
268
224
|
assert.equal(report.providers['mockprov'].status, 'unchanged');
|
|
269
225
|
assert.match(report.suggested_pr_paragraph, /价格漂移监测/);
|
|
270
226
|
assert.match(report.suggested_pr_paragraph, /mockprov/);
|
|
271
227
|
assert.match(report.suggested_pr_paragraph, /人工 PR/);
|
|
272
228
|
ok('PR 段落含监测标题 + provider 变动 + 纪律注脚');
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
force: true
|
|
277
|
-
});
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
rmSync(root, { recursive: true, force: true });
|
|
278
232
|
}
|
|
279
233
|
}
|
|
234
|
+
// ---- 8. baseline schema_version 守门:不匹配 schema 视为不存在(loader 容错) ----
|
|
280
235
|
async function testBadBaseline() {
|
|
281
236
|
const root = tmpRoot();
|
|
282
237
|
try {
|
|
283
238
|
const bp = join(root, 'b.json');
|
|
284
|
-
writeFileSync(bp, JSON.stringify({
|
|
285
|
-
|
|
286
|
-
rows: []
|
|
287
|
-
}));
|
|
239
|
+
writeFileSync(bp, JSON.stringify({ schema_version: 'gotry_llm_price_baseline_v99', rows: [] }));
|
|
240
|
+
// 离线模式:坏 baseline 视为不存在 → skipped_no_baseline
|
|
288
241
|
const report = await runPriceDriftWatch({
|
|
289
242
|
mode: 'offline',
|
|
290
|
-
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
291
|
-
providers: makeMockProvider('mockprov', bp, '')
|
|
243
|
+
observedAt: () => new Date('2026-08-30T12:00:00Z'),
|
|
244
|
+
providers: makeMockProvider('mockprov', bp, ''),
|
|
292
245
|
});
|
|
293
246
|
assert.equal(report.providers['mockprov'].status, 'skipped_no_baseline', '坏 baseline schema_version 视为不存在,offline 标 SKIP');
|
|
294
247
|
ok('坏 baseline schema_version → offline 视为不存在(SKIP)');
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
force: true
|
|
299
|
-
});
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
rmSync(root, { recursive: true, force: true });
|
|
300
251
|
}
|
|
301
252
|
}
|
|
302
253
|
await testOfflineNoBaseline();
|
|
@@ -307,135 +258,83 @@ await testFetchUnchanged();
|
|
|
307
258
|
await testFetchDrift();
|
|
308
259
|
await testPrParagraph();
|
|
309
260
|
await testBadBaseline();
|
|
261
|
+
// ---- 9. snapshot 路径:parser 解析 __NEXT_DATA__ 内嵌 JSON dump,fallback 解析段落文字 ----
|
|
310
262
|
async function testSnapshotNextDataParser() {
|
|
311
263
|
const root = tmpRoot();
|
|
312
264
|
try {
|
|
313
265
|
const bp = join(root, 'b.json');
|
|
314
266
|
const sp = join(root, 'snapshot.html');
|
|
315
267
|
writeFileSync(bp, makeBaseline([
|
|
316
|
-
{
|
|
317
|
-
model: 'gpt-4o',
|
|
318
|
-
input_cache_miss_usd_per_1m: 2.5,
|
|
319
|
-
input_cache_hit_usd_per_1m: 1.25,
|
|
320
|
-
output_usd_per_1m: 10.0
|
|
321
|
-
}
|
|
268
|
+
{ model: 'gpt-4o', input_cache_miss_usd_per_1m: 2.5, input_cache_hit_usd_per_1m: 1.25, output_usd_per_1m: 10.0 },
|
|
322
269
|
]));
|
|
270
|
+
// 真实 snapshot 文件模拟 __NEXT_DATA__ 形态
|
|
323
271
|
const html = `<html><script id="__NEXT_DATA__" type="application/json">${JSON.stringify({
|
|
324
|
-
props: {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
{
|
|
328
|
-
name: 'gpt-4o',
|
|
329
|
-
input_cost_per_token: 0.0000025,
|
|
330
|
-
output_cost_per_token: 0.00001,
|
|
331
|
-
cached_input_cost_per_token: 0.00000125
|
|
332
|
-
}
|
|
333
|
-
]
|
|
334
|
-
}
|
|
335
|
-
}
|
|
272
|
+
props: { pageProps: { models: [
|
|
273
|
+
{ name: 'gpt-4o', input_cost_per_token: 0.0000025, output_cost_per_token: 0.00001, cached_input_cost_per_token: 0.00000125 },
|
|
274
|
+
] } },
|
|
336
275
|
})}</script></html>`;
|
|
337
276
|
writeFileSync(sp, html);
|
|
338
277
|
const report = await runPriceDriftWatch({
|
|
339
278
|
mode: 'offline',
|
|
340
|
-
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
341
|
-
providers: [
|
|
342
|
-
{
|
|
343
|
-
id: 'mockprov',
|
|
344
|
-
source_url: 'mock://mockprov',
|
|
345
|
-
baseline_path: bp,
|
|
346
|
-
snapshot_path: sp,
|
|
347
|
-
parse: parseOpenaiHtml
|
|
348
|
-
}
|
|
349
|
-
]
|
|
279
|
+
observedAt: () => new Date('2026-08-30T12:00:00Z'),
|
|
280
|
+
providers: [{ id: 'mockprov', source_url: 'mock://mockprov', baseline_path: bp, snapshot_path: sp, parse: parseOpenaiHtml }],
|
|
350
281
|
});
|
|
351
282
|
assert.equal(report.providers['mockprov'].status, 'unchanged', 'snapshot 与 baseline 同价 → unchanged(offline)');
|
|
352
283
|
ok('snapshot 路径:__NEXT_DATA__ 内嵌 JSON dump 解析成功 + 与 baseline 同 → unchanged');
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
force: true
|
|
357
|
-
});
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
rmSync(root, { recursive: true, force: true });
|
|
358
287
|
}
|
|
359
288
|
}
|
|
289
|
+
// ---- 10. snapshot 路径:parser fallback 段落文字;baseline 不一致 → drift ----
|
|
360
290
|
async function testSnapshotFallbackDrift() {
|
|
361
291
|
const root = tmpRoot();
|
|
362
292
|
try {
|
|
363
293
|
const bp = join(root, 'b.json');
|
|
364
294
|
const sp = join(root, 'snapshot.html');
|
|
365
295
|
writeFileSync(bp, makeBaseline([
|
|
366
|
-
{
|
|
367
|
-
model: 'gpt-4o',
|
|
368
|
-
input_cache_miss_usd_per_1m: 2.5,
|
|
369
|
-
input_cache_hit_usd_per_1m: 1.25,
|
|
370
|
-
output_usd_per_1m: 10.0
|
|
371
|
-
}
|
|
296
|
+
{ model: 'gpt-4o', input_cache_miss_usd_per_1m: 2.5, input_cache_hit_usd_per_1m: 1.25, output_usd_per_1m: 10.0 },
|
|
372
297
|
]));
|
|
298
|
+
// __NEXT_DATA__ 解析失败(空 props),parser fallback 走段落文字
|
|
373
299
|
const html = '<html><body><p>gpt-4o … $5.00 / 1M input … $20.00 / 1M output</p></body></html>';
|
|
374
300
|
writeFileSync(sp, html);
|
|
375
301
|
const report = await runPriceDriftWatch({
|
|
376
302
|
mode: 'offline',
|
|
377
|
-
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
378
|
-
providers: [
|
|
379
|
-
{
|
|
380
|
-
id: 'mockprov',
|
|
381
|
-
source_url: 'mock://mockprov',
|
|
382
|
-
baseline_path: bp,
|
|
383
|
-
snapshot_path: sp,
|
|
384
|
-
parse: parseOpenaiHtml
|
|
385
|
-
}
|
|
386
|
-
]
|
|
303
|
+
observedAt: () => new Date('2026-08-30T12:00:00Z'),
|
|
304
|
+
providers: [{ id: 'mockprov', source_url: 'mock://mockprov', baseline_path: bp, snapshot_path: sp, parse: parseOpenaiHtml }],
|
|
387
305
|
});
|
|
388
306
|
const r = report.providers['mockprov'];
|
|
389
307
|
assert.equal(r.status, 'drift', 'snapshot fallback 解析 + baseline 不一致 → drift');
|
|
390
308
|
const drift = r.drift ?? [];
|
|
391
|
-
assert.ok(drift.some(
|
|
392
|
-
assert.ok(drift.some(
|
|
309
|
+
assert.ok(drift.some(d => d.field === 'input_cache_miss_usd_per_1m' && d.from === 2.5 && d.to === 5.0 && d.direction === 'up'), 'miss 字段 up drift');
|
|
310
|
+
assert.ok(drift.some(d => d.field === 'output_usd_per_1m' && d.from === 10.0 && d.to === 20.0 && d.direction === 'up'), 'output 字段 up drift');
|
|
393
311
|
ok('snapshot fallback:段落文字解析 + 与 baseline 不同 → drift(含 up/down/new/removed)');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
force: true
|
|
398
|
-
});
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
rmSync(root, { recursive: true, force: true });
|
|
399
315
|
}
|
|
400
316
|
}
|
|
317
|
+
// ---- 11. snapshot 缺失守门:有 baseline 但无 snapshot → skipped_no_snapshot ----
|
|
401
318
|
async function testSnapshotMissing() {
|
|
402
319
|
const root = tmpRoot();
|
|
403
320
|
try {
|
|
404
321
|
const bp = join(root, 'b.json');
|
|
405
322
|
writeFileSync(bp, makeBaseline([
|
|
406
|
-
{
|
|
407
|
-
model: 'gpt-4o',
|
|
408
|
-
input_cache_miss_usd_per_1m: 2.5,
|
|
409
|
-
input_cache_hit_usd_per_1m: 1.25,
|
|
410
|
-
output_usd_per_1m: 10.0
|
|
411
|
-
}
|
|
323
|
+
{ model: 'gpt-4o', input_cache_miss_usd_per_1m: 2.5, input_cache_hit_usd_per_1m: 1.25, output_usd_per_1m: 10.0 },
|
|
412
324
|
]));
|
|
413
325
|
const report = await runPriceDriftWatch({
|
|
414
326
|
mode: 'offline',
|
|
415
|
-
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
416
|
-
providers: [
|
|
417
|
-
{
|
|
418
|
-
id: 'mockprov',
|
|
419
|
-
source_url: 'mock://mockprov',
|
|
420
|
-
baseline_path: bp,
|
|
421
|
-
snapshot_path: join(root, 'no-such-snapshot.html'),
|
|
422
|
-
parse: parseOpenaiHtml
|
|
423
|
-
}
|
|
424
|
-
]
|
|
327
|
+
observedAt: () => new Date('2026-08-30T12:00:00Z'),
|
|
328
|
+
providers: [{ id: 'mockprov', source_url: 'mock://mockprov', baseline_path: bp, snapshot_path: join(root, 'no-such-snapshot.html'), parse: parseOpenaiHtml }],
|
|
425
329
|
});
|
|
426
330
|
assert.equal(report.providers['mockprov'].status, 'skipped_no_snapshot', 'snapshot 缺失 → SKIP');
|
|
427
331
|
ok('snapshot 缺失 → skipped_no_snapshot(offline)');
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
force: true
|
|
432
|
-
});
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
rmSync(root, { recursive: true, force: true });
|
|
433
335
|
}
|
|
434
336
|
}
|
|
435
337
|
await testSnapshotNextDataParser();
|
|
436
338
|
await testSnapshotFallbackDrift();
|
|
437
339
|
await testSnapshotMissing();
|
|
438
340
|
console.log(`price-drift-watch tests: ${passed} 组断言全绿(offline,fetch 模式手动/CI 心跳)`);
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
//# sourceURL=ts/scripts/price-drift-tests.ts
|