@danceiny/gotry 0.0.1-rc.15 → 0.0.1-rc.16
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 +10 -6
- package/README.zh-CN.md +10 -6
- package/bin/gotry-bootstrap.js +294 -5
- package/cordis.gotry-patch.yml +10 -0
- package/data/airline-airports.json +39 -0
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +1 -1
- package/dist/capabilities/effect.js +293 -0
- package/dist/capabilities/fact-log.js +41 -0
- package/dist/capabilities/flyai.js +1 -1
- package/dist/capabilities/hbcli.js +43 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +90 -0
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +2 -1
- package/dist/capabilities/session/extension-bridge.js +345 -0
- package/dist/capabilities/session/extension-channel.js +93 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/health-watch.js +216 -0
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +377 -0
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +77 -1
- package/dist/capabilities/session-search.js +69 -2
- package/dist/capabilities/weather.js +1 -1
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/booking-saga-tests.js +1 -1
- package/dist/scripts/bootstrap-tests.js +39 -3
- package/dist/scripts/build-changelog.js +279 -0
- package/dist/scripts/changelog-tests.js +165 -0
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/effect-tests.js +474 -0
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/extension-tests.js +468 -0
- package/dist/scripts/fact-gate-tests.js +318 -0
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +180 -0
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +51 -0
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/nightly-evidence-tests.js +67 -4
- package/dist/scripts/nightly-evidence.js +59 -3
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +218 -0
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/price-drift-tests.js +441 -0
- package/dist/scripts/price-drift-watch.js +491 -0
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +1 -1
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +1 -1
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke.js +76 -1
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +1 -1
- package/dist/scripts/wizard-bootstrap.js +32 -0
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +310 -0
- package/dist/src/bookable-facts.js +349 -0
- package/dist/src/booking-saga.js +1 -1
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +1 -1
- package/dist/src/engine.js +1 -1
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +189 -46
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +14 -7
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +1 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +26 -0
- package/extension/background.js +160 -0
- package/extension/content-bridge.js +41 -0
- package/extension/content-main.js +62 -0
- package/extension/manifest.json +32 -0
- package/package.json +6 -2
- package/ts/capabilities/fact-log.ts +54 -0
- package/ts/capabilities/hbcli.ts +46 -0
- package/ts/capabilities/session/benchmark.ts +4 -1
- package/ts/capabilities/session/extension-bridge.ts +354 -0
- package/ts/capabilities/session/extension-channel.ts +92 -0
- package/ts/capabilities/session/health-watch.ts +257 -0
- package/ts/capabilities/session/wizard.ts +282 -0
- package/ts/capabilities/session-login.ts +53 -2
- package/ts/capabilities/session-search.ts +68 -3
- package/ts/src/artifact-gate.ts +365 -0
- package/ts/src/bookable-facts.ts +557 -0
- package/ts/src/index.ts +102 -8
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { runPriceDriftWatch, parseOpenaiHtml } from './price-drift-watch.js';
|
|
6
|
+
let passed = 0;
|
|
7
|
+
function ok(label) {
|
|
8
|
+
passed += 1;
|
|
9
|
+
console.log(` ok ${label}`);
|
|
10
|
+
}
|
|
11
|
+
function tmpRoot() {
|
|
12
|
+
return mkdtempSync(join(tmpdir(), 'drift-'));
|
|
13
|
+
}
|
|
14
|
+
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
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function makeMockProvider(id, baselinePath, _fetchReturn) {
|
|
23
|
+
const parser = (html)=>{
|
|
24
|
+
const rows = [];
|
|
25
|
+
for (const seg of html.split(';')){
|
|
26
|
+
const [model, nums] = seg.split(':');
|
|
27
|
+
if (!model || !nums) continue;
|
|
28
|
+
const [miss, hit, out] = nums.split(',').map(Number);
|
|
29
|
+
if (!Number.isFinite(miss) || !Number.isFinite(out)) continue;
|
|
30
|
+
rows.push({
|
|
31
|
+
model: model.trim(),
|
|
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
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return rows;
|
|
38
|
+
};
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
id,
|
|
42
|
+
source_url: `mock://${id}`,
|
|
43
|
+
baseline_path: baselinePath,
|
|
44
|
+
parse: parser
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
}
|
|
48
|
+
function fetcherFromMap(map) {
|
|
49
|
+
return async (url)=>{
|
|
50
|
+
const v = map[url];
|
|
51
|
+
if (v instanceof Error) throw v;
|
|
52
|
+
if (typeof v === 'string') return v;
|
|
53
|
+
throw new Error(`no mock for ${url}`);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async function testOfflineNoBaseline() {
|
|
57
|
+
const root = tmpRoot();
|
|
58
|
+
try {
|
|
59
|
+
const report = await runPriceDriftWatch({
|
|
60
|
+
mode: 'offline',
|
|
61
|
+
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
62
|
+
providers: makeMockProvider('mockprov', join(root, 'no-such-file.json'), '')
|
|
63
|
+
});
|
|
64
|
+
assert.equal(report.schema_version, 'gotry_llm_price_drift_v1');
|
|
65
|
+
assert.equal(report.providers['mockprov'].status, 'skipped_no_baseline');
|
|
66
|
+
ok('offline 无 baseline → skipped_no_baseline');
|
|
67
|
+
} finally{
|
|
68
|
+
rmSync(root, {
|
|
69
|
+
recursive: true,
|
|
70
|
+
force: true
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function testOfflineWithBaseline() {
|
|
75
|
+
const root = tmpRoot();
|
|
76
|
+
try {
|
|
77
|
+
const bp = join(root, 'b.json');
|
|
78
|
+
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
|
+
]));
|
|
86
|
+
const report = await runPriceDriftWatch({
|
|
87
|
+
mode: 'offline',
|
|
88
|
+
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
89
|
+
providers: makeMockProvider('mockprov', bp, '')
|
|
90
|
+
});
|
|
91
|
+
assert.equal(report.providers['mockprov'].status, 'unchanged');
|
|
92
|
+
assert.match(report.providers['mockprov'].reason ?? '', /--fetch/, 'offline reason 应提示 --fetch');
|
|
93
|
+
ok('offline 有 baseline → unchanged + reason 提示 --fetch');
|
|
94
|
+
} finally{
|
|
95
|
+
rmSync(root, {
|
|
96
|
+
recursive: true,
|
|
97
|
+
force: true
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function testFetchFailure() {
|
|
102
|
+
const root = tmpRoot();
|
|
103
|
+
try {
|
|
104
|
+
const bp = join(root, 'b.json');
|
|
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
|
+
}
|
|
112
|
+
]));
|
|
113
|
+
const report = await runPriceDriftWatch({
|
|
114
|
+
mode: 'fetch',
|
|
115
|
+
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
116
|
+
providers: makeMockProvider('mockprov', bp, ''),
|
|
117
|
+
fetcher: fetcherFromMap({
|
|
118
|
+
'mock://mockprov': new Error('ECONNREFUSED')
|
|
119
|
+
})
|
|
120
|
+
});
|
|
121
|
+
assert.equal(report.providers['mockprov'].status, 'skipped_no_network');
|
|
122
|
+
assert.match(report.providers['mockprov'].reason ?? '', /ECONNREFUSED/);
|
|
123
|
+
ok('fetch 失败 → skipped_no_network');
|
|
124
|
+
} finally{
|
|
125
|
+
rmSync(root, {
|
|
126
|
+
recursive: true,
|
|
127
|
+
force: true
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function testFetchFirstTimeWritesBaseline() {
|
|
132
|
+
const root = tmpRoot();
|
|
133
|
+
try {
|
|
134
|
+
const bp = join(root, 'b.json');
|
|
135
|
+
const report = await runPriceDriftWatch({
|
|
136
|
+
mode: 'fetch',
|
|
137
|
+
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
138
|
+
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
|
+
})
|
|
142
|
+
});
|
|
143
|
+
assert.equal(report.providers['mockprov'].status, 'fetched_no_table_entry');
|
|
144
|
+
const { existsSync, readFileSync } = await import('node:fs');
|
|
145
|
+
assert.ok(existsSync(bp), 'baseline fixture 必须被写');
|
|
146
|
+
const written = JSON.parse(readFileSync(bp, 'utf8'));
|
|
147
|
+
assert.equal(written['schema_version'], 'gotry_llm_price_baseline_v1');
|
|
148
|
+
assert.ok(Array.isArray(written['rows']));
|
|
149
|
+
assert.equal(written['rows'].length, 2);
|
|
150
|
+
ok('fetch 首次 → fetched_no_table_entry + 写 fixture');
|
|
151
|
+
} finally{
|
|
152
|
+
rmSync(root, {
|
|
153
|
+
recursive: true,
|
|
154
|
+
force: true
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function testFetchUnchanged() {
|
|
159
|
+
const root = tmpRoot();
|
|
160
|
+
try {
|
|
161
|
+
const bp = join(root, 'b.json');
|
|
162
|
+
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
|
+
}
|
|
169
|
+
]));
|
|
170
|
+
const report = await runPriceDriftWatch({
|
|
171
|
+
mode: 'fetch',
|
|
172
|
+
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
173
|
+
providers: makeMockProvider('mockprov', bp, ''),
|
|
174
|
+
fetcher: fetcherFromMap({
|
|
175
|
+
'mock://mockprov': 'm1:1.0,0.1,3.0'
|
|
176
|
+
})
|
|
177
|
+
});
|
|
178
|
+
assert.equal(report.providers['mockprov'].status, 'unchanged');
|
|
179
|
+
ok('fetch 模式:baseline + current 同 → unchanged');
|
|
180
|
+
} finally{
|
|
181
|
+
rmSync(root, {
|
|
182
|
+
recursive: true,
|
|
183
|
+
force: true
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
async function testFetchDrift() {
|
|
188
|
+
const root = tmpRoot();
|
|
189
|
+
try {
|
|
190
|
+
const bp = join(root, 'b.json');
|
|
191
|
+
writeFileSync(bp, makeBaseline([
|
|
192
|
+
{
|
|
193
|
+
model: 'm-keep',
|
|
194
|
+
input_cache_miss_usd_per_1m: 1.0,
|
|
195
|
+
input_cache_hit_usd_per_1m: 0.1,
|
|
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
|
+
}
|
|
216
|
+
]));
|
|
217
|
+
const report = await runPriceDriftWatch({
|
|
218
|
+
mode: 'fetch',
|
|
219
|
+
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
220
|
+
providers: makeMockProvider('mockprov', bp, ''),
|
|
221
|
+
fetcher: fetcherFromMap({
|
|
222
|
+
'mock://mockprov': [
|
|
223
|
+
'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
|
+
].join(';')
|
|
228
|
+
})
|
|
229
|
+
});
|
|
230
|
+
const r = report.providers['mockprov'];
|
|
231
|
+
assert.equal(r.status, 'drift');
|
|
232
|
+
const drift = r.drift ?? [];
|
|
233
|
+
const fields = drift.map((d)=>`${d.model}|${d.field}|${d.direction}`).sort();
|
|
234
|
+
assert.deepEqual(fields, [
|
|
235
|
+
'm-down|input_cache_miss_usd_per_1m|down',
|
|
236
|
+
'm-new|input_cache_miss_usd_per_1m|new',
|
|
237
|
+
'm-removed|input_cache_miss_usd_per_1m|removed',
|
|
238
|
+
'm-up|input_cache_miss_usd_per_1m|up'
|
|
239
|
+
], 'drift 字段方向必须逐项正确');
|
|
240
|
+
ok('fetch drift:up/down/new/removed 四向全检');
|
|
241
|
+
} finally{
|
|
242
|
+
rmSync(root, {
|
|
243
|
+
recursive: true,
|
|
244
|
+
force: true
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async function testPrParagraph() {
|
|
249
|
+
const root = tmpRoot();
|
|
250
|
+
try {
|
|
251
|
+
const bp = join(root, 'b.json');
|
|
252
|
+
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
|
+
}
|
|
259
|
+
]));
|
|
260
|
+
const report = await runPriceDriftWatch({
|
|
261
|
+
mode: 'fetch',
|
|
262
|
+
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
263
|
+
providers: makeMockProvider('mockprov', bp, ''),
|
|
264
|
+
fetcher: fetcherFromMap({
|
|
265
|
+
'mock://mockprov': 'deepseek-v4-flash:1.32,0.014,3.0'
|
|
266
|
+
})
|
|
267
|
+
});
|
|
268
|
+
assert.equal(report.providers['mockprov'].status, 'unchanged');
|
|
269
|
+
assert.match(report.suggested_pr_paragraph, /价格漂移监测/);
|
|
270
|
+
assert.match(report.suggested_pr_paragraph, /mockprov/);
|
|
271
|
+
assert.match(report.suggested_pr_paragraph, /人工 PR/);
|
|
272
|
+
ok('PR 段落含监测标题 + provider 变动 + 纪律注脚');
|
|
273
|
+
} finally{
|
|
274
|
+
rmSync(root, {
|
|
275
|
+
recursive: true,
|
|
276
|
+
force: true
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function testBadBaseline() {
|
|
281
|
+
const root = tmpRoot();
|
|
282
|
+
try {
|
|
283
|
+
const bp = join(root, 'b.json');
|
|
284
|
+
writeFileSync(bp, JSON.stringify({
|
|
285
|
+
schema_version: 'gotry_llm_price_baseline_v99',
|
|
286
|
+
rows: []
|
|
287
|
+
}));
|
|
288
|
+
const report = await runPriceDriftWatch({
|
|
289
|
+
mode: 'offline',
|
|
290
|
+
observedAt: ()=>new Date('2026-08-30T12:00:00Z'),
|
|
291
|
+
providers: makeMockProvider('mockprov', bp, '')
|
|
292
|
+
});
|
|
293
|
+
assert.equal(report.providers['mockprov'].status, 'skipped_no_baseline', '坏 baseline schema_version 视为不存在,offline 标 SKIP');
|
|
294
|
+
ok('坏 baseline schema_version → offline 视为不存在(SKIP)');
|
|
295
|
+
} finally{
|
|
296
|
+
rmSync(root, {
|
|
297
|
+
recursive: true,
|
|
298
|
+
force: true
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
await testOfflineNoBaseline();
|
|
303
|
+
await testOfflineWithBaseline();
|
|
304
|
+
await testFetchFailure();
|
|
305
|
+
await testFetchFirstTimeWritesBaseline();
|
|
306
|
+
await testFetchUnchanged();
|
|
307
|
+
await testFetchDrift();
|
|
308
|
+
await testPrParagraph();
|
|
309
|
+
await testBadBaseline();
|
|
310
|
+
async function testSnapshotNextDataParser() {
|
|
311
|
+
const root = tmpRoot();
|
|
312
|
+
try {
|
|
313
|
+
const bp = join(root, 'b.json');
|
|
314
|
+
const sp = join(root, 'snapshot.html');
|
|
315
|
+
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
|
+
}
|
|
322
|
+
]));
|
|
323
|
+
const html = `<html><script id="__NEXT_DATA__" type="application/json">${JSON.stringify({
|
|
324
|
+
props: {
|
|
325
|
+
pageProps: {
|
|
326
|
+
models: [
|
|
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
|
+
}
|
|
336
|
+
})}</script></html>`;
|
|
337
|
+
writeFileSync(sp, html);
|
|
338
|
+
const report = await runPriceDriftWatch({
|
|
339
|
+
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
|
+
]
|
|
350
|
+
});
|
|
351
|
+
assert.equal(report.providers['mockprov'].status, 'unchanged', 'snapshot 与 baseline 同价 → unchanged(offline)');
|
|
352
|
+
ok('snapshot 路径:__NEXT_DATA__ 内嵌 JSON dump 解析成功 + 与 baseline 同 → unchanged');
|
|
353
|
+
} finally{
|
|
354
|
+
rmSync(root, {
|
|
355
|
+
recursive: true,
|
|
356
|
+
force: true
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
async function testSnapshotFallbackDrift() {
|
|
361
|
+
const root = tmpRoot();
|
|
362
|
+
try {
|
|
363
|
+
const bp = join(root, 'b.json');
|
|
364
|
+
const sp = join(root, 'snapshot.html');
|
|
365
|
+
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
|
+
}
|
|
372
|
+
]));
|
|
373
|
+
const html = '<html><body><p>gpt-4o … $5.00 / 1M input … $20.00 / 1M output</p></body></html>';
|
|
374
|
+
writeFileSync(sp, html);
|
|
375
|
+
const report = await runPriceDriftWatch({
|
|
376
|
+
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
|
+
]
|
|
387
|
+
});
|
|
388
|
+
const r = report.providers['mockprov'];
|
|
389
|
+
assert.equal(r.status, 'drift', 'snapshot fallback 解析 + baseline 不一致 → drift');
|
|
390
|
+
const drift = r.drift ?? [];
|
|
391
|
+
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');
|
|
392
|
+
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
|
+
ok('snapshot fallback:段落文字解析 + 与 baseline 不同 → drift(含 up/down/new/removed)');
|
|
394
|
+
} finally{
|
|
395
|
+
rmSync(root, {
|
|
396
|
+
recursive: true,
|
|
397
|
+
force: true
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function testSnapshotMissing() {
|
|
402
|
+
const root = tmpRoot();
|
|
403
|
+
try {
|
|
404
|
+
const bp = join(root, 'b.json');
|
|
405
|
+
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
|
+
}
|
|
412
|
+
]));
|
|
413
|
+
const report = await runPriceDriftWatch({
|
|
414
|
+
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
|
+
]
|
|
425
|
+
});
|
|
426
|
+
assert.equal(report.providers['mockprov'].status, 'skipped_no_snapshot', 'snapshot 缺失 → SKIP');
|
|
427
|
+
ok('snapshot 缺失 → skipped_no_snapshot(offline)');
|
|
428
|
+
} finally{
|
|
429
|
+
rmSync(root, {
|
|
430
|
+
recursive: true,
|
|
431
|
+
force: true
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
await testSnapshotNextDataParser();
|
|
436
|
+
await testSnapshotFallbackDrift();
|
|
437
|
+
await testSnapshotMissing();
|
|
438
|
+
console.log(`price-drift-watch tests: ${passed} 组断言全绿(offline,fetch 模式手动/CI 心跳)`);
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/price-drift-tests.ts
|