@danceiny/gotry 0.0.1-rc.21 → 0.0.1-rc.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +102 -194
- package/README.zh-CN.md +115 -210
- package/bin/gotry-backend.js +53 -0
- package/bin/gotry-bootstrap.js +466 -35
- package/bin/gotry-inner.js +255 -36
- package/bin/gotry-process-liveness.d.ts +27 -0
- package/bin/gotry-process-liveness.js +97 -0
- package/bin/gotry-runtime-resolution.d.ts +1 -1
- package/bin/gotry-runtime-resolution.js +2 -2
- package/client/client.js +174 -0
- package/cordis.gotry-patch.yml +70 -93
- package/dist/capabilities/agent-reach-deep.js +45 -122
- package/dist/capabilities/agent-reach.js +63 -148
- package/dist/capabilities/anything.js +89 -75
- package/dist/capabilities/artifacts.js +123 -104
- package/dist/capabilities/channel-health.js +65 -45
- package/dist/capabilities/channel-registry.js +108 -87
- package/dist/capabilities/doctor.js +357 -235
- package/dist/capabilities/effect.js +223 -309
- package/dist/capabilities/fact-log.js +28 -14
- package/dist/capabilities/flyai.js +245 -156
- package/dist/capabilities/ground-transfer.js +509 -0
- package/dist/capabilities/hbcli.js +144 -148
- package/dist/capabilities/hotelbyte-transaction.js +194 -0
- package/dist/capabilities/incident-log.js +131 -61
- package/dist/capabilities/model-override.js +30 -10
- package/dist/capabilities/opensky.js +54 -43
- package/dist/capabilities/resilience.js +50 -32
- package/dist/capabilities/session/action-cache.js +50 -44
- package/dist/capabilities/session/adapters/ctrip-flight.js +119 -62
- package/dist/capabilities/session/adapters/ctrip-hotel.js +125 -110
- package/dist/capabilities/session/adapters/dida-portal.js +206 -0
- package/dist/capabilities/session/adapters/meituan-local.js +35 -54
- package/dist/capabilities/session/adapters/rail-12306.js +169 -112
- package/dist/capabilities/session/benchmark.js +85 -149
- package/dist/capabilities/session/extension-bridge.js +320 -259
- package/dist/capabilities/session/extension-channel.js +57 -76
- package/dist/capabilities/session/extension-distribution.js +115 -119
- package/dist/capabilities/session/extract.js +31 -21
- package/dist/capabilities/session/golden-score.js +55 -33
- package/dist/capabilities/session/health-watch.js +146 -154
- package/dist/capabilities/session/read-guard.js +55 -47
- package/dist/capabilities/session/static-flight-golden.js +49 -36
- package/dist/capabilities/session/transport.js +54 -61
- package/dist/capabilities/session/wizard.js +63 -93
- package/dist/capabilities/session-consent.js +143 -47
- package/dist/capabilities/session-login.js +95 -126
- package/dist/capabilities/session-search.js +391 -253
- package/dist/capabilities/sponsor-plugin.js +341 -0
- package/dist/capabilities/visa-policy.js +87 -73
- package/dist/capabilities/weather.js +163 -235
- package/dist/scripts/action-cache-tests.js +24 -59
- package/dist/scripts/agent-planning-turn-deadline-e2e.js +91 -142
- package/dist/scripts/agent-planning-turn-deadline-tests.js +80 -176
- package/dist/scripts/agent-reach-deep-tests.js +30 -40
- package/dist/scripts/agent-reach-tests.js +26 -18
- package/dist/scripts/agent-reach-wrapper-tests.js +46 -50
- package/dist/scripts/anything-tests.js +58 -62
- package/dist/scripts/artifact-client-contract-tests.js +69 -0
- package/dist/scripts/artifacts-capability-tests.js +235 -0
- package/dist/scripts/async-collect.js +26 -15
- package/dist/scripts/benchmark-environment-bridge-e2e.js +287 -684
- package/dist/scripts/benchmark-environment-bridge-tests.js +818 -3123
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +66 -279
- package/dist/scripts/booking-copilot-availability-policy-tests.js +153 -1257
- package/dist/scripts/booking-copilot-bin-proof-tests.js +22 -45
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +64 -76
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +69 -199
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +393 -759
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +33 -75
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +60 -110
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +28 -66
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +72 -271
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +73 -275
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +784 -3398
- package/dist/scripts/booking-copilot-server-proof-tests.js +63 -168
- package/dist/scripts/booking-copilot-startup-proof-tests.js +38 -124
- package/dist/scripts/booking-executor-tests.js +80 -0
- package/dist/scripts/booking-recovery-chain-tests.js +182 -0
- package/dist/scripts/booking-saga-tests.js +61 -123
- package/dist/scripts/booking-surface-contract-proof-tests.js +152 -1167
- package/dist/scripts/bootstrap-tests.js +1383 -128
- package/dist/scripts/build-changelog.js +89 -130
- package/dist/scripts/build-metrics-report.js +135 -143
- package/dist/scripts/changelog-tests.js +35 -82
- package/dist/scripts/channel-probe-tests.js +44 -116
- package/dist/scripts/channel-probe.js +84 -167
- package/dist/scripts/channel-registry-tests.js +97 -185
- package/dist/scripts/companion-tests.js +25 -70
- package/dist/scripts/dida-cookie-probe.js +87 -0
- package/dist/scripts/dida-sw-probe.js +114 -0
- package/dist/scripts/diff-test.js +17 -12
- package/dist/scripts/doctor-tests.js +428 -120
- package/dist/scripts/dsh-artifact-e2e.js +150 -0
- package/dist/scripts/dsh-artifact-web-e2e.js +443 -0
- package/dist/scripts/dsh-http-proxy-sse-proof.js +90 -0
- package/dist/scripts/dsh-runtime-closure-tests.js +110 -153
- package/dist/scripts/dsh-runtime-closure.js +75 -63
- package/dist/scripts/dsh-session-v3-migration-proof.js +106 -0
- package/dist/scripts/dsh-subprocess-local-proof.js +149 -0
- package/dist/scripts/dsh-target-closure-proof.js +178 -0
- package/dist/scripts/effect-tests.js +165 -552
- package/dist/scripts/engine-run.js +1 -3
- package/dist/scripts/engine-tests.js +16 -10
- package/dist/scripts/evaluation-cadence-tests.js +69 -242
- package/dist/scripts/evaluation-contract-tests.js +193 -499
- package/dist/scripts/extension-distribution-cli.js +19 -15
- package/dist/scripts/extension-distribution-tests.js +98 -211
- package/dist/scripts/extension-tests.js +366 -389
- package/dist/scripts/fact-gate-tests.js +725 -263
- package/dist/scripts/fixtures/extension-bridge-unref-child.mjs +21 -0
- package/dist/scripts/flight-hotel-anchor-363-registered-e2e.js +169 -0
- package/dist/scripts/flight-malformed-tests.js +159 -0
- package/dist/scripts/flyai-tests.js +325 -68
- package/dist/scripts/g5-guard-tests.js +131 -0
- package/dist/scripts/g5-guard.js +189 -0
- package/dist/scripts/gen-lunar-anchors.js +106 -0
- package/dist/scripts/gotry-backend-kernel-tests.js +79 -0
- package/dist/scripts/ground-transfer-tests.js +956 -0
- package/dist/scripts/hbcli-e2e-tests.js +71 -90
- package/dist/scripts/hbcli-live-optin-tests.js +96 -0
- package/dist/scripts/hbcli-release-contract-tests.js +122 -0
- package/dist/scripts/hbcli-tests.js +41 -115
- package/dist/scripts/health-watch-cli.js +42 -28
- package/dist/scripts/hotel-date-gate-tests.js +496 -0
- package/dist/scripts/hotelbyte-reconcile-tests.js +141 -0
- package/dist/scripts/hotelbyte-spawn-e2e-tests.js +373 -0
- package/dist/scripts/i18n-tests.js +16 -9
- package/dist/scripts/incident-tests.js +281 -87
- package/dist/scripts/issue-194-job-id-guard-tests.js +106 -0
- package/dist/scripts/issue-233-cancel-refund-commission-tests.js +392 -0
- package/dist/scripts/issue-271-liveness-tests.js +547 -0
- package/dist/scripts/issue-289-model-retry-real-tests.js +296 -0
- package/dist/scripts/issue-289-model-retry-tests.js +441 -0
- package/dist/scripts/issue-289-web-retry-e2e.js +630 -0
- package/dist/scripts/issue-338-home-city-e2e.js +577 -0
- package/dist/scripts/issue343-real-entry-e2e.js +160 -0
- package/dist/scripts/journey-tests.js +22 -27
- package/dist/scripts/kernel-manifest-evidence.js +109 -0
- package/dist/scripts/kernel-manifest-gate.js +126 -0
- package/dist/scripts/kernel-manifest-tests.js +212 -0
- package/dist/scripts/kernel-manifest-trace-entry.js +7 -0
- package/dist/scripts/kernel-manifest-trace-hook.mjs +28 -0
- package/dist/scripts/kernel-manifest.js +369 -0
- package/dist/scripts/ledger-repair-apply-tests.js +316 -0
- package/dist/scripts/ledger-repair-plan-tests.js +375 -0
- package/dist/scripts/ledger-tests.js +325 -340
- package/dist/scripts/ledger-workflow-crash.js +10 -20
- package/dist/scripts/map-tools-vendor-package-proof.js +206 -134
- package/dist/scripts/memory-capture-tests.js +242 -61
- package/dist/scripts/memory-decay-tests.js +18 -52
- package/dist/scripts/memory-lifecycle-tests.js +657 -0
- package/dist/scripts/memory-lifecycle.js +196 -0
- package/dist/scripts/memory-metrics.js +24 -12
- package/dist/scripts/memory-value-report.js +474 -222
- package/dist/scripts/memory-value-tests.js +328 -0
- package/dist/scripts/metrics-report-tests.js +62 -199
- package/dist/scripts/model-override-e2e.js +66 -105
- package/dist/scripts/nightly-evidence-tests.js +64 -89
- package/dist/scripts/nightly-evidence.js +108 -111
- package/dist/scripts/nudge-digest.js +39 -28
- package/dist/scripts/onboarding-tests.js +63 -88
- package/dist/scripts/opensky-check.js +18 -13
- package/dist/scripts/opensky-tests.js +28 -28
- package/dist/scripts/persona-surface-guard-tests.js +37 -12
- package/dist/scripts/pnpm-dsh-closure-proof.js +2 -5
- package/dist/scripts/policy-anchor-359-registered-e2e.js +214 -0
- package/dist/scripts/price-drift-tests.js +126 -227
- package/dist/scripts/price-drift-watch.js +209 -253
- package/dist/scripts/probe-poi-tests.js +52 -45
- package/dist/scripts/product-metrics.js +187 -273
- package/dist/scripts/publish-preverify.js +47 -41
- package/dist/scripts/realtime-pricing-tests.js +37 -76
- package/dist/scripts/replay-async.js +20 -18
- package/dist/scripts/replay-real.js +16 -18
- package/dist/scripts/replay.js +52 -82
- package/dist/scripts/session-attach-diagnose.js +16 -12
- package/dist/scripts/session-attach-poc.js +34 -37
- package/dist/scripts/session-benchmark.js +66 -147
- package/dist/scripts/session-dida-live-e2e.js +151 -0
- package/dist/scripts/session-extract-tests.js +14 -35
- package/dist/scripts/session-login.js +14 -13
- package/dist/scripts/session-tests.js +545 -583
- package/dist/scripts/sf-live-benchmark.js +92 -110
- package/dist/scripts/sf-live-cli-tests.js +2 -9
- package/dist/scripts/sf-soft-score-tests.js +19 -60
- package/dist/scripts/sf-summary-tests.js +149 -0
- package/dist/scripts/sf-summary.js +466 -84
- package/dist/scripts/skeleton-check.js +16 -14
- package/dist/scripts/skeleton-integration-test.js +11 -10
- package/dist/scripts/skills-contract-tests.js +51 -46
- package/dist/scripts/smoke-session-gate-tests.js +12 -20
- package/dist/scripts/smoke.js +541 -422
- package/dist/scripts/sponsor-reuse-tests.js +262 -0
- package/dist/scripts/state-cli-tests.js +226 -101
- package/dist/scripts/state-cli.js +549 -212
- package/dist/scripts/static-golden-tests.js +46 -180
- package/dist/scripts/time-eval-tests.js +210 -192
- package/dist/scripts/travel-timeline-tests.js +29 -93
- package/dist/scripts/turn-handoff-collect-tests.js +46 -93
- package/dist/scripts/turn-handoff-collect.js +71 -99
- package/dist/scripts/turn-policy-tests.js +28 -29
- package/dist/scripts/typed-contract-canary.js +77 -162
- package/dist/scripts/tz-resolver-sanity.js +50 -0
- package/dist/scripts/tz-resolver-tests.js +584 -0
- package/dist/scripts/unified-tests.js +23 -19
- package/dist/scripts/visa-policy-tests.js +31 -44
- package/dist/scripts/weather-tests.js +121 -543
- package/dist/scripts/wish-channel-gate-tests.js +29 -86
- package/dist/scripts/write-gate-tests.js +522 -0
- package/dist/scripts/z3-lifecycle-fault-tests.js +188 -0
- package/dist/scripts/z3-lifecycle-tests.js +170 -0
- package/dist/scripts/z3-race-repeat-tests.js +39 -0
- package/dist/scripts/z3-race-tests.js +18 -12
- package/dist/src/artifact-gate.js +652 -221
- package/dist/src/backend/kernel.js +91 -0
- package/dist/src/backend/modules/booking-copilot.js +55 -0
- package/dist/src/backend/modules/booking-executor.js +124 -0
- package/dist/src/backend/modules/session-search.js +194 -0
- package/dist/src/benchmark-agent-conformance.js +296 -262
- package/dist/src/benchmark-environment-bridge.js +298 -413
- package/dist/src/benchmark-headless-child-diagnostics.js +93 -100
- package/dist/src/benchmark-tool-isolation.js +94 -67
- package/dist/src/bookable-facts.js +299 -174
- package/dist/src/booking-saga.js +64 -120
- package/dist/src/booking-surface/availability-policy.js +443 -576
- package/dist/src/booking-surface/cancel-refund-commission.js +379 -0
- package/dist/src/booking-surface/contracts.js +24 -80
- package/dist/src/booking-surface/dsh-planner.js +510 -281
- package/dist/src/booking-surface/dsh-plugin.js +17 -1
- package/dist/src/booking-surface/error-codes.js +82 -10
- package/dist/src/booking-surface/index.js +0 -3
- package/dist/src/booking-surface/profile.js +10 -54
- package/dist/src/booking-surface/recovery-chain.js +367 -0
- package/dist/src/booking-surface/runtime.js +765 -891
- package/dist/src/booking-surface/server.js +200 -251
- package/dist/src/booking-surface/startup.js +58 -54
- package/dist/src/booking-surface/validation.js +150 -218
- package/dist/src/bridge.js +20 -13
- package/dist/src/companions.js +48 -74
- package/dist/src/contracts.js +21 -43
- package/dist/src/dsh-llm.js +82 -120
- package/dist/src/engine.js +253 -239
- package/dist/src/evaluation-cadence.js +72 -209
- package/dist/src/evaluation-contracts.js +220 -843
- package/dist/src/flight-pack-adapter.js +54 -0
- package/dist/src/flight-pack-contract.js +2 -0
- package/dist/src/gotry-backend.js +70 -0
- package/dist/src/hotel-date-gate.js +118 -0
- package/dist/src/i18n.js +18 -7
- package/dist/src/index.js +1425 -1629
- package/dist/src/journey.js +132 -81
- package/dist/src/ledger-repair-apply.js +538 -0
- package/dist/src/ledger-repair-plan.js +463 -0
- package/dist/src/loop.js +257 -166
- package/dist/src/memory-capture.js +180 -32
- package/dist/src/memory-decay.js +30 -14
- package/dist/src/memory-lifecycle.js +1310 -0
- package/dist/src/memory-utility.js +27 -40
- package/dist/src/mock-llm.js +36 -61
- package/dist/src/model.js +81 -36
- package/dist/src/realtime-pricing.js +63 -72
- package/dist/src/slot-spec.js +84 -106
- package/dist/src/state-ledger.js +310 -471
- package/dist/src/subagent-job-id-guard.js +58 -0
- package/dist/src/time-anchor.js +231 -33
- package/dist/src/tool-packet.js +20 -8
- package/dist/src/travel-slots.js +58 -35
- package/dist/src/travel-timeline.js +44 -63
- package/dist/src/turn-deadline.js +121 -127
- package/dist/src/turn-policy.js +61 -89
- package/dist/src/tz-resolver.js +143 -0
- package/dist/src/unified.js +504 -381
- package/dist/src/wish-pool.js +39 -14
- package/dist/src/write-gate.js +495 -0
- package/dist/src/z3-shared.js +273 -7
- package/extension/README.md +4 -3
- package/extension/background.js +125 -14
- package/extension/content-bridge.js +23 -1
- package/extension/content-main.js +15 -4
- package/extension/manifest.json +16 -8
- package/package.json +251 -220
- package/ts/capabilities/agent-reach.ts +4 -4
- package/ts/capabilities/anything.ts +1 -1
- package/ts/capabilities/artifacts.ts +46 -9
- package/ts/capabilities/flyai.ts +156 -37
- package/ts/capabilities/hbcli.ts +1 -1
- package/ts/capabilities/incident-log.ts +88 -59
- package/ts/capabilities/session/adapters/ctrip-flight.ts +99 -36
- package/ts/capabilities/session/adapters/dida-portal.ts +250 -0
- package/ts/capabilities/session/adapters/rail-12306.ts +123 -24
- package/ts/capabilities/session/extension-bridge.ts +226 -101
- package/ts/capabilities/session/extension-channel.ts +22 -9
- package/ts/capabilities/session/wizard.ts +2 -2
- package/ts/capabilities/session-consent.ts +98 -14
- package/ts/capabilities/session-login.ts +1 -0
- package/ts/capabilities/session-search.ts +315 -31
- package/ts/dsh-runtime/vendor/README.md +8 -4
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.en.md +7 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/README.md +5 -3
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/settings-ns.js +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/lib/types/settings-ns.d.ts +1 -1
- package/ts/dsh-runtime/vendor/dsh-map-tools/package.json +6 -6
- package/ts/package.json +236 -16
- package/ts/scripts/map-tools-vendor-package-proof.ts +152 -31
- package/ts/scripts/state-cli.ts +418 -51
- package/ts/src/artifact-gate.ts +561 -15
- package/ts/src/benchmark-environment-bridge.ts +37 -20
- package/ts/src/bookable-facts.ts +183 -7
- package/ts/src/booking-surface/cancel-refund-commission.ts +488 -0
- package/ts/src/booking-surface/dsh-planner.ts +240 -32
- package/ts/src/booking-surface/dsh-plugin.js +17 -1
- package/ts/src/booking-surface/error-codes.ts +75 -0
- package/ts/src/booking-surface/recovery-chain.ts +409 -0
- package/ts/src/booking-surface/server.ts +84 -51
- package/ts/src/booking-surface/validation.ts +2 -0
- package/ts/src/contracts.ts +12 -2
- package/ts/src/dsh-llm.ts +4 -8
- package/ts/src/index.ts +609 -90
- package/ts/src/loop.ts +116 -7
- package/ts/src/memory-capture.ts +227 -3
- package/ts/src/mock-llm.ts +4 -8
- package/ts/src/model.ts +88 -2
- package/ts/src/state-ledger.ts +47 -29
- package/ts/src/subagent-job-id-guard.ts +72 -0
- package/ts/src/time-anchor.ts +217 -13
- package/ts/src/unified.ts +316 -54
|
@@ -1,20 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 效应解译器测试(effect_interpreter.v1,issue #16 采纳,ADR-18;纯离线零网络):
|
|
3
|
+
* 1. 注册表封闭性:未登记效应 → 结构化拒绝面(不抛错,declined=unknown-effect)
|
|
4
|
+
* 2. 指数退避 withRetry:500→1000→2000 封顶链;累计记账;非瞬时失败不重试
|
|
5
|
+
* 3. 断路器三态:closed→(连续失败达阈)→open→(冷却)→half-open 单探测→成功回 closed/失败再开
|
|
6
|
+
* 4. 生产解译器×瞬时失败:重试后成功(trace.attempts/backoff 记账,evidence 行)
|
|
7
|
+
* 5. FlyAI×Sentinel:上游说不永不重试(attempts=1);连续失败计入熔断,熔断后零执行成本
|
|
8
|
+
* 6. 断路拒绝面:不执行、不抛错、平铺失败观察;冷却后 half-open 单探测成功 → closed
|
|
9
|
+
* 7. mock 解译器:夹具回放确定性;未登记夹具=结构化拒绝
|
|
10
|
+
* 8. SESSION 通道策略:永不重试、不熔断(风控红线),verdict 原样透传
|
|
11
|
+
* 9. 真实 handler 离线冒烟:hbcli 不可达 → 静态包降级(永不抛错,证据链非空)
|
|
12
|
+
* 11. HBCLI×timeout(2026-09-02 迪拜 session:冷启动建后端 session 超 30s):
|
|
13
|
+
* timeout 类失败重试 1 次即恢复;ENOENT/退码类仍永不重试(切换不是重试)
|
|
14
|
+
*
|
|
15
|
+
* 运行: cd ts && npx tsx scripts/effect-tests.ts
|
|
16
|
+
*/
|
|
1
17
|
import assert from 'node:assert/strict';
|
|
2
18
|
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
19
|
import { tmpdir } from 'node:os';
|
|
4
20
|
import { join } from 'node:path';
|
|
5
21
|
import { backoffDelayMs, CircuitBreaker, withRetry } from '../capabilities/resilience.js';
|
|
6
|
-
import { declinedObservation, makeMockInterpreter, makeProductionInterpreter, __resetEffectBreakersForTest } from '../capabilities/effect.js';
|
|
7
|
-
const sleep0 = async ()=>{};
|
|
22
|
+
import { declinedObservation, makeMockInterpreter, makeProductionInterpreter, __resetEffectBreakersForTest, } from '../capabilities/effect.js';
|
|
23
|
+
const sleep0 = async () => { }; // 回退即时放行(确定性,不真睡)
|
|
8
24
|
let clock = 1_000_000;
|
|
9
|
-
const now = ()=>clock;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
14
|
-
const badItp = await interp({
|
|
15
|
-
effect: 'CTRIP_DIRECT_QUERY',
|
|
16
|
-
params: {}
|
|
17
|
-
});
|
|
25
|
+
const now = () => clock;
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// 1. 注册表封闭性:未登记效应 → 结构化拒绝(不抛错)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const interp = makeProductionInterpreter({ sleep: sleep0, now });
|
|
30
|
+
const badItp = await interp({ effect: 'CTRIP_DIRECT_QUERY', params: {} });
|
|
18
31
|
assert.equal(badItp.result, null, '未登记效应 result=null');
|
|
19
32
|
assert.equal(badItp.trace.declined, 'unknown-effect');
|
|
20
33
|
assert.equal(badItp.trace.attempts, 0, '未登记效应零执行');
|
|
@@ -23,57 +36,25 @@ assert.equal(badObs.ok, false);
|
|
|
23
36
|
assert.match(badObs.summary, /未登记效应/, '拒绝面人话 summary');
|
|
24
37
|
assert.match(badObs.evidence, /\[效应:CTRIP_DIRECT_QUERY@/, '拒绝面带证据链');
|
|
25
38
|
console.log('1. 注册表封闭性:未登记效应 → 结构化拒绝面(不抛错)OK');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}, 1), 500);
|
|
30
|
-
assert.equal(backoffDelayMs({
|
|
31
|
-
|
|
32
|
-
|
|
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, '封顶');
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// 2. 指数退避:500→1000→2000 封顶;累计记账;非瞬时失败不重试
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
assert.equal(backoffDelayMs({ baseDelayMs: 500, maxDelayMs: 8000 }, 1), 500);
|
|
43
|
+
assert.equal(backoffDelayMs({ baseDelayMs: 500, maxDelayMs: 8000 }, 2), 1000);
|
|
44
|
+
assert.equal(backoffDelayMs({ baseDelayMs: 500, maxDelayMs: 8000 }, 4), 4000);
|
|
45
|
+
assert.equal(backoffDelayMs({ baseDelayMs: 500, maxDelayMs: 2000 }, 6), 2000, '封顶');
|
|
42
46
|
const retried = [];
|
|
43
|
-
const rt = await withRetry(async ()=>({
|
|
44
|
-
|
|
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');
|
|
47
|
+
const rt = await withRetry(async () => ({ ok: false }), { maxAttempts: 4, baseDelayMs: 500, maxDelayMs: 2000, isRetryable: () => true, sleep: sleep0 }, (_failN, delayMs) => retried.push(delayMs));
|
|
48
|
+
assert.deepEqual(retried, [500, 1000, 2000], '退避序列 500/1000/2000');
|
|
57
49
|
assert.equal(rt.attempts, 4, '重试到上限');
|
|
58
50
|
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
|
-
});
|
|
51
|
+
const noRetry = await withRetry(async () => ({ ok: false, verdict: 'error', error: '{"message":"SentinelBlockException"}' }), { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 2000, isRetryable: () => false, sleep: sleep0 });
|
|
70
52
|
assert.equal(noRetry.attempts, 1, '瞬时判定说不重试就 1 次');
|
|
71
53
|
console.log('2. 指数退避(封顶链/累计记账/非瞬时止步)OK');
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
});
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// 3. 断路器三态(时钟注入确定性)
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
const br = new CircuitBreaker({ failureThreshold: 2, openMs: 60_000, now });
|
|
77
58
|
assert.equal(br.canAttempt().state, 'closed');
|
|
78
59
|
br.onFailure();
|
|
79
60
|
br.onFailure();
|
|
@@ -94,49 +75,35 @@ assert.equal(br.canAttempt().allowed, true);
|
|
|
94
75
|
br.onFailure();
|
|
95
76
|
assert.equal(br.state(), 'open', '探测失败 → 重新 open(冷却重启)');
|
|
96
77
|
console.log('3. 断路器三态(closed→open→half-open 单探测→双向收敛)OK');
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// 4. 生产解译器×瞬时失败:重试后成功,trace 记账(WEATHER 免费源策略 2 次上限)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
97
81
|
let flaky = 0;
|
|
98
82
|
const ip = makeProductionInterpreter({
|
|
99
83
|
sleep: sleep0,
|
|
100
84
|
breakers: new Map(),
|
|
101
85
|
handlers: {
|
|
102
|
-
WEATHER_FORECAST: async ()=>{
|
|
86
|
+
WEATHER_FORECAST: async () => {
|
|
103
87
|
flaky += 1;
|
|
104
|
-
return flaky < 2
|
|
105
|
-
ok: false,
|
|
106
|
-
via: 'open-meteo-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
}
|
|
88
|
+
return flaky < 2
|
|
89
|
+
? { ok: false, via: 'open-meteo-error', evidence: '[实时API:open-meteo@error]', latencyMs: 1, error: 'socket hang up' }
|
|
90
|
+
: { ok: true, via: 'open-meteo', evidence: '[实时API:open-meteo@ts]', latencyMs: 2, daily: [] };
|
|
91
|
+
},
|
|
92
|
+
},
|
|
126
93
|
});
|
|
94
|
+
const itp4 = await ip({ effect: 'WEATHER_FORECAST', params: { latitude: 25.6, longitude: 100.2 } });
|
|
127
95
|
assert.equal(itp4.trace.attempts, 2, '瞬时失败重试第 2 次成功(免费源上限 2)');
|
|
128
96
|
assert.equal(itp4.trace.backoffMs, 400, '回退按 API 策略记账');
|
|
129
97
|
assert.ok(itp4.result.ok, '成功 observation 原样透传');
|
|
130
98
|
assert.match(itp4.trace.evidence[0] ?? '', /attempts=2 backoff=400ms breaker=closed/, 'trace 证据行');
|
|
131
99
|
console.log('4. 生产解译器×瞬时失败:重试后成功 + trace 记账 OK');
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// 5. FlyAI×Sentinel:不重试,但连续失败计入熔断
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
132
103
|
const sentinelResult = {
|
|
133
|
-
ok: false,
|
|
134
|
-
via: 'flyai-error',
|
|
135
|
-
verdict: 'error',
|
|
136
|
-
kind: 'flight',
|
|
137
|
-
latencyMs: 5,
|
|
104
|
+
ok: false, via: 'flyai-error', verdict: 'error', kind: 'flight', latencyMs: 5,
|
|
138
105
|
evidence: '[实时API:flyai@error@ts] parse failed(SentinelBlockException)',
|
|
139
|
-
error: 'failed to parse flyai output as JSON (incomplete FlyAI JSON object): {"message":"SentinelBlockException"}'
|
|
106
|
+
error: 'failed to parse flyai output as JSON (incomplete FlyAI JSON object): {"message":"SentinelBlockException"}',
|
|
140
107
|
};
|
|
141
108
|
let sentinelCalls = 0;
|
|
142
109
|
const flyaiBreakers = new Map();
|
|
@@ -144,364 +111,169 @@ const ipS = makeProductionInterpreter({
|
|
|
144
111
|
sleep: sleep0,
|
|
145
112
|
breakers: flyaiBreakers,
|
|
146
113
|
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
|
|
114
|
+
handlers: { FLYAI_SEARCH: async () => { sentinelCalls += 1; return structuredClone(sentinelResult); } },
|
|
167
115
|
});
|
|
116
|
+
const q = { kind: 'flight', origin: '上海', destination: '丽江', depDate: '2026-10-01' };
|
|
117
|
+
await ipS({ effect: 'FLYAI_SEARCH', params: q });
|
|
118
|
+
await ipS({ effect: 'FLYAI_SEARCH', params: q });
|
|
168
119
|
assert.equal(sentinelCalls, 2, '每次调用只打一次上游(Sentinel 不重试)');
|
|
169
|
-
const itp3 = await ipS({
|
|
170
|
-
effect: 'FLYAI_SEARCH',
|
|
171
|
-
params: q
|
|
172
|
-
});
|
|
120
|
+
const itp3 = await ipS({ effect: 'FLYAI_SEARCH', params: q });
|
|
173
121
|
assert.equal(itp3.trace.attempts, 1);
|
|
174
122
|
assert.equal(itp3.trace.breaker, 'open', '连续 3 次失败(FlyAI 阈值 3)→ 断路器开启');
|
|
175
123
|
assert.equal(sentinelCalls, 3);
|
|
176
|
-
const itp4th = await ipS({
|
|
177
|
-
effect: 'FLYAI_SEARCH',
|
|
178
|
-
params: q
|
|
179
|
-
});
|
|
124
|
+
const itp4th = await ipS({ effect: 'FLYAI_SEARCH', params: q });
|
|
180
125
|
assert.equal(itp4th.trace.declined, 'circuit-open', '第 4 次:熔断中零执行成本拒绝');
|
|
181
126
|
assert.equal(itp4th.result, null);
|
|
182
127
|
assert.equal(sentinelCalls, 3, '熔断后不再打上游');
|
|
183
128
|
console.log('5. FlyAI×Sentinel:永不重试 + 连环失败触发熔断保护配额 OK');
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// 6. 断路拒绝面 + 冷却后 half-open 单探测成功恢复 closed
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
184
132
|
const declined = declinedObservation('FLYAI_SEARCH', itp4th.trace);
|
|
185
133
|
assert.equal(declined.ok, false);
|
|
186
134
|
assert.match(declined.summary, /断路器开启/, '拒绝面指引「不要立即重试」,不伪装成 miss');
|
|
187
|
-
clock += 120_000;
|
|
135
|
+
clock += 120_000; // 冷却满(FlyAI 60s)→ half-open
|
|
188
136
|
const ipOk = makeProductionInterpreter({
|
|
189
|
-
sleep: sleep0,
|
|
190
|
-
|
|
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
|
-
}
|
|
137
|
+
sleep: sleep0, breakers: flyaiBreakers, now, // 复用同一断路器 Map——状态在 Map 里存活
|
|
138
|
+
handlers: { FLYAI_SEARCH: async () => ({ ok: true, via: 'flyai', verdict: 'hit', kind: 'flight', latencyMs: 1, evidence: 'e', options: [] }) },
|
|
209
139
|
});
|
|
140
|
+
const itp6 = await ipOk({ effect: 'FLYAI_SEARCH', params: { kind: 'flight' } });
|
|
210
141
|
assert.equal(itp6.trace.breaker, 'closed', 'half-open 单探测成功 → closed 恢复');
|
|
211
142
|
assert.ok(itp6.result != null && !itp6.trace.declined);
|
|
212
143
|
console.log('6. 断路拒绝面(结构化/零执行)+ 冷却后单探测恢复 closed OK');
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// 7. mock 解译器:夹具回放确定性(CI 无网)
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
213
147
|
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
|
-
}
|
|
148
|
+
FLYAI_SEARCH: { ok: true, via: 'flyai', verdict: 'hit', kind: 'flight', latencyMs: 0, evidence: '[实时API:flyai@2026-08-29T00:00:00Z] 2/2 flight options', options: [{ no: '9C6617', name: '吉祥航空', depDateTime: '2026-10-01 07:55', arrDateTime: '2026-10-01 11:20', depStation: '浦东T2', arrStation: '丽江三义', durationMin: 205, price: 580 }] },
|
|
234
149
|
});
|
|
235
|
-
const mockFx = {
|
|
236
|
-
effect: 'FLYAI_SEARCH',
|
|
237
|
-
params: {
|
|
238
|
-
kind: 'flight',
|
|
239
|
-
origin: '上海',
|
|
240
|
-
destination: '丽江',
|
|
241
|
-
depDate: '2026-10-01'
|
|
242
|
-
}
|
|
243
|
-
};
|
|
150
|
+
const mockFx = { effect: 'FLYAI_SEARCH', params: { kind: 'flight', origin: '上海', destination: '丽江', depDate: '2026-10-01' } };
|
|
244
151
|
const mockA = await mock(mockFx);
|
|
245
152
|
const mockB = await mock(mockFx);
|
|
246
153
|
assert.deepEqual(mockA.result, mockB.result, '同一夹具两次回放同值');
|
|
247
154
|
assert.equal(mockA.result.verdict, 'hit');
|
|
248
155
|
assert.match(mockA.trace.evidence[0] ?? '', /\[效应:mock@.*夹具回放/, 'mock trace 标注');
|
|
249
|
-
const mockMiss = await mock({
|
|
250
|
-
effect: 'WEATHER_FORECAST',
|
|
251
|
-
params: {}
|
|
252
|
-
});
|
|
156
|
+
const mockMiss = await mock({ effect: 'WEATHER_FORECAST', params: {} });
|
|
253
157
|
assert.equal(mockMiss.trace.declined, 'unknown-effect', 'mock 未登记夹具 = 拒绝面');
|
|
254
158
|
console.log('7. mock 解译器(夹具回放/未登记拒绝/确定性)OK');
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// 8. SESSION 通道策略:永不重试、不熔断(风控红线),一次调用一次透传
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
255
162
|
let sessionCalls = 0;
|
|
256
163
|
const ipSess = makeProductionInterpreter({
|
|
257
164
|
sleep: sleep0,
|
|
258
165
|
breakers: new Map(),
|
|
259
166
|
handlers: {
|
|
260
|
-
SESSION_FLIGHT_SEARCH: async ()=>{
|
|
167
|
+
SESSION_FLIGHT_SEARCH: async () => {
|
|
261
168
|
sessionCalls += 1;
|
|
262
|
-
return {
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
169
|
+
return { ok: false, via: 'session-ctrip-flight-error', verdict: 'challenged', evidence: '[会话:ctrip-flight@error@ts] 风控命中', latencyMs: 9, error: 'challenge' };
|
|
170
|
+
},
|
|
171
|
+
},
|
|
281
172
|
});
|
|
173
|
+
const sessQ = { from: '上海', to: '丽江', date: '2026-10-01' };
|
|
174
|
+
const itp8 = await ipSess({ effect: 'SESSION_FLIGHT_SEARCH', params: sessQ });
|
|
282
175
|
assert.equal(itp8.result.verdict, 'challenged', '渠道 verdict 原样透传');
|
|
283
176
|
assert.equal(itp8.trace.attempts, 1, 'SESSION 永不重试(挑战=红线)');
|
|
284
177
|
assert.equal(itp8.trace.breaker, 'off', 'SESSION 不参与断路器');
|
|
285
|
-
for(let i = 0; i < 5; i++)
|
|
286
|
-
effect: 'SESSION_FLIGHT_SEARCH',
|
|
287
|
-
|
|
288
|
-
});
|
|
289
|
-
const itp8z = await ipSess({
|
|
290
|
-
effect: 'SESSION_FLIGHT_SEARCH',
|
|
291
|
-
params: sessQ
|
|
292
|
-
});
|
|
178
|
+
for (let i = 0; i < 5; i++)
|
|
179
|
+
await ipSess({ effect: 'SESSION_FLIGHT_SEARCH', params: sessQ });
|
|
180
|
+
const itp8z = await ipSess({ effect: 'SESSION_FLIGHT_SEARCH', params: sessQ });
|
|
293
181
|
assert.ok(itp8z.trace.breaker === 'off' && !itp8z.trace.declined, 'SESSION 无断路拒绝(治理在节律闸与授权闸)');
|
|
294
182
|
assert.equal(sessionCalls, 7);
|
|
295
183
|
console.log('8. SESSION 通道策略(永不重试/不熔断/verdict 透传)OK');
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// 9. 真实 handler 离线冒烟:hbcli 显式不可达 → 静态包降级(永不抛错,零网络)
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
296
187
|
__resetEffectBreakersForTest();
|
|
297
188
|
const tmp = await mkdtemp(join(tmpdir(), 'effect-test-'));
|
|
298
189
|
const itp9 = await interp({
|
|
299
190
|
effect: 'HBCLI_HOTEL_SEARCH',
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
hbcliBin: join(tmp, 'no-such-hbcli'),
|
|
303
|
-
fallbackPath: join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json')
|
|
304
|
-
}
|
|
191
|
+
// 显式不存在的自定义 bin:hbcliBinCandidates 对自定义名不回退已知安装位 → ENOENT → 静态包降级
|
|
192
|
+
params: { destination: '大理', hbcliBin: join(tmp, 'no-such-hbcli'), fallbackPath: join(import.meta.dirname, '..', '..', 'data', 'hotels_2026.json') },
|
|
305
193
|
});
|
|
306
194
|
const hb9 = itp9.result;
|
|
307
195
|
assert.ok(hb9 != null, '真实 handler 降级仍返回 observation');
|
|
308
196
|
assert.ok(typeof hb9.summary === 'string' && hb9.summary.length > 0, '降级带人话 summary');
|
|
309
197
|
assert.ok(itp9.trace.evidence[0]?.startsWith('[效应:HBCLI_HOTEL_SEARCH@'), '解译层证据行');
|
|
310
|
-
await rm(tmp, {
|
|
311
|
-
recursive: true,
|
|
312
|
-
force: true
|
|
313
|
-
});
|
|
198
|
+
await rm(tmp, { recursive: true, force: true });
|
|
314
199
|
console.log(`9. 真实 handler 离线冒烟(HBCLI 静态包降级,summary=${(hb9.summary ?? '').slice(0, 40)}…)OK`);
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// 10. M0 预订链读效应:HBCLI_HOTEL_RATES / HBCLI_CHECK_AVAIL
|
|
202
|
+
// (注入 handler 验策略面;真实 handler 离线冒烟验价格面 fail-closed;mock 夹具)
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// 10a. 注册进生产解译器:注入 handler 派发 / 永不重试 / 失败计熔断 / 三连错开闸
|
|
315
205
|
let ratesDispatches = 0;
|
|
316
206
|
const ipM0 = makeProductionInterpreter({
|
|
317
207
|
sleep: sleep0,
|
|
318
208
|
breakers: new Map(),
|
|
319
209
|
handlers: {
|
|
320
|
-
HBCLI_HOTEL_RATES: async ()=>{
|
|
210
|
+
HBCLI_HOTEL_RATES: async () => {
|
|
321
211
|
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
|
-
};
|
|
212
|
+
return { via: 'hbcli-realtime', exitCode: 0, result: { rooms: [] }, evidence: '[实时API:hbcli@ts]', latencyMs: 1, rates: { rooms: [] }, summary: 'ok' };
|
|
335
213
|
},
|
|
336
|
-
HBCLI_CHECK_AVAIL: async ()=>({
|
|
337
|
-
|
|
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
|
-
}
|
|
214
|
+
HBCLI_CHECK_AVAIL: async () => ({ via: 'hbcli-error', exitCode: 1, result: null, evidence: '[实时API:hbcli@error@ts]', latencyMs: 1, error: 'boom', avail: null, summary: 'err' }),
|
|
215
|
+
},
|
|
356
216
|
});
|
|
217
|
+
const itp10 = await ipM0({ effect: 'HBCLI_HOTEL_RATES', params: { hotelId: '900000001', checkIn: '2026-09-05', checkOut: '2026-09-07', adults: 2 } });
|
|
357
218
|
assert.equal(itp10.trace.attempts, 1, 'RATES 永不重试(候选路径切换不是重试)');
|
|
358
219
|
assert.ok(itp10.result != null && !itp10.trace.declined, 'RATES 命中注入 handler,observation 透传');
|
|
359
220
|
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
|
-
});
|
|
221
|
+
const itp10b = await ipM0({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-1' } });
|
|
366
222
|
assert.equal(itp10b.result?.via, 'hbcli-error', '渠道失败观察原样透传(平铺失败面,isFailure 计熔断)');
|
|
367
|
-
await ipM0({
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
});
|
|
223
|
+
await ipM0({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-2' } });
|
|
224
|
+
await ipM0({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-3' } });
|
|
225
|
+
const itp10c = await ipM0({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-4' } });
|
|
385
226
|
assert.equal(itp10c.trace.declined, 'circuit-open', '3 连错熔断保护(同 HBCLI 族策略)');
|
|
386
227
|
assert.equal(ratesDispatches, 1, '成功通道不受失败通道熔断影响(per-effect 隔离)');
|
|
387
228
|
console.log('10a. 预订链读效应注册(派发/永不重试/熔断族/per-effect 隔离)OK');
|
|
229
|
+
// 10b. 真实 handler 离线冒烟:不可达 bin → 诚实失败,价格面无静态降级(fail-closed)
|
|
388
230
|
__resetEffectBreakersForTest();
|
|
389
231
|
const tmp10 = await mkdtemp(join(tmpdir(), 'effect-m0-'));
|
|
390
232
|
const itp10d = await interp({
|
|
391
233
|
effect: 'HBCLI_HOTEL_RATES',
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
hbcliBin: join(tmp10, 'no-such-hbcli')
|
|
395
|
-
}
|
|
234
|
+
// 自定义不存在的 bin:候选路径不回退已知安装位 → ENOENT → 诚实失败(hotel-list 才有静态包降级)
|
|
235
|
+
params: { hotelId: '900000001', hbcliBin: join(tmp10, 'no-such-hbcli') },
|
|
396
236
|
});
|
|
397
237
|
const hr10 = itp10d.result;
|
|
398
238
|
assert.ok(hr10 != null, '不可达仍返回 observation(永不抛错)');
|
|
399
239
|
assert.equal(hr10.via, 'hbcli-error', '未伪装成功');
|
|
400
240
|
assert.match(hr10.summary ?? '', /不可用/, 'summary 明示不可用');
|
|
401
241
|
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
|
-
});
|
|
242
|
+
const itp10e = await interp({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-x', hbcliBin: join(tmp10, 'no-such-hbcli') } });
|
|
409
243
|
assert.equal(itp10e.result?.via, 'hbcli-error', 'check-avail 同口径诚实失败');
|
|
410
|
-
await rm(tmp10, {
|
|
411
|
-
recursive: true,
|
|
412
|
-
force: true
|
|
413
|
-
});
|
|
244
|
+
await rm(tmp10, { recursive: true, force: true });
|
|
414
245
|
console.log('10b. 真实 handler 离线冒烟(价格面 fail-closed,无静态降级)OK');
|
|
246
|
+
// 10c. mock 解译器夹具回放(预订链 CI 形态;录制形态 = 渠道 observation 原样)
|
|
415
247
|
const mock10 = makeMockInterpreter({
|
|
416
|
-
HBCLI_HOTEL_RATES: {
|
|
417
|
-
|
|
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
|
-
}
|
|
248
|
+
HBCLI_HOTEL_RATES: { via: 'hbcli-realtime', exitCode: 0, result: { rooms: [{ ratePkgId: 'rp-fx-1', roomTypeName: 'Deluxe King' }] }, evidence: '[实时API:hbcli@2026-08-30T00:00:00Z]', latencyMs: 1800, rates: { rooms: [{ ratePkgId: 'rp-fx-1', roomTypeName: 'Deluxe King' }] }, summary: 'hotel 900000001:hbcli 实时报价(2026-09-05 → 2026-09-07)' },
|
|
249
|
+
HBCLI_CHECK_AVAIL: { via: 'hbcli-realtime', exitCode: 0, result: { ratePkgId: 'rp-fx-1', avail: true }, evidence: '[实时API:hbcli@2026-08-30T00:00:01Z]', latencyMs: 900, avail: { ratePkgId: 'rp-fx-1', avail: true }, summary: 'ratePkg rp-fx-1:hbcli 实时验价(库存与价格以后端为准)' },
|
|
466
250
|
});
|
|
251
|
+
const m10a = await mock10({ effect: 'HBCLI_HOTEL_RATES', params: { hotelId: '900000001' } });
|
|
252
|
+
const m10b = await mock10({ effect: 'HBCLI_CHECK_AVAIL', params: { ratePkgId: 'rp-fx-1' } });
|
|
467
253
|
assert.equal(m10a.result.rates?.rooms?.length, 1, 'RATES 夹具回放');
|
|
468
254
|
assert.equal(m10b.result.avail?.avail, true, 'CHECK_AVAIL 夹具回放');
|
|
469
255
|
assert.match(m10a.trace.evidence[0] ?? '', /\[效应:mock@.*夹具回放/, 'mock trace 标注');
|
|
470
256
|
console.log('10c. mock 解译器夹具回放(预订链 CI 形态)OK');
|
|
257
|
+
// ---------------------------------------------------------------------------
|
|
258
|
+
// 11. HBCLI×timeout 冷启动重试(2026-09-02 迪拜 session 复盘:hotel-list 首调
|
|
259
|
+
// 30s 超时,LLM 手动立即重试即成功——上游建后端 session 可超时;策略改为
|
|
260
|
+
// 「仅 timeout 类重试 1 次」,ENOENT/退码类维持「切换不是重试」)
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
471
262
|
__resetEffectBreakersForTest();
|
|
472
263
|
let flakyCalls = 0;
|
|
473
264
|
const ipTimeout = makeProductionInterpreter({
|
|
474
265
|
sleep: sleep0,
|
|
475
266
|
breakers: new Map(),
|
|
476
267
|
handlers: {
|
|
477
|
-
HBCLI_HOTEL_SEARCH: async ()=>{
|
|
268
|
+
HBCLI_HOTEL_SEARCH: async () => {
|
|
478
269
|
flakyCalls += 1;
|
|
479
|
-
if (flakyCalls === 1)
|
|
480
|
-
via: 'hbcli-error',
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
latencyMs: 1,
|
|
485
|
-
error: 'timeout after 30000ms'
|
|
486
|
-
};
|
|
487
|
-
return {
|
|
488
|
-
via: 'hbcli-realtime',
|
|
489
|
-
exitCode: 0,
|
|
490
|
-
result: {
|
|
491
|
-
hotels: []
|
|
492
|
-
},
|
|
493
|
-
evidence: '[实时API:hbcli@ts]',
|
|
494
|
-
latencyMs: 1
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
const itp11 = await ipTimeout({
|
|
500
|
-
effect: 'HBCLI_HOTEL_SEARCH',
|
|
501
|
-
params: {
|
|
502
|
-
destination: '迪拜'
|
|
503
|
-
}
|
|
270
|
+
if (flakyCalls === 1)
|
|
271
|
+
return { via: 'hbcli-error', exitCode: -1, result: null, evidence: '[实时API:hbcli@timeout@ts]', latencyMs: 1, error: 'timeout after 30000ms' };
|
|
272
|
+
return { via: 'hbcli-realtime', exitCode: 0, result: { hotels: [] }, evidence: '[实时API:hbcli@ts]', latencyMs: 1 };
|
|
273
|
+
},
|
|
274
|
+
},
|
|
504
275
|
});
|
|
276
|
+
const itp11 = await ipTimeout({ effect: 'HBCLI_HOTEL_SEARCH', params: { destination: '迪拜' } });
|
|
505
277
|
assert.equal(itp11.trace.attempts, 2, `timeout 重试一次(attempts=2),实际 ${itp11.trace.attempts}`);
|
|
506
278
|
assert.equal(itp11.result?.via, 'hbcli-realtime', '重试后恢复实时');
|
|
507
279
|
assert.equal(itp11.trace.breaker, 'closed', '恢复即成功,断路不计数');
|
|
@@ -510,238 +282,79 @@ const ipEnoent = makeProductionInterpreter({
|
|
|
510
282
|
sleep: sleep0,
|
|
511
283
|
breakers: new Map(),
|
|
512
284
|
handlers: {
|
|
513
|
-
HBCLI_HOTEL_SEARCH: async ()=>{
|
|
285
|
+
HBCLI_HOTEL_SEARCH: async () => {
|
|
514
286
|
hardCalls += 1;
|
|
515
|
-
return {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
result: null,
|
|
519
|
-
evidence: '[实时API:hbcli@spawn_error@ts]',
|
|
520
|
-
latencyMs: 1,
|
|
521
|
-
error: 'spawn hbcli ENOENT'
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
});
|
|
526
|
-
const itp11b = await ipEnoent({
|
|
527
|
-
effect: 'HBCLI_HOTEL_SEARCH',
|
|
528
|
-
params: {
|
|
529
|
-
destination: '迪拜'
|
|
530
|
-
}
|
|
287
|
+
return { via: 'hbcli-error', exitCode: -1, result: null, evidence: '[实时API:hbcli@spawn_error@ts]', latencyMs: 1, error: 'spawn hbcli ENOENT' };
|
|
288
|
+
},
|
|
289
|
+
},
|
|
531
290
|
});
|
|
291
|
+
const itp11b = await ipEnoent({ effect: 'HBCLI_HOTEL_SEARCH', params: { destination: '迪拜' } });
|
|
532
292
|
assert.equal(itp11b.trace.attempts, 1, 'ENOENT 类永不重试(切换不是重试契约不变)');
|
|
533
293
|
console.log('11. HBCLI×timeout 冷启动重试(timeout→attempts=2 恢复实时;ENOENT→attempts=1)OK');
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// 12. D-23 收尾(issue #115):六渠道入注册表——策略表行存在 + 横切语义正确
|
|
296
|
+
// (anything/web/github/video/agent_reach/session_login;没有策略表行就没有效应)
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
534
298
|
__resetEffectBreakersForTest();
|
|
299
|
+
// 12a. 六效应注册在案:handler 注入 → 原样透传,trace.channel 与策略表一致
|
|
535
300
|
const sixChannels = [
|
|
536
|
-
[
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
verdict: 'hit',
|
|
543
|
-
evidence: 'e',
|
|
544
|
-
latencyMs: 1
|
|
545
|
-
}
|
|
546
|
-
],
|
|
547
|
-
[
|
|
548
|
-
'WEB_READ',
|
|
549
|
-
'api',
|
|
550
|
-
{
|
|
551
|
-
ok: true,
|
|
552
|
-
via: 'r.jina.ai',
|
|
553
|
-
evidence: 'e',
|
|
554
|
-
latencyMs: 1
|
|
555
|
-
}
|
|
556
|
-
],
|
|
557
|
-
[
|
|
558
|
-
'GITHUB_SEARCH',
|
|
559
|
-
'cli',
|
|
560
|
-
{
|
|
561
|
-
ok: true,
|
|
562
|
-
verdict: 'found',
|
|
563
|
-
evidence: 'e',
|
|
564
|
-
latencyMs: 1
|
|
565
|
-
}
|
|
566
|
-
],
|
|
567
|
-
[
|
|
568
|
-
'VIDEO_SUBTITLE',
|
|
569
|
-
'cli',
|
|
570
|
-
{
|
|
571
|
-
ok: true,
|
|
572
|
-
verdict: 'found',
|
|
573
|
-
evidence: 'e',
|
|
574
|
-
latencyMs: 1
|
|
575
|
-
}
|
|
576
|
-
],
|
|
577
|
-
[
|
|
578
|
-
'AGENT_REACH',
|
|
579
|
-
'cli',
|
|
580
|
-
{
|
|
581
|
-
ok: true,
|
|
582
|
-
verdict: 'found',
|
|
583
|
-
channel: 'v2ex',
|
|
584
|
-
method: 'read',
|
|
585
|
-
evidence: 'e',
|
|
586
|
-
latencyMs: 1
|
|
587
|
-
}
|
|
588
|
-
],
|
|
589
|
-
[
|
|
590
|
-
'SESSION_LOGIN',
|
|
591
|
-
'browser',
|
|
592
|
-
{
|
|
593
|
-
ok: true,
|
|
594
|
-
via: 'session-login',
|
|
595
|
-
verdict: 'pending',
|
|
596
|
-
evidence: 'e',
|
|
597
|
-
latencyMs: 1
|
|
598
|
-
}
|
|
599
|
-
]
|
|
301
|
+
['ANYTHING_SEARCH', 'cli', { ok: true, via: 'hbcli-anything', verdict: 'hit', evidence: 'e', latencyMs: 1 }],
|
|
302
|
+
['WEB_READ', 'api', { ok: true, via: 'r.jina.ai', evidence: 'e', latencyMs: 1 }],
|
|
303
|
+
['GITHUB_SEARCH', 'cli', { ok: true, verdict: 'found', evidence: 'e', latencyMs: 1 }],
|
|
304
|
+
['VIDEO_SUBTITLE', 'cli', { ok: true, verdict: 'found', evidence: 'e', latencyMs: 1 }],
|
|
305
|
+
['AGENT_REACH', 'cli', { ok: true, verdict: 'found', channel: 'v2ex', method: 'read', evidence: 'e', latencyMs: 1 }],
|
|
306
|
+
['SESSION_LOGIN', 'browser', { ok: true, via: 'session-login', verdict: 'pending', evidence: 'e', latencyMs: 1 }],
|
|
600
307
|
];
|
|
601
|
-
for (const [fx, channel, obs] of sixChannels){
|
|
602
|
-
const ip = makeProductionInterpreter({
|
|
603
|
-
|
|
604
|
-
breakers: new Map(),
|
|
605
|
-
handlers: {
|
|
606
|
-
[fx]: async ()=>obs
|
|
607
|
-
}
|
|
608
|
-
});
|
|
609
|
-
const out = await ip({
|
|
610
|
-
effect: fx,
|
|
611
|
-
params: {}
|
|
612
|
-
});
|
|
308
|
+
for (const [fx, channel, obs] of sixChannels) {
|
|
309
|
+
const ip = makeProductionInterpreter({ sleep: sleep0, breakers: new Map(), handlers: { [fx]: async () => obs } });
|
|
310
|
+
const out = await ip({ effect: fx, params: {} });
|
|
613
311
|
assert.equal(out.result, obs, `${fx} 透传原样`);
|
|
614
312
|
assert.equal(out.trace.channel, channel, `${fx} trace.channel=${channel}(策略表行在案)`);
|
|
615
313
|
assert.equal(out.trace.attempts, 1, `${fx} 成功单次执行`);
|
|
616
314
|
}
|
|
617
315
|
console.log('12a. 六渠道入表(透传 + trace.channel 与策略表一致)OK');
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
'
|
|
621
|
-
|
|
622
|
-
const ip = makeProductionInterpreter({
|
|
623
|
-
sleep: sleep0,
|
|
624
|
-
breakers: new Map(),
|
|
625
|
-
handlers: {
|
|
626
|
-
[fx]: async ()=>{
|
|
627
|
-
throw new Error('upstream says no');
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
});
|
|
631
|
-
const out = await ip({
|
|
632
|
-
effect: fx,
|
|
633
|
-
params: {}
|
|
634
|
-
});
|
|
316
|
+
// 12b. 永不重试族:AGENT_REACH / SESSION_LOGIN handler 恒抛错 → attempts=1(透传面不放大上游消耗)
|
|
317
|
+
for (const fx of ['AGENT_REACH', 'SESSION_LOGIN']) {
|
|
318
|
+
const ip = makeProductionInterpreter({ sleep: sleep0, breakers: new Map(), handlers: { [fx]: async () => { throw new Error('upstream says no'); } } });
|
|
319
|
+
const out = await ip({ effect: fx, params: {} });
|
|
635
320
|
assert.equal(out.result, null, `${fx} 抛错 → result=null`);
|
|
636
321
|
assert.equal(out.trace.attempts, 1, `${fx} 永不重试(attempts=1)`);
|
|
637
322
|
}
|
|
638
323
|
console.log('12b. AGENT_REACH/SESSION_LOGIN 永不重试(反射桥透传/浏览器风控红线)OK');
|
|
324
|
+
// 12c. WEB_READ 瞬时抖动重试 1 次(throw → 成功,attempts=2);GITHUB timeout 重试 / not-installed 不重试
|
|
639
325
|
__resetEffectBreakersForTest();
|
|
640
326
|
let webCalls = 0;
|
|
641
327
|
const ipWeb = makeProductionInterpreter({
|
|
642
|
-
sleep: sleep0,
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
WEB_READ: async ()=>{
|
|
646
|
-
webCalls += 1;
|
|
647
|
-
if (webCalls === 1) throw new Error('socket hang up');
|
|
648
|
-
return {
|
|
649
|
-
ok: true,
|
|
650
|
-
via: 'r.jina.ai',
|
|
651
|
-
evidence: 'e',
|
|
652
|
-
latencyMs: 1
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
});
|
|
657
|
-
const outWeb = await ipWeb({
|
|
658
|
-
effect: 'WEB_READ',
|
|
659
|
-
params: {
|
|
660
|
-
url: 'https://example.com'
|
|
661
|
-
}
|
|
328
|
+
sleep: sleep0, breakers: new Map(),
|
|
329
|
+
handlers: { WEB_READ: async () => { webCalls += 1; if (webCalls === 1)
|
|
330
|
+
throw new Error('socket hang up'); return { ok: true, via: 'r.jina.ai', evidence: 'e', latencyMs: 1 }; } },
|
|
662
331
|
});
|
|
332
|
+
const outWeb = await ipWeb({ effect: 'WEB_READ', params: { url: 'https://example.com' } });
|
|
663
333
|
assert.equal(outWeb.trace.attempts, 2, `WEB_READ 瞬时抖动重试 1 次,实际 ${outWeb.trace.attempts}`);
|
|
664
334
|
let ghTimeoutCalls = 0;
|
|
665
335
|
const ipGhTimeout = makeProductionInterpreter({
|
|
666
|
-
sleep: sleep0,
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
GITHUB_SEARCH: async ()=>{
|
|
670
|
-
ghTimeoutCalls += 1;
|
|
671
|
-
if (ghTimeoutCalls === 1) return {
|
|
672
|
-
ok: false,
|
|
673
|
-
verdict: 'timeout',
|
|
674
|
-
evidence: 'e',
|
|
675
|
-
latencyMs: 1,
|
|
676
|
-
stdout: '',
|
|
677
|
-
stderr: ''
|
|
678
|
-
};
|
|
679
|
-
return {
|
|
680
|
-
ok: true,
|
|
681
|
-
verdict: 'found',
|
|
682
|
-
evidence: 'e',
|
|
683
|
-
latencyMs: 1
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
});
|
|
688
|
-
const outGhT = await ipGhTimeout({
|
|
689
|
-
effect: 'GITHUB_SEARCH',
|
|
690
|
-
params: {
|
|
691
|
-
query: 'x'
|
|
692
|
-
}
|
|
336
|
+
sleep: sleep0, breakers: new Map(),
|
|
337
|
+
handlers: { GITHUB_SEARCH: async () => { ghTimeoutCalls += 1; if (ghTimeoutCalls === 1)
|
|
338
|
+
return { ok: false, verdict: 'timeout', evidence: 'e', latencyMs: 1, stdout: '', stderr: '' }; return { ok: true, verdict: 'found', evidence: 'e', latencyMs: 1 }; } },
|
|
693
339
|
});
|
|
340
|
+
const outGhT = await ipGhTimeout({ effect: 'GITHUB_SEARCH', params: { query: 'x' } });
|
|
694
341
|
assert.equal(outGhT.trace.attempts, 2, 'GITHUB timeout 类重试 1 次(冷启动同 HBCLI 实况)');
|
|
695
342
|
let ghNiCalls = 0;
|
|
696
343
|
const ipGhNi = makeProductionInterpreter({
|
|
697
|
-
sleep: sleep0,
|
|
698
|
-
|
|
699
|
-
handlers: {
|
|
700
|
-
GITHUB_SEARCH: async ()=>{
|
|
701
|
-
ghNiCalls += 1;
|
|
702
|
-
return {
|
|
703
|
-
ok: false,
|
|
704
|
-
verdict: 'not-installed',
|
|
705
|
-
evidence: 'e',
|
|
706
|
-
latencyMs: 0
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
const outGhNi = await ipGhNi({
|
|
712
|
-
effect: 'GITHUB_SEARCH',
|
|
713
|
-
params: {
|
|
714
|
-
query: 'x'
|
|
715
|
-
}
|
|
344
|
+
sleep: sleep0, breakers: new Map(),
|
|
345
|
+
handlers: { GITHUB_SEARCH: async () => { ghNiCalls += 1; return { ok: false, verdict: 'not-installed', evidence: 'e', latencyMs: 0 }; } },
|
|
716
346
|
});
|
|
347
|
+
const outGhNi = await ipGhNi({ effect: 'GITHUB_SEARCH', params: { query: 'x' } });
|
|
717
348
|
assert.equal(outGhNi.trace.attempts, 1, 'not-installed 是配置态(needs-setup 族)不重试');
|
|
718
349
|
console.log('12c. WEB_READ/GITHUB/VIDEO 策略行(timeout 重试/配置态不重试)OK');
|
|
350
|
+
// 12d. mock 面:新效应夹具回放 + 未登记夹具结构化拒绝(与既有效应同语义)
|
|
719
351
|
__resetEffectBreakersForTest();
|
|
720
|
-
const mockIp = makeMockInterpreter({
|
|
721
|
-
|
|
722
|
-
ok: true,
|
|
723
|
-
via: 'hbcli-anything',
|
|
724
|
-
verdict: 'hit',
|
|
725
|
-
evidence: '[实时API:hbcli-anything@ts]',
|
|
726
|
-
latencyMs: 1
|
|
727
|
-
}
|
|
728
|
-
});
|
|
729
|
-
const outMock = await mockIp({
|
|
730
|
-
effect: 'ANYTHING_SEARCH',
|
|
731
|
-
params: {
|
|
732
|
-
keyword: '大理'
|
|
733
|
-
}
|
|
734
|
-
});
|
|
352
|
+
const mockIp = makeMockInterpreter({ ANYTHING_SEARCH: { ok: true, via: 'hbcli-anything', verdict: 'hit', evidence: '[实时API:hbcli-anything@ts]', latencyMs: 1 } });
|
|
353
|
+
const outMock = await mockIp({ effect: 'ANYTHING_SEARCH', params: { keyword: '大理' } });
|
|
735
354
|
assert.equal(outMock.result.verdict, 'hit', 'mock 夹具回放原样');
|
|
736
355
|
assert.equal(outMock.trace.channel, 'mock', 'mock 通道标注');
|
|
737
|
-
const outMockBad = await mockIp({
|
|
738
|
-
effect: 'SESSION_LOGIN',
|
|
739
|
-
params: {}
|
|
740
|
-
});
|
|
356
|
+
const outMockBad = await mockIp({ effect: 'SESSION_LOGIN', params: {} });
|
|
741
357
|
assert.equal(outMockBad.result, null, '未登记夹具 → result=null');
|
|
742
358
|
assert.equal(outMockBad.trace.declined, 'unknown-effect', '未登记夹具 → 结构化拒绝');
|
|
743
359
|
console.log('12d. mock 夹具回放/未登记拒绝(新效应同语义)OK');
|
|
744
360
|
console.log('EFFECT INTERPRETER TESTS: 12/12 OK(effect_interpreter.v1:注册表封闭/退避链/断路三态/Sentinel 不重试/mock 夹具/SESSION 红线/真实降级/M0 预订链读效应/HBCLI timeout 重试/D-23 六渠道入表,纯离线)');
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
//# sourceURL=ts/scripts/effect-tests.ts
|