@danceiny/gotry 0.0.1-rc.14 → 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 +26 -20
- package/README.zh-CN.md +27 -21
- package/bin/gotry-bootstrap.js +331 -3
- 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 +217 -0
- 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 +187 -0
- package/dist/scripts/bootstrap-tests.js +49 -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 +186 -0
- package/dist/scripts/nightly-evidence.js +289 -0
- 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 +159 -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 +153 -0
- 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 +31 -7
- package/dist/src/engine.js +1 -1
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +322 -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 +8 -2
- package/ts/capabilities/artifacts.ts +235 -0
- 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/booking-saga.ts +133 -0
- package/ts/src/dsh-llm.ts +40 -3
- package/ts/src/index.ts +230 -28
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { runOnboardingWizard, defaultExtensionDir, describeExtensionDir } from '../capabilities/session/wizard.js';
|
|
3
|
+
import { startExtensionHealthWatch, withAutoRetryOnExtension, HEALTH_WATCH_INTERVAL_FLOOR_MS, HEALTH_WATCH_INTERVAL_CEIL_MS } from '../capabilities/session/health-watch.js';
|
|
4
|
+
let passed = 0;
|
|
5
|
+
function check(label, fn) {
|
|
6
|
+
return Promise.resolve().then(fn).then(()=>{
|
|
7
|
+
passed += 1;
|
|
8
|
+
console.log(` ok - ${label}`);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
function noopSpawn() {
|
|
12
|
+
const { EventEmitter } = require('node:events');
|
|
13
|
+
const child = new EventEmitter();
|
|
14
|
+
const stream = new EventEmitter();
|
|
15
|
+
child.stdin = stream;
|
|
16
|
+
child.stdout = null;
|
|
17
|
+
child.stderr = null;
|
|
18
|
+
child.unref = ()=>undefined;
|
|
19
|
+
return child;
|
|
20
|
+
}
|
|
21
|
+
function captureSpawn() {
|
|
22
|
+
const calls = [];
|
|
23
|
+
const spawn = (bin, args = [])=>{
|
|
24
|
+
calls.push({
|
|
25
|
+
bin,
|
|
26
|
+
args
|
|
27
|
+
});
|
|
28
|
+
return noopSpawn();
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
spawn,
|
|
32
|
+
calls
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function asyncSleep(ms) {
|
|
36
|
+
if (ms <= 0) return;
|
|
37
|
+
await new Promise((resolve)=>setTimeout(resolve, ms));
|
|
38
|
+
}
|
|
39
|
+
console.log('ONBOARDING UX (§40)');
|
|
40
|
+
await check('1. wizard --dry-run 零网络零浏览器,exit 0,5 步齐全', async ()=>{
|
|
41
|
+
const { spawn } = captureSpawn();
|
|
42
|
+
const result = await runOnboardingWizard({
|
|
43
|
+
dryRun: true,
|
|
44
|
+
forcePlatform: 'darwin',
|
|
45
|
+
spawn
|
|
46
|
+
});
|
|
47
|
+
assert.equal(result.ok, true, `wizard ok 应 true,实际 ${JSON.stringify(result.steps)}`);
|
|
48
|
+
assert.equal(result.steps.length, 5, `应有 5 步,实际 ${result.steps.length}`);
|
|
49
|
+
const expected = [
|
|
50
|
+
'ensure-extension-files',
|
|
51
|
+
'open-chrome-extensions',
|
|
52
|
+
'clipboard-extension-path',
|
|
53
|
+
'panel-guide',
|
|
54
|
+
'watch-extension-ready'
|
|
55
|
+
];
|
|
56
|
+
for(let i = 0; i < expected.length; i++){
|
|
57
|
+
assert.equal(result.steps[i].step, expected[i]);
|
|
58
|
+
assert.equal(result.steps[i].status, 'skip', `dry-run 每步应为 skip,${result.steps[i].step} 实为 ${result.steps[i].status}`);
|
|
59
|
+
}
|
|
60
|
+
assert.equal(result.platform, 'darwin');
|
|
61
|
+
});
|
|
62
|
+
await check('2. health-watch 三时序:0ms / 中途 / 120s+1ms', async ()=>{
|
|
63
|
+
const w0 = startExtensionHealthWatch({
|
|
64
|
+
timeoutMs: 0,
|
|
65
|
+
intervalMs: 10
|
|
66
|
+
});
|
|
67
|
+
const o0 = await w0.waitReady();
|
|
68
|
+
assert.equal(o0.ready, false);
|
|
69
|
+
assert.equal(o0.reason, 'timeout');
|
|
70
|
+
let probeCalls = 0;
|
|
71
|
+
const wMid = startExtensionHealthWatch({
|
|
72
|
+
timeoutMs: 500,
|
|
73
|
+
intervalMs: 30,
|
|
74
|
+
probe: async ()=>{
|
|
75
|
+
probeCalls += 1;
|
|
76
|
+
return probeCalls >= 2;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
const oMid = await wMid.waitReady();
|
|
80
|
+
assert.equal(oMid.ready, true, `中途 ready 实为 ${JSON.stringify(oMid)}`);
|
|
81
|
+
assert.ok(oMid.attempts >= 2);
|
|
82
|
+
const wTimeout = startExtensionHealthWatch({
|
|
83
|
+
timeoutMs: 60,
|
|
84
|
+
intervalMs: 20,
|
|
85
|
+
probe: async ()=>false
|
|
86
|
+
});
|
|
87
|
+
const oTimeout = await wTimeout.waitReady();
|
|
88
|
+
assert.equal(oTimeout.ready, false);
|
|
89
|
+
assert.equal(oTimeout.reason, 'timeout');
|
|
90
|
+
});
|
|
91
|
+
await check('3. 剪贴板 + GUI 面板跨平台降级', async ()=>{
|
|
92
|
+
const darwin = captureSpawn();
|
|
93
|
+
const rDarwin = await runOnboardingWizard({
|
|
94
|
+
dryRun: false,
|
|
95
|
+
forcePlatform: 'darwin',
|
|
96
|
+
extensionDir: '/tmp/gotry-test-ext',
|
|
97
|
+
spawn: darwin.spawn,
|
|
98
|
+
sourceDir: '/'
|
|
99
|
+
});
|
|
100
|
+
const darwinBins = darwin.calls.map((c)=>c.bin);
|
|
101
|
+
assert.ok(darwinBins.includes('open'), `darwin 应调 open(实调 ${darwinBins.join(',')})`);
|
|
102
|
+
assert.ok(darwinBins.includes('pbcopy'), 'darwin 应调 pbcopy');
|
|
103
|
+
assert.ok(darwinBins.includes('osascript'), 'darwin 应调 osascript');
|
|
104
|
+
const linux = captureSpawn();
|
|
105
|
+
await runOnboardingWizard({
|
|
106
|
+
dryRun: false,
|
|
107
|
+
forcePlatform: 'linux',
|
|
108
|
+
extensionDir: '/tmp/gotry-test-ext',
|
|
109
|
+
spawn: linux.spawn,
|
|
110
|
+
sourceDir: '/'
|
|
111
|
+
});
|
|
112
|
+
const linuxBins = linux.calls.map((c)=>c.bin);
|
|
113
|
+
assert.ok(linuxBins.includes('xdg-open'), `linux 应调 xdg-open(实调 ${linuxBins.join(',')})`);
|
|
114
|
+
assert.ok(linuxBins.includes('xclip'), 'linux 应调 xclip');
|
|
115
|
+
assert.ok(linuxBins.includes('zenity'), 'linux 应调 zenity');
|
|
116
|
+
const headless = captureSpawn();
|
|
117
|
+
const rHeadless = await runOnboardingWizard({
|
|
118
|
+
dryRun: true,
|
|
119
|
+
forcePlatform: 'headless',
|
|
120
|
+
extensionDir: '/tmp/gotry-test-ext',
|
|
121
|
+
spawn: headless.spawn
|
|
122
|
+
});
|
|
123
|
+
assert.equal(headless.calls.length, 0, `headless 不应 spawn 任何命令(实调 ${headless.calls.length})`);
|
|
124
|
+
assert.equal(rHeadless.platform, 'headless');
|
|
125
|
+
});
|
|
126
|
+
await check('4. retry-after-watch:needs-extension → watch ready → 自动重放同 query_id', async ()=>{
|
|
127
|
+
let fetchCalls = 0;
|
|
128
|
+
const queryId = 'sf-01';
|
|
129
|
+
const fetch = async ()=>{
|
|
130
|
+
fetchCalls += 1;
|
|
131
|
+
return {
|
|
132
|
+
query_id: queryId,
|
|
133
|
+
verdict: 'needs-extension',
|
|
134
|
+
needsExt: fetchCalls === 1
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
const first = await fetch();
|
|
138
|
+
assert.equal(first.verdict, 'needs-extension');
|
|
139
|
+
let probeCount = 0;
|
|
140
|
+
const watch = startExtensionHealthWatch({
|
|
141
|
+
timeoutMs: 1_000,
|
|
142
|
+
intervalMs: 20,
|
|
143
|
+
probe: async ()=>{
|
|
144
|
+
probeCount += 1;
|
|
145
|
+
return probeCount >= 2;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const outcome = await watch.waitReady();
|
|
149
|
+
assert.equal(outcome.ready, true);
|
|
150
|
+
const retry = await fetch();
|
|
151
|
+
assert.equal(retry.query_id, queryId, '同 query_id 必须保持');
|
|
152
|
+
assert.equal(fetchCalls, 2, `应调用 fetch 两次(首次 + 重放),实 ${fetchCalls}`);
|
|
153
|
+
});
|
|
154
|
+
await check('5. wizard 无 GUI 走终端降级', async ()=>{
|
|
155
|
+
const headless = captureSpawn();
|
|
156
|
+
const result = await runOnboardingWizard({
|
|
157
|
+
dryRun: true,
|
|
158
|
+
forcePlatform: 'headless',
|
|
159
|
+
extensionDir: '/tmp/gotry-test-ext',
|
|
160
|
+
spawn: headless.spawn
|
|
161
|
+
});
|
|
162
|
+
const clip = result.steps.find((s)=>s.step === 'clipboard-extension-path');
|
|
163
|
+
const panel = result.steps.find((s)=>s.step === 'panel-guide');
|
|
164
|
+
const watch = result.steps.find((s)=>s.step === 'watch-extension-ready');
|
|
165
|
+
assert.equal(clip?.status, 'skip', 'headless 剪贴板应 skip');
|
|
166
|
+
assert.equal(panel?.status, 'skip', 'headless 面板应 skip(终端降级)');
|
|
167
|
+
assert.equal(watch?.status, 'skip', 'dry-run watch 应 skip');
|
|
168
|
+
assert.equal(headless.calls.length, 0, `headless 不应调任何 spawn(实调 ${headless.calls.length})`);
|
|
169
|
+
});
|
|
170
|
+
await check('6. withAutoRetryOnExtension 端到端:首次 needs-ext → 自动重放', async ()=>{
|
|
171
|
+
let callCount = 0;
|
|
172
|
+
let ready = false;
|
|
173
|
+
setTimeout(()=>{
|
|
174
|
+
ready = true;
|
|
175
|
+
}, 30);
|
|
176
|
+
const args = {
|
|
177
|
+
fetch: async ()=>{
|
|
178
|
+
callCount += 1;
|
|
179
|
+
return callCount === 1 ? {
|
|
180
|
+
needsExt: true
|
|
181
|
+
} : {
|
|
182
|
+
needsExt: false,
|
|
183
|
+
hit: true
|
|
184
|
+
};
|
|
185
|
+
},
|
|
186
|
+
isExtensionNeeded: (r)=>r.needsExt === true,
|
|
187
|
+
watch: {
|
|
188
|
+
timeoutMs: 1_000,
|
|
189
|
+
intervalMs: 20,
|
|
190
|
+
probe: async ()=>ready
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
const out = await withAutoRetryOnExtension(args);
|
|
194
|
+
assert.equal(out.retried, true, `应触发重放,实 retried=${out.retried}`);
|
|
195
|
+
assert.equal(out.attempts, 2);
|
|
196
|
+
assert.equal(out.result.needsExt, false);
|
|
197
|
+
assert.equal(out.result.hit, true);
|
|
198
|
+
assert.equal(callCount, 2, `withAutoRetry 应调 2 次 fetch,实 ${callCount}`);
|
|
199
|
+
});
|
|
200
|
+
await check('7. health-watch interval 边界常量与 watch 契约对齐', ()=>{
|
|
201
|
+
assert.ok(HEALTH_WATCH_INTERVAL_FLOOR_MS >= 1_000, '下界不应让 watch 风暴');
|
|
202
|
+
assert.ok(HEALTH_WATCH_INTERVAL_CEIL_MS <= 44_000, '上界应 < EXTENSION_CONNECTED_WINDOW_MS');
|
|
203
|
+
});
|
|
204
|
+
await check('8. defaultExtensionDir = ~/.gotry/extension(契约对齐 bootstrap 节)', ()=>{
|
|
205
|
+
const dir = defaultExtensionDir();
|
|
206
|
+
assert.match(dir, /\.gotry[\\/]extension$/, `扩展目录形态应是 ~/.gotry/extension,实 ${dir}`);
|
|
207
|
+
});
|
|
208
|
+
await check('9. describeExtensionDir:不存在的目录不抛错,返回 exists:false', ()=>{
|
|
209
|
+
const desc = describeExtensionDir('/nonexistent/zzz');
|
|
210
|
+
assert.equal(desc.exists, false);
|
|
211
|
+
assert.equal(desc.fileCount, 0);
|
|
212
|
+
assert.equal(desc.hasManifest, false);
|
|
213
|
+
});
|
|
214
|
+
await asyncSleep(0);
|
|
215
|
+
console.log(`ONBOARDING UX (§40): ${passed} pass`);
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/onboarding-tests.ts
|
|
@@ -17,4 +17,4 @@ const target = process.argv[2] ?? 'EK329';
|
|
|
17
17
|
console.log(await checkArrival(target, process.argv[3] ?? null));
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
//# sourceURL=/Users/bytedance/work/gotry/ts/scripts/opensky-check.ts
|
|
20
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/opensky-check.ts
|
|
@@ -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
|