@danceiny/gotry 0.0.1-rc.13 → 0.0.1-rc.15

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.
@@ -0,0 +1,187 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { BOOKING_SAGA_SCHEMA, SAGA_EDGES, SAGA_REJECTIONS, SAGA_STATUSES, SAGA_TRIGGER_EVENT, resolveSagaTrigger, sagaLegalPaths, sagaTraceViolations } from '../src/booking-saga.js';
5
+ import { ensureLedger } from '../src/state-ledger.js';
6
+ let pass = 0;
7
+ let fail = 0;
8
+ function assert(cond, msg) {
9
+ if (cond) {
10
+ pass++;
11
+ console.log(` ok - ${msg}`);
12
+ } else {
13
+ fail++;
14
+ console.error(` FAIL - ${msg}`);
15
+ }
16
+ }
17
+ function parseReceipt(payload) {
18
+ try {
19
+ const doc = JSON.parse(payload);
20
+ return typeof doc.receipt === 'string' && doc.receipt !== '' ? doc.receipt : undefined;
21
+ } catch {
22
+ return undefined;
23
+ }
24
+ }
25
+ const TRIGGERS = [
26
+ 'propose',
27
+ 'confirm',
28
+ 'compensate'
29
+ ];
30
+ const ORIGINS = [
31
+ 'none',
32
+ ...SAGA_STATUSES
33
+ ];
34
+ const COMBOS = ORIGINS.flatMap((from)=>TRIGGERS.map((trigger)=>`${from}:${trigger}`));
35
+ const edgeKeys = new Set(SAGA_EDGES.map((e)=>`${e.from}:${e.trigger}`));
36
+ const rejectKeys = new Set(SAGA_REJECTIONS.map((e)=>`${e.from}:${e.trigger}`));
37
+ assert(BOOKING_SAGA_SCHEMA === 'booking_saga_fsm.v1' && SAGA_STATUSES.join(',') === 'pending,confirmed,compensated', 'schema 词唯一;状态字母表与 pending_writes CHECK 逐字一致');
38
+ assert(COMBOS.length === 12 && new Set(COMBOS).size === 12 && edgeKeys.size === SAGA_EDGES.length && rejectKeys.size === SAGA_REJECTIONS.length, '12 个 (origin,trigger) 组合;边表与拒绝表内部无重复');
39
+ assert(COMBOS.every((k)=>edgeKeys.has(k) !== rejectKeys.has(k)), '每格恰为一条合法边或一条结构化拒绝(全函数,无空洞无第三态)');
40
+ assert(SAGA_EDGES.every((e)=>SAGA_STATUSES.includes(e.to) && e.event === SAGA_TRIGGER_EVENT[e.trigger] && e.guard.length > 0), '合法边:终点在字母表内、事件 kind 与触发器一一对应、守卫语义齐全');
41
+ const allConsistent = ORIGINS.every((from)=>TRIGGERS.every((trigger)=>{
42
+ const v = resolveSagaTrigger(from, trigger);
43
+ const edge = SAGA_EDGES.find((e)=>e.from === from && e.trigger === trigger);
44
+ if (edge) return v.ok && v.to === edge.to && v.event === edge.event;
45
+ return !v.ok && v.reason === SAGA_REJECTIONS.find((x)=>x.from === from && x.trigger === trigger).reason;
46
+ }));
47
+ assert(allConsistent, 'resolveSagaTrigger 十二格与边表/拒绝表全一致(解析器无平行第二逻辑)');
48
+ {
49
+ const vProp = resolveSagaTrigger('none', 'propose');
50
+ const vConf = resolveSagaTrigger('pending', 'confirm');
51
+ const vComp = resolveSagaTrigger('confirmed', 'compensate');
52
+ assert(vProp.ok && vProp.to === 'pending' && vConf.ok && vConf.to === 'confirmed' && vComp.ok && vComp.to === 'compensated', '主路径 ∅→pending→confirmed→compensated 连通(L2 登记只不执行 / L3 确认携 receipt / saga 补偿)');
53
+ const vDouble = resolveSagaTrigger('confirmed', 'confirm');
54
+ assert(!vDouble.ok && vDouble.reason === 'already-confirmed', '二次确认结构化拒绝(already-confirmed)');
55
+ const vIdem = resolveSagaTrigger('compensated', 'propose');
56
+ const vRevive = resolveSagaTrigger('compensated', 'confirm');
57
+ const vReComp = resolveSagaTrigger('compensated', 'compensate');
58
+ assert(!vIdem.ok && vIdem.reason === 'idem-exists' && !vRevive.ok && vRevive.reason === 'absorbed-compensated' && !vReComp.ok && vReComp.reason === 'already-compensated', 'compensated 吸收态:propose=幂等存在,confirm/compensate 拒绝(无复活路径)');
59
+ assert(sagaLegalPaths().length === 4, '合法路径恰四条(主路径及其真前缀)');
60
+ const legalClean = sagaLegalPaths().every((path)=>{
61
+ const events = path.map((trigger, i)=>({
62
+ seq: i + 1,
63
+ kind: SAGA_TRIGGER_EVENT[trigger],
64
+ receipt: trigger === 'confirm' ? 'PNR-X' : undefined
65
+ }));
66
+ return sagaTraceViolations(events).length === 0;
67
+ });
68
+ assert(legalClean, '四条合法路径的审计链均零违例');
69
+ const flagOf = (events)=>sagaTraceViolations(events)[0];
70
+ assert(flagOf([
71
+ {
72
+ kind: 'write.confirmed',
73
+ receipt: 'PNR-A'
74
+ }
75
+ ]) !== undefined, '缺 pending 起点的 confirm 被审计链抓出');
76
+ assert(flagOf([
77
+ {
78
+ kind: 'write.pending'
79
+ },
80
+ {
81
+ kind: 'write.confirmed',
82
+ receipt: 'PNR-A'
83
+ },
84
+ {
85
+ kind: 'write.confirmed',
86
+ receipt: 'PNR-B'
87
+ }
88
+ ]) !== undefined, '双确认被审计链抓出(每键至多确认一次)');
89
+ assert(flagOf([
90
+ {
91
+ kind: 'write.pending'
92
+ },
93
+ {
94
+ kind: 'write.compensated'
95
+ },
96
+ {
97
+ kind: 'write.confirmed',
98
+ receipt: 'X'
99
+ }
100
+ ]) !== undefined, 'compensated 之后不得再有 saga 事件(吸收态)');
101
+ assert((flagOf([
102
+ {
103
+ kind: 'write.pending'
104
+ },
105
+ {
106
+ kind: 'write.confirmed'
107
+ }
108
+ ]) ?? '').includes('receipt'), 'write.confirmed 空 receipt 被抓(L3 必携外部回执)');
109
+ }const root = mkdtempSync(join(tmpdir(), 'gotry-booking-saga-'));
110
+ try {
111
+ const ledger = ensureLedger(root);
112
+ const sagaEventsOf = (l, key)=>{
113
+ const rows = l.db.prepare(`SELECT seq, kind, payload FROM events WHERE tenant_id = ? AND subject_id = ? AND kind IN ('write.pending','write.confirmed','write.compensated') ORDER BY seq`).all(l.tenant, key);
114
+ return rows.map((r)=>({
115
+ seq: r.seq,
116
+ kind: r.kind,
117
+ receipt: parseReceipt(r.payload)
118
+ }));
119
+ };
120
+ const K1 = 'booking:K1';
121
+ const proposed = ledger.requestPendingWrite({
122
+ idemKey: K1,
123
+ seam: 'flight-order-confirm',
124
+ payload: {
125
+ flight: 'MU123'
126
+ }
127
+ });
128
+ assert(proposed.created === true && proposed.status === 'pending' && resolveSagaTrigger('none', 'propose').to === 'pending', '物理提议 ∅→pending 与状态机同归宿(L2 只登记不执行)');
129
+ const dup = ledger.requestPendingWrite({
130
+ idemKey: K1,
131
+ seam: 'flight-order-confirm',
132
+ payload: {
133
+ flight: 'MU123'
134
+ }
135
+ });
136
+ assert(dup.created === false && dup.status === 'pending' && resolveSagaTrigger('pending', 'propose').reason === 'idem-exists', '同幂等键重复提议:账本 no-op = 状态机 idem-exists');
137
+ const confirmed = ledger.confirmPendingWrite(K1, 'PNR-A');
138
+ assert(confirmed.ok === true && confirmed.status === 'confirmed' && resolveSagaTrigger('pending', 'confirm').to === 'confirmed', 'L3 具名确认 pending→confirmed(携 receipt)');
139
+ const reConfirmed = ledger.confirmPendingWrite(K1, 'PNR-SHOULD-IGNORE');
140
+ assert(reConfirmed.ok === false && (ledger.listPendingWrites().find((w)=>w.idem_key === K1)?.receipt ?? null) === 'PNR-A' && resolveSagaTrigger('confirmed', 'confirm').reason === 'already-confirmed', '二次确认被账本守卫拒绝且 receipt 不可变(仍为 PNR-A)');
141
+ const compensated = ledger.compensatePendingWrite(K1, '用户改签退款');
142
+ assert(compensated.ok === true && ledger.listPendingWrites().find((w)=>w.idem_key === K1)?.status === 'compensated', '已确认副作用可 saga 补偿(pending|confirmed → compensated)');
143
+ const afterConfirm = ledger.confirmPendingWrite(K1, 'PNR-POST');
144
+ const afterCompensate = ledger.compensatePendingWrite(K1, '重复补偿');
145
+ const afterPropose = ledger.requestPendingWrite({
146
+ idemKey: K1,
147
+ seam: 'flight-order-confirm',
148
+ payload: {}
149
+ });
150
+ assert(afterConfirm.ok === false && afterCompensate.ok === false && afterPropose.created === false, '吸收态:compensated 后 confirm/compensate/propose 全拒,零复活');
151
+ const audit = sagaEventsOf(ledger, K1);
152
+ assert(audit.map((e)=>e.kind).join(',') === 'write.pending,write.confirmed,write.compensated', '审计事件链与边表路径同构(被拒调用零事件)');
153
+ assert(sagaTraceViolations(audit).length === 0, '真实账本审计链过词汇层校验零违例');
154
+ const K3 = 'booking:K3';
155
+ ledger.requestPendingWrite({
156
+ idemKey: K3,
157
+ seam: 'hotel-order-confirm',
158
+ payload: {}
159
+ });
160
+ const emptyReceipt = ledger.confirmPendingWrite(K3, '');
161
+ assert(emptyReceipt.ok === true && sagaTraceViolations(sagaEventsOf(ledger, K3)).some((v)=>v.includes('receipt')), '已知边界:账本物理层暂不挡空 receipt,词汇层审计链抓出(物理 CHECK 随 M5 拍板入 schema)');
162
+ const other = ensureLedger(root, 'tenant-b');
163
+ const crossProp = other.requestPendingWrite({
164
+ idemKey: K1,
165
+ seam: 'flight-order-confirm',
166
+ payload: {}
167
+ });
168
+ assert(crossProp.created === true, '同 idem_key 跨租户互不可见:tenant-b 可建自己的 K1(tenant_id 一等字段,ADR-16)');
169
+ const LOCAL_ONLY = 'booking:local-only';
170
+ ledger.requestPendingWrite({
171
+ idemKey: LOCAL_ONLY,
172
+ seam: 'flight-order-confirm',
173
+ payload: {}
174
+ });
175
+ const crossMissing = other.confirmPendingWrite(LOCAL_ONLY, 'PNR-CROSS');
176
+ assert(crossMissing.ok === false && crossMissing.status === 'missing', 'tenant-b 看不到 local 的 saga 键(拒绝 missing)');
177
+ } finally{
178
+ rmSync(root, {
179
+ recursive: true,
180
+ force: true
181
+ });
182
+ }
183
+ console.log(`\nBOOKING SAGA TESTS: ${pass} ok, ${fail} fail`);
184
+ if (fail > 0) process.exit(1);
185
+
186
+
187
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/booking-saga-tests.ts
@@ -0,0 +1,66 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { join } from 'node:path';
4
+ const repoRoot = join(import.meta.dirname, '..', '..');
5
+ const bootstrap = join(repoRoot, 'bin', 'gotry-bootstrap.js');
6
+ function runBootstrap(extraArgs, extraEnv) {
7
+ try {
8
+ const out = execFileSync('node', [
9
+ bootstrap,
10
+ ...extraArgs
11
+ ], {
12
+ encoding: 'utf-8',
13
+ timeout: 60_000,
14
+ env: {
15
+ ...process.env,
16
+ ...extraEnv
17
+ }
18
+ });
19
+ return {
20
+ code: 0,
21
+ out
22
+ };
23
+ } catch (e) {
24
+ const err = e;
25
+ return {
26
+ code: err.status ?? 1,
27
+ out: err.stdout ?? ''
28
+ };
29
+ }
30
+ }
31
+ const c1 = runBootstrap([
32
+ '--check-only'
33
+ ], {});
34
+ assert.equal(c1.code, 0, `--check-only 应 exit 0,实际 ${c1.code}\n${c1.out}`);
35
+ assert.ok(c1.out.includes('hbcli'), '报告应含 hbcli 节');
36
+ assert.ok(c1.out.includes('agent-reach'), '报告应含 agent-reach 节');
37
+ assert.ok(c1.out.includes('flyai'), '报告应含 flyai(无需安装)节');
38
+ assert.ok(c1.out.includes('dsh-better-sidebar'), '报告应含 dsh-better-sidebar 节(issue #25 产物查看面)');
39
+ console.log('1. --check-only 探测报告 exit 0(hbcli/agent-reach/flyai/dsh-better-sidebar 四节齐)OK');
40
+ const c2 = runBootstrap([
41
+ '--auto'
42
+ ], {
43
+ GOTRY_SETUP_SKIP: '1'
44
+ });
45
+ assert.equal(c2.code, 0, '--auto 跳过态应 exit 0(永不挡 npm install)');
46
+ assert.ok(c2.out.includes('跳过'), '应输出跳过说明');
47
+ console.log('2. --auto + GOTRY_SETUP_SKIP=1 → exit 0(安装永不失败)OK');
48
+ const c3 = runBootstrap([], {
49
+ GOTRY_SETUP_SKIP: '1'
50
+ });
51
+ assert.equal(c3.code, 0, '显式模式跳过态应 exit 0');
52
+ assert.ok(c3.out.includes('跳过'), '应输出跳过说明');
53
+ console.log('3. 显式模式 + GOTRY_SETUP_SKIP=1 → exit 0 OK');
54
+ const c4 = runBootstrap([
55
+ '--check-only'
56
+ ], {
57
+ GOTRY_SETUP_SIDEBAR: '0'
58
+ });
59
+ assert.equal(c4.code, 0, '单项跳过态应 exit 0');
60
+ assert.ok(c4.out.includes('GOTRY_SETUP_SIDEBAR=0 跳过'), '应输出侧栏单项跳过说明');
61
+ assert.ok(c4.out.includes('hbcli'), '其余节不受单项开关影响');
62
+ console.log('4. GOTRY_SETUP_SIDEBAR=0 单项跳过(其余节照常)OK');
63
+ console.log('BOOTSTRAP TESTS: 4/4 OK(探测报告/跳过开关/单项开关/postinstall 非致命)');
64
+
65
+
66
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/bootstrap-tests.ts
@@ -0,0 +1,95 @@
1
+ import assert from 'node:assert/strict';
2
+ import { writeFile, mkdtemp, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { flyaiSearch } from '../capabilities/flyai.js';
6
+ const tmp = await mkdtemp(join(tmpdir(), 'flyai-test-'));
7
+ async function fakeCli(name, code, payload) {
8
+ const p = join(tmp, name);
9
+ await writeFile(p, `#!/bin/sh\necho '${payload}'\nexit ${code}\n`, {
10
+ mode: 0o755
11
+ });
12
+ return p;
13
+ }
14
+ const base = {
15
+ kind: 'flight',
16
+ origin: '上海',
17
+ destination: '丽江',
18
+ depDate: '2026-10-01'
19
+ };
20
+ const sentinelBin = await fakeCli('flyai-sentinel', 0, '{"message":"SentinelBlockException: flow control"}');
21
+ const s = await flyaiSearch({
22
+ ...base,
23
+ cliBin: sentinelBin,
24
+ timeoutMs: 5000
25
+ });
26
+ assert.equal(s.ok, false, 'Sentinel 形状应 ok=false');
27
+ assert.equal(s.verdict, 'error', `Sentinel 形状应判 error,实际 ${s.verdict}`);
28
+ assert.match(s.error ?? '', /sentinel/i, `error 应保留 sentinel 字样(供上层限流识别),实际 ${s.error}`);
29
+ assert.match(s.evidence, /\[实时API:flyai@error@/, 'error 证据链标注');
30
+ console.log('1. Sentinel 非业务形状 → error(非静默 miss)OK');
31
+ const missBin = await fakeCli('flyai-miss', 0, '{"data":{"itemList":[]}}');
32
+ const m = await flyaiSearch({
33
+ ...base,
34
+ cliBin: missBin,
35
+ timeoutMs: 5000
36
+ });
37
+ assert.equal(m.ok, true, '业务空形状 ok=true');
38
+ assert.equal(m.verdict, 'miss', `空 itemList 应判 miss,实际 ${m.verdict}`);
39
+ assert.match(m.evidence, /0\/0 flight options/, 'miss 证据链 0/0');
40
+ console.log('2. 业务空形状 → miss(0/0)OK');
41
+ const hitPayload = JSON.stringify({
42
+ data: {
43
+ itemList: [
44
+ {
45
+ journeys: [
46
+ {
47
+ segments: [
48
+ {
49
+ marketingTransportNo: '9C6617',
50
+ marketingTransportName: '吉祥航空',
51
+ depDateTime: '2026-10-01 07:55',
52
+ arrDateTime: '2026-10-01 11:20',
53
+ depStationName: '浦东T2',
54
+ arrStationName: '丽江三义',
55
+ duration: 205
56
+ }
57
+ ]
58
+ }
59
+ ],
60
+ ticketPrice: '580',
61
+ jumpUrl: 'https://www.fliggy.com/demo'
62
+ }
63
+ ]
64
+ }
65
+ });
66
+ const hitBin = await fakeCli('flyai-hit', 0, hitPayload);
67
+ const h = await flyaiSearch({
68
+ ...base,
69
+ cliBin: hitBin,
70
+ timeoutMs: 5000
71
+ });
72
+ assert.equal(h.verdict, 'hit', `业务条目应判 hit,实际 ${h.verdict}`);
73
+ assert.equal(h.options?.length, 1, '1 个选项');
74
+ assert.equal(h.options[0].no, '9C6617');
75
+ assert.equal(h.options[0].price, 580, '价格数值解析');
76
+ assert.equal(h.options[0].depStation, '浦东T2');
77
+ assert.match(h.evidence, /1\/1 flight options/, 'hit 证据链 1/1');
78
+ console.log('3. 业务命中 → hit(9C6617 ¥580)OK');
79
+ const failBin = await fakeCli('flyai-fail', 1, '');
80
+ const f = await flyaiSearch({
81
+ ...base,
82
+ cliBin: failBin,
83
+ timeoutMs: 5000
84
+ });
85
+ assert.equal(f.ok, false);
86
+ assert.equal(f.verdict, 'error', '非零退出应判 error');
87
+ console.log('4. 非零退出 → error OK');
88
+ await rm(tmp, {
89
+ recursive: true,
90
+ force: true
91
+ });
92
+ console.log('FLYAI TESTS: 4/4 OK(离线假 CLI:Sentinel→error / 空 itemList→miss / 命中→hit / exit≠0→error)');
93
+
94
+
95
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/flyai-tests.ts
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
2
2
  import { writeFile, mkdtemp, rm } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { join } from 'node:path';
5
- import { callHbcliJson, searchHotels } from '../capabilities/hbcli.js';
5
+ import { callHbcliJson, searchHotels, hbcliBinCandidates } from '../capabilities/hbcli.js';
6
6
  const tmp = await mkdtemp(join(tmpdir(), 'hbcli-test-'));
7
7
  async function fakeBin(name, code, payload) {
8
8
  const p = join(tmp, name);
@@ -112,11 +112,36 @@ const r5 = await searchHotels({
112
112
  });
113
113
  assert.equal(r5.hotels ?? null, null, '无目的地命中时 hotels 为 null(不整包倾倒)');
114
114
  assert.match(r5.summary, /无「巴黎」住宿数据/, 'summary 明示静态包无该目的地');
115
- await rm(tmp, {
115
+ const r6 = await searchHotels({
116
+ destination: '普吉'
117
+ }, {
118
+ hbcliBin: '/nope/hbcli',
119
+ fallbackPath: fallback
120
+ });
121
+ assert.equal(r6.via, 'hbcli-error', 'no-binary path');
122
+ assert.ok(r6.summary.includes('未安装 hbcli'), `summary 应人话化 ENOENT,实际 ${r6.summary}`);
123
+ assert.ok(r6.summary.includes('gotry setup'), '应带 gotry setup 安装指引');
124
+ assert.ok(r6.summary.includes('降级到静态包'), '人话化后仍指明降级到静态包');
125
+ console.log(`6. ENOENT 人话化 OK:${r6.summary}`);
126
+ {
127
+ const home = '/home/t';
128
+ assert.deepEqual(hbcliBinCandidates('hbcli', home), [
129
+ 'hbcli',
130
+ '/home/t/.local/bin/hbcli',
131
+ '/home/t/.staicli/current/hbcli'
132
+ ], '默认名应带安装位候选');
133
+ assert.deepEqual(hbcliBinCandidates('hbcli-not-on-path', home), [
134
+ 'hbcli-not-on-path'
135
+ ], '自定义名单候选(测试确定性)');
136
+ assert.deepEqual(hbcliBinCandidates('/nope/hbcli', home), [
137
+ '/nope/hbcli'
138
+ ], '绝对路径单候选');
139
+ console.log('7. hbcliBinCandidates(~/.local/bin + ~/.staicli/current 回退)OK');
140
+ }await rm(tmp, {
116
141
  recursive: true,
117
142
  force: true
118
143
  });
119
- console.log('HBCLI TESTS: 6/6 OK (happy / error / no-binary / fallback-filter / fallback-no-match / v0.3.0 旗标回归)');
144
+ console.log('HBCLI TESTS: 8/8 OK (happy / error / no-binary / fallback-filter / fallback-no-match / v0.3.0 旗标回归 / ENOENT 人话化 / 候选路径)');
120
145
 
121
146
 
122
147
  //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/hbcli-tests.ts
@@ -0,0 +1,123 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, mkdtempSync, rmSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { buildNightlyRunRecord, loadPriceTable, loadPromptSet, priceRunCost, runNightlyEvidence } from './nightly-evidence.js';
6
+ import { parseNightlyRun } from './product-metrics.js';
7
+ let passed = 0;
8
+ function ok(label) {
9
+ passed += 1;
10
+ console.log(` ok ${label}`);
11
+ }
12
+ const priceTable = loadPriceTable('data/llm-price-table.json');
13
+ const promptSet = loadPromptSet('data/m3-nightly-prompts.json');
14
+ assert.equal(priceTable.schema_version, 'gotry_llm_price_table_v1');
15
+ assert.equal(promptSet.schema_version, 'gotry_m3_nightly_prompt_set_v1');
16
+ assert.ok(promptSet.turns.length >= 1);
17
+ ok('封存数据文件 schema 与轮次');
18
+ for (const [name, entry] of Object.entries(priceTable.prices)){
19
+ assert.ok(entry.input_cache_hit_usd_per_1m_peak <= entry.input_cache_miss_usd_per_1m_peak, `${name} hit<=miss`);
20
+ assert.ok(entry.output_usd_per_1m_offpeak <= entry.output_usd_per_1m_peak, `${name} offpeak<=peak`);
21
+ }
22
+ ok('价目保守上界不变量(hit<=miss, offpeak<=peak)');
23
+ const baseUsage = {
24
+ calls: 3,
25
+ inputTokens: 1_000_000,
26
+ outputTokens: 1_000_000,
27
+ inputCacheHitTokens: 0,
28
+ inputCacheMissTokens: 1_000_000,
29
+ responsesMissingUsage: 0
30
+ };
31
+ const cost = priceRunCost('deepseek-chat', baseUsage, priceTable);
32
+ assert.equal(cost, 1.76);
33
+ ok('peak miss 换算 0.44+1.32=1.76(经 alias)');
34
+ const hitUsage = {
35
+ ...baseUsage,
36
+ inputCacheHitTokens: 1_000_000,
37
+ inputCacheMissTokens: 0
38
+ };
39
+ assert.equal(priceRunCost('deepseek-chat', hitUsage, priceTable), 1.334);
40
+ ok('peak hit 换算 0.014+1.32=1.334');
41
+ assert.throws(()=>priceRunCost('unknown-model-xyz', baseUsage, priceTable), /no entry for model/);
42
+ const dangling = {
43
+ ...priceTable,
44
+ aliases: {
45
+ 'deepseek-chat': 'not-a-model'
46
+ }
47
+ };
48
+ assert.throws(()=>priceRunCost('deepseek-chat', baseUsage, dangling), /has no entry/);
49
+ assert.throws(()=>priceRunCost('deepseek-chat', {
50
+ ...baseUsage,
51
+ responsesMissingUsage: 1
52
+ }, priceTable), /cost unprovable/);
53
+ ok('未知模型/悬空alias/usage缺失 fail-closed');
54
+ const executeAt = '2026-08-29T12:00:00.000Z';
55
+ const recordInput = {
56
+ schema_version: 'gotry_m3_nightly_run_v1',
57
+ executed_at: executeAt,
58
+ real_llm: true,
59
+ prompt_set_sha256: 'a'.repeat(64),
60
+ output_sha256: 'b'.repeat(64),
61
+ cost_usd: 0.25
62
+ };
63
+ const rec1 = buildNightlyRunRecord(recordInput);
64
+ const rec2 = buildNightlyRunRecord({
65
+ ...recordInput
66
+ });
67
+ assert.equal(rec1.run_key, rec2.run_key);
68
+ assert.match(rec1.run_key, /^hmac-sha256:[0-9a-f]{64}$/);
69
+ parseNightlyRun(JSON.parse(JSON.stringify(rec1)), 0);
70
+ const rec3 = buildNightlyRunRecord({
71
+ ...recordInput,
72
+ cost_usd: 0.26
73
+ });
74
+ assert.notEqual(rec3.run_key, rec1.run_key);
75
+ ok('run_key 确定性 + 已过消费方 parseNightlyRun');
76
+ const prevKey = process.env['LLM_API_KEY'];
77
+ const prevDeepseekKey = process.env['DEEPSEEK_API_KEY'];
78
+ const waitParent = mkdtempSync(join(tmpdir(), 'nightly-wait-'));
79
+ const waitRoot = join(waitParent, 'evidence', 'm3');
80
+ try {
81
+ delete process.env['LLM_API_KEY'];
82
+ delete process.env['DEEPSEEK_API_KEY'];
83
+ const waiting = await runNightlyEvidence({
84
+ evidenceRoot: waitRoot,
85
+ dryRun: false,
86
+ clock: ()=>new Date(executeAt)
87
+ });
88
+ assert.equal(waiting.state, 'waiting_external_evidence');
89
+ assert.equal(waiting.record, null);
90
+ assert.ok(waiting.reason?.includes('backoff'));
91
+ assert.equal(existsSync(waitRoot), false, 'waiting 不得创建任何证据目录');
92
+ } finally{
93
+ if (prevKey !== undefined) process.env['LLM_API_KEY'] = prevKey;
94
+ if (prevDeepseekKey !== undefined) process.env['DEEPSEEK_API_KEY'] = prevDeepseekKey;
95
+ rmSync(waitParent, {
96
+ recursive: true,
97
+ force: true
98
+ });
99
+ }
100
+ ok('无凭证 → waiting_external_evidence 且零写入');
101
+ const dryParent = mkdtempSync(join(tmpdir(), 'nightly-dry-'));
102
+ const dryRoot = join(dryParent, 'evidence', 'm3');
103
+ try {
104
+ const dry = await runNightlyEvidence({
105
+ evidenceRoot: dryRoot,
106
+ dryRun: true,
107
+ clock: ()=>new Date(executeAt)
108
+ });
109
+ assert.equal(dry.state, 'dry_run');
110
+ assert.equal(dry.record?.real_llm, false);
111
+ parseNightlyRun(JSON.parse(JSON.stringify(dry.record)), 0);
112
+ assert.equal(existsSync(dryRoot), false, 'dry-run 不得写任何证据文件');
113
+ } finally{
114
+ rmSync(dryParent, {
115
+ recursive: true,
116
+ force: true
117
+ });
118
+ }
119
+ ok('dry-run mock 全链演练 + real_llm=false + 零落盘');
120
+ console.log(`nightly-evidence tests: ${passed} 组断言全绿(offline,真跑不在 CI)`);
121
+
122
+
123
+ //# sourceURL=/Users/bytedance/work/gotry/ts/scripts/nightly-evidence-tests.ts