@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,474 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { backoffDelayMs, CircuitBreaker, withRetry } from '../capabilities/resilience.js';
|
|
6
|
+
import { declinedObservation, makeMockInterpreter, makeProductionInterpreter, __resetEffectBreakersForTest } from '../capabilities/effect.js';
|
|
7
|
+
const sleep0 = async ()=>{};
|
|
8
|
+
let clock = 1_000_000;
|
|
9
|
+
const now = ()=>clock;
|
|
10
|
+
const interp = makeProductionInterpreter({
|
|
11
|
+
sleep: sleep0,
|
|
12
|
+
now
|
|
13
|
+
});
|
|
14
|
+
const badItp = await interp({
|
|
15
|
+
effect: 'CTRIP_DIRECT_QUERY',
|
|
16
|
+
params: {}
|
|
17
|
+
});
|
|
18
|
+
assert.equal(badItp.result, null, '未登记效应 result=null');
|
|
19
|
+
assert.equal(badItp.trace.declined, 'unknown-effect');
|
|
20
|
+
assert.equal(badItp.trace.attempts, 0, '未登记效应零执行');
|
|
21
|
+
const badObs = declinedObservation('CTRIP_DIRECT_QUERY', badItp.trace);
|
|
22
|
+
assert.equal(badObs.ok, false);
|
|
23
|
+
assert.match(badObs.summary, /未登记效应/, '拒绝面人话 summary');
|
|
24
|
+
assert.match(badObs.evidence, /\[效应:CTRIP_DIRECT_QUERY@/, '拒绝面带证据链');
|
|
25
|
+
console.log('1. 注册表封闭性:未登记效应 → 结构化拒绝面(不抛错)OK');
|
|
26
|
+
assert.equal(backoffDelayMs({
|
|
27
|
+
baseDelayMs: 500,
|
|
28
|
+
maxDelayMs: 8000
|
|
29
|
+
}, 1), 500);
|
|
30
|
+
assert.equal(backoffDelayMs({
|
|
31
|
+
baseDelayMs: 500,
|
|
32
|
+
maxDelayMs: 8000
|
|
33
|
+
}, 2), 1000);
|
|
34
|
+
assert.equal(backoffDelayMs({
|
|
35
|
+
baseDelayMs: 500,
|
|
36
|
+
maxDelayMs: 8000
|
|
37
|
+
}, 4), 4000);
|
|
38
|
+
assert.equal(backoffDelayMs({
|
|
39
|
+
baseDelayMs: 500,
|
|
40
|
+
maxDelayMs: 2000
|
|
41
|
+
}, 6), 2000, '封顶');
|
|
42
|
+
const retried = [];
|
|
43
|
+
const rt = await withRetry(async ()=>({
|
|
44
|
+
ok: false
|
|
45
|
+
}), {
|
|
46
|
+
maxAttempts: 4,
|
|
47
|
+
baseDelayMs: 500,
|
|
48
|
+
maxDelayMs: 2000,
|
|
49
|
+
isRetryable: ()=>true,
|
|
50
|
+
sleep: sleep0
|
|
51
|
+
}, (_failN, delayMs)=>retried.push(delayMs));
|
|
52
|
+
assert.deepEqual(retried, [
|
|
53
|
+
500,
|
|
54
|
+
1000,
|
|
55
|
+
2000
|
|
56
|
+
], '退避序列 500/1000/2000');
|
|
57
|
+
assert.equal(rt.attempts, 4, '重试到上限');
|
|
58
|
+
assert.equal(rt.backoffMs, 3500, '累计回退记账');
|
|
59
|
+
const noRetry = await withRetry(async ()=>({
|
|
60
|
+
ok: false,
|
|
61
|
+
verdict: 'error',
|
|
62
|
+
error: '{"message":"SentinelBlockException"}'
|
|
63
|
+
}), {
|
|
64
|
+
maxAttempts: 3,
|
|
65
|
+
baseDelayMs: 500,
|
|
66
|
+
maxDelayMs: 2000,
|
|
67
|
+
isRetryable: ()=>false,
|
|
68
|
+
sleep: sleep0
|
|
69
|
+
});
|
|
70
|
+
assert.equal(noRetry.attempts, 1, '瞬时判定说不重试就 1 次');
|
|
71
|
+
console.log('2. 指数退避(封顶链/累计记账/非瞬时止步)OK');
|
|
72
|
+
const br = new CircuitBreaker({
|
|
73
|
+
failureThreshold: 2,
|
|
74
|
+
openMs: 60_000,
|
|
75
|
+
now
|
|
76
|
+
});
|
|
77
|
+
assert.equal(br.canAttempt().state, 'closed');
|
|
78
|
+
br.onFailure();
|
|
79
|
+
br.onFailure();
|
|
80
|
+
assert.equal(br.state(), 'open', '连续 2 失败 → open');
|
|
81
|
+
assert.equal(br.canAttempt().allowed, false, 'open 中拒绝');
|
|
82
|
+
clock += 61_000;
|
|
83
|
+
assert.equal(br.state(), 'half-open', '冷却满 → half-open');
|
|
84
|
+
const gate = br.canAttempt();
|
|
85
|
+
assert.equal(gate.allowed, true, 'half-open 放行单探测');
|
|
86
|
+
assert.equal(br.canAttempt().allowed, false, '探测在途拒并发');
|
|
87
|
+
br.onSuccess();
|
|
88
|
+
assert.equal(br.state(), 'closed', '探测成功 → closed');
|
|
89
|
+
br.onFailure();
|
|
90
|
+
br.onFailure();
|
|
91
|
+
clock += 61_000;
|
|
92
|
+
assert.equal(br.state(), 'half-open');
|
|
93
|
+
assert.equal(br.canAttempt().allowed, true);
|
|
94
|
+
br.onFailure();
|
|
95
|
+
assert.equal(br.state(), 'open', '探测失败 → 重新 open(冷却重启)');
|
|
96
|
+
console.log('3. 断路器三态(closed→open→half-open 单探测→双向收敛)OK');
|
|
97
|
+
let flaky = 0;
|
|
98
|
+
const ip = makeProductionInterpreter({
|
|
99
|
+
sleep: sleep0,
|
|
100
|
+
breakers: new Map(),
|
|
101
|
+
handlers: {
|
|
102
|
+
WEATHER_FORECAST: async ()=>{
|
|
103
|
+
flaky += 1;
|
|
104
|
+
return flaky < 2 ? {
|
|
105
|
+
ok: false,
|
|
106
|
+
via: 'open-meteo-error',
|
|
107
|
+
evidence: '[实时API:open-meteo@error]',
|
|
108
|
+
latencyMs: 1,
|
|
109
|
+
error: 'socket hang up'
|
|
110
|
+
} : {
|
|
111
|
+
ok: true,
|
|
112
|
+
via: 'open-meteo',
|
|
113
|
+
evidence: '[实时API:open-meteo@ts]',
|
|
114
|
+
latencyMs: 2,
|
|
115
|
+
daily: []
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
const itp4 = await ip({
|
|
121
|
+
effect: 'WEATHER_FORECAST',
|
|
122
|
+
params: {
|
|
123
|
+
latitude: 25.6,
|
|
124
|
+
longitude: 100.2
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
assert.equal(itp4.trace.attempts, 2, '瞬时失败重试第 2 次成功(免费源上限 2)');
|
|
128
|
+
assert.equal(itp4.trace.backoffMs, 400, '回退按 API 策略记账');
|
|
129
|
+
assert.ok(itp4.result.ok, '成功 observation 原样透传');
|
|
130
|
+
assert.match(itp4.trace.evidence[0] ?? '', /attempts=2 backoff=400ms breaker=closed/, 'trace 证据行');
|
|
131
|
+
console.log('4. 生产解译器×瞬时失败:重试后成功 + trace 记账 OK');
|
|
132
|
+
const sentinelResult = {
|
|
133
|
+
ok: false,
|
|
134
|
+
via: 'flyai-error',
|
|
135
|
+
verdict: 'error',
|
|
136
|
+
kind: 'flight',
|
|
137
|
+
latencyMs: 5,
|
|
138
|
+
evidence: '[实时API:flyai@error@ts] parse failed(SentinelBlockException)',
|
|
139
|
+
error: 'failed to parse flyai output as JSON (incomplete FlyAI JSON object): {"message":"SentinelBlockException"}'
|
|
140
|
+
};
|
|
141
|
+
let sentinelCalls = 0;
|
|
142
|
+
const flyaiBreakers = new Map();
|
|
143
|
+
const ipS = makeProductionInterpreter({
|
|
144
|
+
sleep: sleep0,
|
|
145
|
+
breakers: flyaiBreakers,
|
|
146
|
+
now,
|
|
147
|
+
handlers: {
|
|
148
|
+
FLYAI_SEARCH: async ()=>{
|
|
149
|
+
sentinelCalls += 1;
|
|
150
|
+
return structuredClone(sentinelResult);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
const q = {
|
|
155
|
+
kind: 'flight',
|
|
156
|
+
origin: '上海',
|
|
157
|
+
destination: '丽江',
|
|
158
|
+
depDate: '2026-10-01'
|
|
159
|
+
};
|
|
160
|
+
await ipS({
|
|
161
|
+
effect: 'FLYAI_SEARCH',
|
|
162
|
+
params: q
|
|
163
|
+
});
|
|
164
|
+
await ipS({
|
|
165
|
+
effect: 'FLYAI_SEARCH',
|
|
166
|
+
params: q
|
|
167
|
+
});
|
|
168
|
+
assert.equal(sentinelCalls, 2, '每次调用只打一次上游(Sentinel 不重试)');
|
|
169
|
+
const itp3 = await ipS({
|
|
170
|
+
effect: 'FLYAI_SEARCH',
|
|
171
|
+
params: q
|
|
172
|
+
});
|
|
173
|
+
assert.equal(itp3.trace.attempts, 1);
|
|
174
|
+
assert.equal(itp3.trace.breaker, 'open', '连续 3 次失败(FlyAI 阈值 3)→ 断路器开启');
|
|
175
|
+
assert.equal(sentinelCalls, 3);
|
|
176
|
+
const itp4th = await ipS({
|
|
177
|
+
effect: 'FLYAI_SEARCH',
|
|
178
|
+
params: q
|
|
179
|
+
});
|
|
180
|
+
assert.equal(itp4th.trace.declined, 'circuit-open', '第 4 次:熔断中零执行成本拒绝');
|
|
181
|
+
assert.equal(itp4th.result, null);
|
|
182
|
+
assert.equal(sentinelCalls, 3, '熔断后不再打上游');
|
|
183
|
+
console.log('5. FlyAI×Sentinel:永不重试 + 连环失败触发熔断保护配额 OK');
|
|
184
|
+
const declined = declinedObservation('FLYAI_SEARCH', itp4th.trace);
|
|
185
|
+
assert.equal(declined.ok, false);
|
|
186
|
+
assert.match(declined.summary, /断路器开启/, '拒绝面指引「不要立即重试」,不伪装成 miss');
|
|
187
|
+
clock += 120_000;
|
|
188
|
+
const ipOk = makeProductionInterpreter({
|
|
189
|
+
sleep: sleep0,
|
|
190
|
+
breakers: flyaiBreakers,
|
|
191
|
+
now,
|
|
192
|
+
handlers: {
|
|
193
|
+
FLYAI_SEARCH: async ()=>({
|
|
194
|
+
ok: true,
|
|
195
|
+
via: 'flyai',
|
|
196
|
+
verdict: 'hit',
|
|
197
|
+
kind: 'flight',
|
|
198
|
+
latencyMs: 1,
|
|
199
|
+
evidence: 'e',
|
|
200
|
+
options: []
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
const itp6 = await ipOk({
|
|
205
|
+
effect: 'FLYAI_SEARCH',
|
|
206
|
+
params: {
|
|
207
|
+
kind: 'flight'
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
assert.equal(itp6.trace.breaker, 'closed', 'half-open 单探测成功 → closed 恢复');
|
|
211
|
+
assert.ok(itp6.result != null && !itp6.trace.declined);
|
|
212
|
+
console.log('6. 断路拒绝面(结构化/零执行)+ 冷却后单探测恢复 closed OK');
|
|
213
|
+
const mock = makeMockInterpreter({
|
|
214
|
+
FLYAI_SEARCH: {
|
|
215
|
+
ok: true,
|
|
216
|
+
via: 'flyai',
|
|
217
|
+
verdict: 'hit',
|
|
218
|
+
kind: 'flight',
|
|
219
|
+
latencyMs: 0,
|
|
220
|
+
evidence: '[实时API:flyai@2026-08-29T00:00:00Z] 2/2 flight options',
|
|
221
|
+
options: [
|
|
222
|
+
{
|
|
223
|
+
no: '9C6617',
|
|
224
|
+
name: '吉祥航空',
|
|
225
|
+
depDateTime: '2026-10-01 07:55',
|
|
226
|
+
arrDateTime: '2026-10-01 11:20',
|
|
227
|
+
depStation: '浦东T2',
|
|
228
|
+
arrStation: '丽江三义',
|
|
229
|
+
durationMin: 205,
|
|
230
|
+
price: 580
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
const mockFx = {
|
|
236
|
+
effect: 'FLYAI_SEARCH',
|
|
237
|
+
params: {
|
|
238
|
+
kind: 'flight',
|
|
239
|
+
origin: '上海',
|
|
240
|
+
destination: '丽江',
|
|
241
|
+
depDate: '2026-10-01'
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
const mockA = await mock(mockFx);
|
|
245
|
+
const mockB = await mock(mockFx);
|
|
246
|
+
assert.deepEqual(mockA.result, mockB.result, '同一夹具两次回放同值');
|
|
247
|
+
assert.equal(mockA.result.verdict, 'hit');
|
|
248
|
+
assert.match(mockA.trace.evidence[0] ?? '', /\[效应:mock@.*夹具回放/, 'mock trace 标注');
|
|
249
|
+
const mockMiss = await mock({
|
|
250
|
+
effect: 'WEATHER_FORECAST',
|
|
251
|
+
params: {}
|
|
252
|
+
});
|
|
253
|
+
assert.equal(mockMiss.trace.declined, 'unknown-effect', 'mock 未登记夹具 = 拒绝面');
|
|
254
|
+
console.log('7. mock 解译器(夹具回放/未登记拒绝/确定性)OK');
|
|
255
|
+
let sessionCalls = 0;
|
|
256
|
+
const ipSess = makeProductionInterpreter({
|
|
257
|
+
sleep: sleep0,
|
|
258
|
+
breakers: new Map(),
|
|
259
|
+
handlers: {
|
|
260
|
+
SESSION_FLIGHT_SEARCH: async ()=>{
|
|
261
|
+
sessionCalls += 1;
|
|
262
|
+
return {
|
|
263
|
+
ok: false,
|
|
264
|
+
via: 'session-ctrip-flight-error',
|
|
265
|
+
verdict: 'challenged',
|
|
266
|
+
evidence: '[会话:ctrip-flight@error@ts] 风控命中',
|
|
267
|
+
latencyMs: 9,
|
|
268
|
+
error: 'challenge'
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
const sessQ = {
|
|
274
|
+
from: '上海',
|
|
275
|
+
to: '丽江',
|
|
276
|
+
date: '2026-10-01'
|
|
277
|
+
};
|
|
278
|
+
const itp8 = await ipSess({
|
|
279
|
+
effect: 'SESSION_FLIGHT_SEARCH',
|
|
280
|
+
params: sessQ
|
|
281
|
+
});
|
|
282
|
+
assert.equal(itp8.result.verdict, 'challenged', '渠道 verdict 原样透传');
|
|
283
|
+
assert.equal(itp8.trace.attempts, 1, 'SESSION 永不重试(挑战=红线)');
|
|
284
|
+
assert.equal(itp8.trace.breaker, 'off', 'SESSION 不参与断路器');
|
|
285
|
+
for(let i = 0; i < 5; i++)await ipSess({
|
|
286
|
+
effect: 'SESSION_FLIGHT_SEARCH',
|
|
287
|
+
params: sessQ
|
|
288
|
+
});
|
|
289
|
+
const itp8z = await ipSess({
|
|
290
|
+
effect: 'SESSION_FLIGHT_SEARCH',
|
|
291
|
+
params: sessQ
|
|
292
|
+
});
|
|
293
|
+
assert.ok(itp8z.trace.breaker === 'off' && !itp8z.trace.declined, 'SESSION 无断路拒绝(治理在节律闸与授权闸)');
|
|
294
|
+
assert.equal(sessionCalls, 7);
|
|
295
|
+
console.log('8. SESSION 通道策略(永不重试/不熔断/verdict 透传)OK');
|
|
296
|
+
__resetEffectBreakersForTest();
|
|
297
|
+
const tmp = await mkdtemp(join(tmpdir(), 'effect-test-'));
|
|
298
|
+
const itp9 = await interp({
|
|
299
|
+
effect: 'HBCLI_HOTEL_SEARCH',
|
|
300
|
+
params: {
|
|
301
|
+
destination: '大理',
|
|
302
|
+
hbcliBin: join(tmp, 'no-such-hbcli'),
|
|
303
|
+
fallbackPath: join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json')
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
const hb9 = itp9.result;
|
|
307
|
+
assert.ok(hb9 != null, '真实 handler 降级仍返回 observation');
|
|
308
|
+
assert.ok(typeof hb9.summary === 'string' && hb9.summary.length > 0, '降级带人话 summary');
|
|
309
|
+
assert.ok(itp9.trace.evidence[0]?.startsWith('[效应:HBCLI_HOTEL_SEARCH@'), '解译层证据行');
|
|
310
|
+
await rm(tmp, {
|
|
311
|
+
recursive: true,
|
|
312
|
+
force: true
|
|
313
|
+
});
|
|
314
|
+
console.log(`9. 真实 handler 离线冒烟(HBCLI 静态包降级,summary=${(hb9.summary ?? '').slice(0, 40)}…)OK`);
|
|
315
|
+
let ratesDispatches = 0;
|
|
316
|
+
const ipM0 = makeProductionInterpreter({
|
|
317
|
+
sleep: sleep0,
|
|
318
|
+
breakers: new Map(),
|
|
319
|
+
handlers: {
|
|
320
|
+
HBCLI_HOTEL_RATES: async ()=>{
|
|
321
|
+
ratesDispatches += 1;
|
|
322
|
+
return {
|
|
323
|
+
via: 'hbcli-realtime',
|
|
324
|
+
exitCode: 0,
|
|
325
|
+
result: {
|
|
326
|
+
rooms: []
|
|
327
|
+
},
|
|
328
|
+
evidence: '[实时API:hbcli@ts]',
|
|
329
|
+
latencyMs: 1,
|
|
330
|
+
rates: {
|
|
331
|
+
rooms: []
|
|
332
|
+
},
|
|
333
|
+
summary: 'ok'
|
|
334
|
+
};
|
|
335
|
+
},
|
|
336
|
+
HBCLI_CHECK_AVAIL: async ()=>({
|
|
337
|
+
via: 'hbcli-error',
|
|
338
|
+
exitCode: 1,
|
|
339
|
+
result: null,
|
|
340
|
+
evidence: '[实时API:hbcli@error@ts]',
|
|
341
|
+
latencyMs: 1,
|
|
342
|
+
error: 'boom',
|
|
343
|
+
avail: null,
|
|
344
|
+
summary: 'err'
|
|
345
|
+
})
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
const itp10 = await ipM0({
|
|
349
|
+
effect: 'HBCLI_HOTEL_RATES',
|
|
350
|
+
params: {
|
|
351
|
+
hotelId: '900000001',
|
|
352
|
+
checkIn: '2026-09-05',
|
|
353
|
+
checkOut: '2026-09-07',
|
|
354
|
+
adults: 2
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
assert.equal(itp10.trace.attempts, 1, 'RATES 永不重试(候选路径切换不是重试)');
|
|
358
|
+
assert.ok(itp10.result != null && !itp10.trace.declined, 'RATES 命中注入 handler,observation 透传');
|
|
359
|
+
assert.ok(itp10.trace.evidence[0]?.startsWith('[效应:HBCLI_HOTEL_RATES@'), '解译层证据行');
|
|
360
|
+
const itp10b = await ipM0({
|
|
361
|
+
effect: 'HBCLI_CHECK_AVAIL',
|
|
362
|
+
params: {
|
|
363
|
+
ratePkgId: 'rp-1'
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
assert.equal(itp10b.result?.via, 'hbcli-error', '渠道失败观察原样透传(平铺失败面,isFailure 计熔断)');
|
|
367
|
+
await ipM0({
|
|
368
|
+
effect: 'HBCLI_CHECK_AVAIL',
|
|
369
|
+
params: {
|
|
370
|
+
ratePkgId: 'rp-2'
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
await ipM0({
|
|
374
|
+
effect: 'HBCLI_CHECK_AVAIL',
|
|
375
|
+
params: {
|
|
376
|
+
ratePkgId: 'rp-3'
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
const itp10c = await ipM0({
|
|
380
|
+
effect: 'HBCLI_CHECK_AVAIL',
|
|
381
|
+
params: {
|
|
382
|
+
ratePkgId: 'rp-4'
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
assert.equal(itp10c.trace.declined, 'circuit-open', '3 连错熔断保护(同 HBCLI 族策略)');
|
|
386
|
+
assert.equal(ratesDispatches, 1, '成功通道不受失败通道熔断影响(per-effect 隔离)');
|
|
387
|
+
console.log('10a. 预订链读效应注册(派发/永不重试/熔断族/per-effect 隔离)OK');
|
|
388
|
+
__resetEffectBreakersForTest();
|
|
389
|
+
const tmp10 = await mkdtemp(join(tmpdir(), 'effect-m0-'));
|
|
390
|
+
const itp10d = await interp({
|
|
391
|
+
effect: 'HBCLI_HOTEL_RATES',
|
|
392
|
+
params: {
|
|
393
|
+
hotelId: '900000001',
|
|
394
|
+
hbcliBin: join(tmp10, 'no-such-hbcli')
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
const hr10 = itp10d.result;
|
|
398
|
+
assert.ok(hr10 != null, '不可达仍返回 observation(永不抛错)');
|
|
399
|
+
assert.equal(hr10.via, 'hbcli-error', '未伪装成功');
|
|
400
|
+
assert.match(hr10.summary ?? '', /不可用/, 'summary 明示不可用');
|
|
401
|
+
assert.ok(!/静态包/.test(hr10.summary ?? '') || /无静态降级/.test(hr10.summary ?? ''), '价格面不降级到静态包');
|
|
402
|
+
const itp10e = await interp({
|
|
403
|
+
effect: 'HBCLI_CHECK_AVAIL',
|
|
404
|
+
params: {
|
|
405
|
+
ratePkgId: 'rp-x',
|
|
406
|
+
hbcliBin: join(tmp10, 'no-such-hbcli')
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
assert.equal(itp10e.result?.via, 'hbcli-error', 'check-avail 同口径诚实失败');
|
|
410
|
+
await rm(tmp10, {
|
|
411
|
+
recursive: true,
|
|
412
|
+
force: true
|
|
413
|
+
});
|
|
414
|
+
console.log('10b. 真实 handler 离线冒烟(价格面 fail-closed,无静态降级)OK');
|
|
415
|
+
const mock10 = makeMockInterpreter({
|
|
416
|
+
HBCLI_HOTEL_RATES: {
|
|
417
|
+
via: 'hbcli-realtime',
|
|
418
|
+
exitCode: 0,
|
|
419
|
+
result: {
|
|
420
|
+
rooms: [
|
|
421
|
+
{
|
|
422
|
+
ratePkgId: 'rp-fx-1',
|
|
423
|
+
roomTypeName: 'Deluxe King'
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
},
|
|
427
|
+
evidence: '[实时API:hbcli@2026-08-30T00:00:00Z]',
|
|
428
|
+
latencyMs: 1800,
|
|
429
|
+
rates: {
|
|
430
|
+
rooms: [
|
|
431
|
+
{
|
|
432
|
+
ratePkgId: 'rp-fx-1',
|
|
433
|
+
roomTypeName: 'Deluxe King'
|
|
434
|
+
}
|
|
435
|
+
]
|
|
436
|
+
},
|
|
437
|
+
summary: 'hotel 900000001:hbcli 实时报价(2026-09-05 → 2026-09-07)'
|
|
438
|
+
},
|
|
439
|
+
HBCLI_CHECK_AVAIL: {
|
|
440
|
+
via: 'hbcli-realtime',
|
|
441
|
+
exitCode: 0,
|
|
442
|
+
result: {
|
|
443
|
+
ratePkgId: 'rp-fx-1',
|
|
444
|
+
avail: true
|
|
445
|
+
},
|
|
446
|
+
evidence: '[实时API:hbcli@2026-08-30T00:00:01Z]',
|
|
447
|
+
latencyMs: 900,
|
|
448
|
+
avail: {
|
|
449
|
+
ratePkgId: 'rp-fx-1',
|
|
450
|
+
avail: true
|
|
451
|
+
},
|
|
452
|
+
summary: 'ratePkg rp-fx-1:hbcli 实时验价(库存与价格以后端为准)'
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
const m10a = await mock10({
|
|
456
|
+
effect: 'HBCLI_HOTEL_RATES',
|
|
457
|
+
params: {
|
|
458
|
+
hotelId: '900000001'
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
const m10b = await mock10({
|
|
462
|
+
effect: 'HBCLI_CHECK_AVAIL',
|
|
463
|
+
params: {
|
|
464
|
+
ratePkgId: 'rp-fx-1'
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
assert.equal(m10a.result.rates?.rooms?.length, 1, 'RATES 夹具回放');
|
|
468
|
+
assert.equal(m10b.result.avail?.avail, true, 'CHECK_AVAIL 夹具回放');
|
|
469
|
+
assert.match(m10a.trace.evidence[0] ?? '', /\[效应:mock@.*夹具回放/, 'mock trace 标注');
|
|
470
|
+
console.log('10c. mock 解译器夹具回放(预订链 CI 形态)OK');
|
|
471
|
+
console.log('EFFECT INTERPRETER TESTS: 10/10 OK(effect_interpreter.v1:注册表封闭/退避链/断路三态/Sentinel 不重试/mock 夹具/SESSION 红线/真实降级/M0 预订链读效应,纯离线)');
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
//# sourceURL=/Users/bytedance/work/gotry-publish/ts/scripts/effect-tests.ts
|