@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
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #338 持久默认出发地 E2E(issue #338 acceptance,isolated 工程证据):
|
|
3
|
+
*
|
|
4
|
+
* 跑在临时 stateRoot,验证 issue #338 的验收路径,
|
|
5
|
+
* 每条都是隔离 fixture,不在真实用户状态读写。
|
|
6
|
+
*
|
|
7
|
+
* 验收路径(一一对应 issue #338 验收清单):
|
|
8
|
+
* 1. 首次显式保存 → fresh stateRoot 落盘 + motivation.patch 事件 + 投影 homeCity + typed preference 三件套
|
|
9
|
+
* 2. 新插件/读回上下文:close 第一段 ledger → 起第二段插件 → motivation_brief 拿到持久 city + 证据指针 + 记录时间
|
|
10
|
+
* 3. 当轮显式出发地覆盖默认(pure resolveDefaultOrigin 契约)
|
|
11
|
+
* 4. 重复保存 = 幂等(无新事件)
|
|
12
|
+
* 5. 跨 tenant 不可见(tenant 隔离守门)
|
|
13
|
+
* 6. 显式清除移除活跃投影 + 审计事件/证据边界保留 + 后续 missing
|
|
14
|
+
* 7. 重建(rebuildProjections)后投影与直读一致(账本/投影永不分叉)
|
|
15
|
+
* 8. 默认值永不筛候选/不改硬约束(同 payload 在 saved/cleared 下产出完全一致)
|
|
16
|
+
*
|
|
17
|
+
* 工程切片不变量(issue #338 acceptance-core):
|
|
18
|
+
* - 持久 home-city 值携带其自身的「证据指针」+「更新时间」(来自 typed
|
|
19
|
+
* homeCityPreference),不再从全局 evidence 尾派生
|
|
20
|
+
* - 无关动机补丁不会改写 homeCityPreference.evidence / updated_at
|
|
21
|
+
* - 空白 city 入口被拒,零事件落账
|
|
22
|
+
* - trim before persist
|
|
23
|
+
* - 显式 null 与从未存都走 missing;resolver 不猜测
|
|
24
|
+
* - 不进算术/求解/硬约束/候选过滤(feasibility 同输入同结果)
|
|
25
|
+
*
|
|
26
|
+
* 不替代 #20 真实回访价值证据(归 #20);仅 issue #338 的工程切片。
|
|
27
|
+
*/
|
|
28
|
+
import assert from 'node:assert/strict';
|
|
29
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
30
|
+
import { tmpdir } from 'node:os';
|
|
31
|
+
import { join } from 'node:path';
|
|
32
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
33
|
+
import { ToolRuntime } from '@deepseek-ai/dsh-tools';
|
|
34
|
+
import { renderPrompt, SystemPrompt } from '@deepseek-ai/dsh-system-prompt';
|
|
35
|
+
import { parse as parseYaml } from 'yaml';
|
|
36
|
+
import { apply } from '../src/index.js';
|
|
37
|
+
import { resolveDefaultOrigin } from '../src/memory-capture.js';
|
|
38
|
+
import { ensureLedger, openLedgerIfExists } from '../src/state-ledger.js';
|
|
39
|
+
let pass = 0;
|
|
40
|
+
let fail = 0;
|
|
41
|
+
function check(cond, msg) {
|
|
42
|
+
if (cond) {
|
|
43
|
+
pass++;
|
|
44
|
+
console.log(` ok - ${msg}`);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
fail++;
|
|
48
|
+
console.error(` FAIL - ${msg}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function loadFixturePayload() {
|
|
52
|
+
// 复用 #2 golden 价表(纯离线,无副作用):feasibility_check 的同一基准 → 验证
|
|
53
|
+
// homeCity 不影响候选/可行性。
|
|
54
|
+
const path = join('..', 'data', 'golden_erhai.json');
|
|
55
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
56
|
+
}
|
|
57
|
+
function loadPersonaPrefix() {
|
|
58
|
+
const patchPath = join(import.meta.dirname, '..', '..', 'cordis.gotry-patch.yml');
|
|
59
|
+
const document = parseYaml(readFileSync(patchPath, 'utf-8'));
|
|
60
|
+
if (!Array.isArray(document))
|
|
61
|
+
throw new Error('cordis.gotry-patch.yml must contain a patch array');
|
|
62
|
+
const systemPrompt = document.find(entry => (typeof entry === 'object' && entry !== null && !Array.isArray(entry)
|
|
63
|
+
&& entry.id === 'system-prompt'));
|
|
64
|
+
const prefix = systemPrompt?.config?.personaPrefix;
|
|
65
|
+
if (typeof prefix !== 'string')
|
|
66
|
+
throw new Error('cordis.gotry-patch.yml system-prompt.personaPrefix is missing');
|
|
67
|
+
return prefix;
|
|
68
|
+
}
|
|
69
|
+
let toolCall = 0;
|
|
70
|
+
function runTool(ctx, name, args) {
|
|
71
|
+
return ctx.tools.execute({
|
|
72
|
+
callId: `issue-338-${++toolCall}-${name}`,
|
|
73
|
+
name,
|
|
74
|
+
arguments: args,
|
|
75
|
+
signal: new AbortController().signal,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
function resultValue(result) {
|
|
79
|
+
assert.equal(result.isError, false, `真实 ToolRuntime 调用失败: ${result.isError ? result.error.message : 'unknown'}`);
|
|
80
|
+
return result.value;
|
|
81
|
+
}
|
|
82
|
+
async function bootPlugin(stateRoot, personaPrefix = '') {
|
|
83
|
+
const ctx = new Context();
|
|
84
|
+
await ctx.plugin(SystemPrompt, { personaPrefix });
|
|
85
|
+
await ctx.plugin(ToolRuntime, {});
|
|
86
|
+
apply(ctx, {
|
|
87
|
+
stateRoot,
|
|
88
|
+
timeoutMs: 30_000,
|
|
89
|
+
hbcliBin: 'hbcli-not-on-path',
|
|
90
|
+
sessionAccess: 'ask',
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
ctx,
|
|
94
|
+
// E2E 用:让测试代码强制释放模块级 openLedgers 缓存,后续 bootPlugin
|
|
95
|
+
// 就会从 SQLite 真实重读,模拟「进程重启后再开读回」
|
|
96
|
+
close: async () => {
|
|
97
|
+
const ledger = openLedgerIfExists(stateRoot);
|
|
98
|
+
if (ledger)
|
|
99
|
+
ledger.close();
|
|
100
|
+
await ctx.fiber.dispose();
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
async function renderRawProfilePrompt(stateRoot, profile) {
|
|
105
|
+
const profilePath = join(stateRoot, 'gotry-state', 'motivation-profile.json');
|
|
106
|
+
mkdirSync(join(stateRoot, 'gotry-state'), { recursive: true });
|
|
107
|
+
writeFileSync(profilePath, JSON.stringify(profile));
|
|
108
|
+
const before = readFileSync(profilePath, 'utf-8');
|
|
109
|
+
const world = await bootPlugin(stateRoot, loadPersonaPrefix());
|
|
110
|
+
try {
|
|
111
|
+
const prompt = renderPrompt(await world.ctx.systemPrompt.assemble());
|
|
112
|
+
return {
|
|
113
|
+
prompt,
|
|
114
|
+
before,
|
|
115
|
+
after: readFileSync(profilePath, 'utf-8'),
|
|
116
|
+
dbCreated: existsSync(join(stateRoot, 'gotry-state', 'gotry-state.db')),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
await world.close();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function main() {
|
|
124
|
+
// ===== acceptance #1:首次显式保存(gotry_motivation_save → fresh stateRoot) =====
|
|
125
|
+
console.log('=== acceptance #1:首次显式保存(gotry_motivation_save) ===');
|
|
126
|
+
const root1 = mkdtempSync(join(tmpdir(), 'gotry-338-1-'));
|
|
127
|
+
let world1 = null;
|
|
128
|
+
let world2 = null;
|
|
129
|
+
try {
|
|
130
|
+
world1 = await bootPlugin(root1);
|
|
131
|
+
const homeQuote = '用户原话:我常驻上海,从这里出发';
|
|
132
|
+
const firstResult = await runTool(world1.ctx, 'gotry_motivation_save', {
|
|
133
|
+
profile: {
|
|
134
|
+
evidence: [homeQuote],
|
|
135
|
+
homeCity: '上海',
|
|
136
|
+
homeCityEvidence: homeQuote,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const first = resultValue(firstResult);
|
|
140
|
+
check(first.ok === true && first.saved === true, '首次保存应 ok=true 且 saved=true');
|
|
141
|
+
check(first.profile?.homeCity === '上海', `首次保存后画像 homeCity='上海'(实际 ${String(first.profile?.homeCity)})`);
|
|
142
|
+
check((first.profile?.evidence ?? []).includes(homeQuote), 'evidence 追加进画像');
|
|
143
|
+
// typed preference 三件套
|
|
144
|
+
const pref = first.profile?.homeCityPreference;
|
|
145
|
+
check(pref?.value === '上海' && pref?.evidence === homeQuote && typeof pref?.updated_at === 'string', 'typed preference 三件套(value/evidence/updated_at)写入返回 profile');
|
|
146
|
+
// 读经账本(非内存)
|
|
147
|
+
const ledger = openLedgerIfExists(root1);
|
|
148
|
+
check(!!ledger, '账本应在 fresh stateRoot 落盘(fresh root + 写操作)');
|
|
149
|
+
const persisted = ledger?.readMotivation();
|
|
150
|
+
check(persisted?.homeCity === '上海', '账本投影 homeCity 持久化(账本是权威面)');
|
|
151
|
+
check(persisted?.homeCityPreference?.value === '上海', '账本投影 homeCityPreference.value 持久化');
|
|
152
|
+
check(persisted?.homeCityPreference?.evidence === homeQuote, '账本投影 homeCityPreference.evidence 持久化(锁定指针)');
|
|
153
|
+
check(typeof persisted?.homeCityPreference?.updated_at === 'string', '账本投影 homeCityPreference.updated_at 持久化(事件边界 ISO)');
|
|
154
|
+
check(typeof persisted?.updated_at === 'string', 'updated_at 时间戳落账');
|
|
155
|
+
// motivation.patch 事件落账(append-only)
|
|
156
|
+
const events = ledger?.readEvents('motivation.patch', 10) ?? [];
|
|
157
|
+
check(events.length === 1, `首次保存产生恰好 1 条 motivation.patch 事件(实际 ${events.length})`);
|
|
158
|
+
// ===== acceptance #2:真实 Cordis → SystemPrompt → renderPrompt 读回 =====
|
|
159
|
+
console.log('\n=== acceptance #2:真实 model-facing system prompt 读回 default + evidence + updated_at ===');
|
|
160
|
+
check(loadPersonaPrefix().includes('{{motivation_brief}}'), 'cordis.gotry-patch.yml 使用 motivation_brief 占位符');
|
|
161
|
+
// 关闭第一段 context 的 ledger,再起第二段真实 Context/SystemPrompt/ToolRuntime。
|
|
162
|
+
// 这一步证明下一次 model request 的 prompt 从 SQLite 磁盘读回;不调用 LLM。
|
|
163
|
+
await world1.close();
|
|
164
|
+
world1 = null;
|
|
165
|
+
world2 = await bootPlugin(root1, loadPersonaPrefix());
|
|
166
|
+
const assembly = await world2.ctx.systemPrompt.assemble();
|
|
167
|
+
const renderedPrompt = renderPrompt(assembly);
|
|
168
|
+
check(renderedPrompt.includes('常驻城市: 上海'), '最终渲染 system prompt 含持久常驻城市');
|
|
169
|
+
check(renderedPrompt.includes(homeQuote), '最终渲染 system prompt 含 exact home-city evidence');
|
|
170
|
+
check(typeof pref?.updated_at === 'string' && renderedPrompt.includes(`记录于 ${pref.updated_at}`), '最终渲染 system prompt 含持久 preference timestamp');
|
|
171
|
+
check(renderedPrompt.includes('本轮显式出发地优先'), '最终渲染 system prompt 含 explicit-current-turn-wins 指令');
|
|
172
|
+
check(renderedPrompt.includes('软默认') && renderedPrompt.includes('不删候选') && renderedPrompt.includes('不进可行性硬约束'), '最终渲染 system prompt 明示 soft-default/no-hard-filter 边界');
|
|
173
|
+
check(!renderedPrompt.includes('{{motivation_brief}}'), '最终渲染 system prompt 不残留 motivation_brief 占位符');
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
if (world2)
|
|
177
|
+
await world2.close();
|
|
178
|
+
if (world1)
|
|
179
|
+
await world1.close();
|
|
180
|
+
rmSync(root1, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
// ===== acceptance #2b:raw/坏 typed profile 只读且不得推断默认 =====
|
|
183
|
+
console.log('\n=== acceptance #2b:raw/坏 typed profile → missing clarification (real prompt path) ===');
|
|
184
|
+
const legacyRoot = mkdtempSync(join(tmpdir(), 'gotry-338-2b-legacy-'));
|
|
185
|
+
try {
|
|
186
|
+
const legacyCity = '旧版上海';
|
|
187
|
+
const unrelatedEvidence = '用户原话:这条只谈预算,不是城市声明';
|
|
188
|
+
const rawReadback = await renderRawProfilePrompt(legacyRoot, {
|
|
189
|
+
weights: { escape_rest: 1 },
|
|
190
|
+
evidence: [unrelatedEvidence, '用户原话:另一个全局事实'],
|
|
191
|
+
hard: {},
|
|
192
|
+
homeCity: legacyCity,
|
|
193
|
+
updated_at: '2026-09-10T00:00:00.000Z',
|
|
194
|
+
});
|
|
195
|
+
check(!rawReadback.prompt.includes(legacyCity), 'raw legacy homeCity 不注入 system prompt');
|
|
196
|
+
check(!rawReadback.prompt.includes(unrelatedEvidence), 'raw legacy 全局 evidence 不标为城市原话');
|
|
197
|
+
check(rawReadback.prompt.includes('无可验证的持久默认') && rawReadback.prompt.includes('本轮请明确出发地'), 'raw legacy prompt 要求当轮明确出发地');
|
|
198
|
+
check(rawReadback.before === rawReadback.after && !rawReadback.dbCreated, 'raw legacy 读取不改写或迁移文件');
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
rmSync(legacyRoot, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
const malformedCases = [
|
|
204
|
+
{
|
|
205
|
+
label: 'evidence pointer absent from pool',
|
|
206
|
+
city: '脱池上海',
|
|
207
|
+
evidence: '用户原话:全局事实不是城市声明',
|
|
208
|
+
profile: {
|
|
209
|
+
weights: { escape_rest: 1 },
|
|
210
|
+
evidence: ['用户原话:全局事实不是城市声明'],
|
|
211
|
+
hard: {},
|
|
212
|
+
homeCity: '脱池上海',
|
|
213
|
+
homeCityPreference: { value: '脱池上海', evidence: '用户原话:不在 evidence pool', updated_at: '2026-09-10T00:00:00.000Z' },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
label: 'mismatched mirror',
|
|
218
|
+
city: '镜像上海',
|
|
219
|
+
evidence: '用户原话:typed 北京',
|
|
220
|
+
profile: {
|
|
221
|
+
weights: { escape_rest: 1 },
|
|
222
|
+
evidence: ['用户原话:全局事实不是城市声明', '用户原话:typed 北京'],
|
|
223
|
+
hard: {},
|
|
224
|
+
homeCity: '镜像上海',
|
|
225
|
+
homeCityPreference: { value: 'typed 北京', evidence: '用户原话:typed 北京', updated_at: '2026-09-10T00:00:00.000Z' },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
label: 'impossible timestamp',
|
|
230
|
+
city: '非法日期上海',
|
|
231
|
+
evidence: '用户原话:全局事实也不是城市声明',
|
|
232
|
+
profile: {
|
|
233
|
+
weights: { escape_rest: 1 },
|
|
234
|
+
evidence: ['用户原话:全局事实也不是城市声明', '用户原话:我常驻非法日期上海'],
|
|
235
|
+
hard: {},
|
|
236
|
+
homeCity: '非法日期上海',
|
|
237
|
+
homeCityPreference: { value: '非法日期上海', evidence: '用户原话:我常驻非法日期上海', updated_at: '2026-02-30T00:00:00.000Z' },
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
label: 'bad evidence/timestamp',
|
|
242
|
+
city: '坏证据上海',
|
|
243
|
+
evidence: '用户原话:全局事实也不是城市声明',
|
|
244
|
+
profile: {
|
|
245
|
+
weights: { escape_rest: 1 },
|
|
246
|
+
evidence: ['用户原话:全局事实也不是城市声明'],
|
|
247
|
+
hard: {},
|
|
248
|
+
homeCity: '坏证据上海',
|
|
249
|
+
homeCityPreference: { value: '坏证据上海', evidence: ' ', updated_at: 'not-a-timestamp' },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
];
|
|
253
|
+
for (const malformed of malformedCases) {
|
|
254
|
+
const malformedRoot = mkdtempSync(join(tmpdir(), 'gotry-338-2b-malformed-'));
|
|
255
|
+
try {
|
|
256
|
+
const rawReadback = await renderRawProfilePrompt(malformedRoot, malformed.profile);
|
|
257
|
+
check(!rawReadback.prompt.includes(malformed.city), `${malformed.label} city 不注入 system prompt`);
|
|
258
|
+
check(!rawReadback.prompt.includes(malformed.evidence), `${malformed.label} global evidence 不标为城市原话`);
|
|
259
|
+
check(rawReadback.prompt.includes('无可验证的持久默认') && rawReadback.prompt.includes('本轮请明确出发地'), `${malformed.label} prompt 要求当轮明确出发地`);
|
|
260
|
+
check(rawReadback.before === rawReadback.after && !rawReadback.dbCreated, `${malformed.label} 读取不改写或迁移文件`);
|
|
261
|
+
const explicit = resolveDefaultOrigin('北京', malformed.profile);
|
|
262
|
+
check(explicit.source === 'explicit' && explicit.origin === '北京', `${malformed.label} 下显式 origin 仍优先`);
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
rmSync(malformedRoot, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// ===== acceptance #3:当轮显式出发地覆盖默认(pure resolveDefaultOrigin) =====
|
|
269
|
+
console.log('\n=== acceptance #3:当轮显式出发地覆盖默认 ===');
|
|
270
|
+
const root3 = mkdtempSync(join(tmpdir(), 'gotry-338-3-'));
|
|
271
|
+
let world3 = null;
|
|
272
|
+
try {
|
|
273
|
+
world3 = await bootPlugin(root3);
|
|
274
|
+
const homeQuote = '用户原话:我常驻上海';
|
|
275
|
+
const saved = resultValue(await runTool(world3.ctx, 'gotry_motivation_save', {
|
|
276
|
+
profile: { homeCity: '上海', evidence: [homeQuote] },
|
|
277
|
+
}));
|
|
278
|
+
check(saved.saved === true, 'resolver fixture 的保存经真实 ToolRuntime 成功');
|
|
279
|
+
const persisted = openLedgerIfExists(root3)?.readMotivation();
|
|
280
|
+
const explicit = resolveDefaultOrigin('北京', persisted);
|
|
281
|
+
check(explicit.source === 'explicit' && explicit.origin === '北京', `pure resolver explicit 优先(实际 source=${explicit.source}, origin=${explicit.origin})`);
|
|
282
|
+
check(explicit.homeCity === '上海', 'explicit 路径不丢弃 homeCity 供 UI 展示');
|
|
283
|
+
const blankAsMissing = resolveDefaultOrigin(' ', persisted);
|
|
284
|
+
check(blankAsMissing.source === 'home_default' && blankAsMissing.origin === '上海', '空白 origin 视作未传,落 home_default');
|
|
285
|
+
// typed preference 路径标记
|
|
286
|
+
check(explicit.homeCityPreferenceSource === 'none', 'explicit 路径 source 标签为 none');
|
|
287
|
+
check(blankAsMissing.homeCityPreferenceSource === 'typed', '空白 origin 时走 typed preference 路径');
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
if (world3)
|
|
291
|
+
await world3.close();
|
|
292
|
+
rmSync(root3, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
// ===== acceptance #4:重复保存 = 幂等(无新事件) =====
|
|
295
|
+
console.log('\n=== acceptance #4:重复保存 = 幂等 ===');
|
|
296
|
+
const root4 = mkdtempSync(join(tmpdir(), 'gotry-338-4-'));
|
|
297
|
+
let world4 = null;
|
|
298
|
+
try {
|
|
299
|
+
world4 = await bootPlugin(root4);
|
|
300
|
+
const homeQuote = '用户原话:我常驻上海';
|
|
301
|
+
const unrelatedFirst = '用户原话:这次预算不超过 5000';
|
|
302
|
+
const first = resultValue(await runTool(world4.ctx, 'gotry_motivation_save', {
|
|
303
|
+
profile: { homeCity: '上海', evidence: [unrelatedFirst, homeQuote], homeCityEvidence: homeQuote },
|
|
304
|
+
}));
|
|
305
|
+
check(first.saved === true, '首次保存 saved=true');
|
|
306
|
+
const ledger = openLedgerIfExists(root4);
|
|
307
|
+
const countAfterFirst = ledger.readEvents('motivation.patch', 50).length;
|
|
308
|
+
const prefFirst = ledger.readMotivation()?.homeCityPreference;
|
|
309
|
+
check(prefFirst?.evidence === homeQuote, '无关新事实在前时 preference 仍锁定后面的 home-city quote');
|
|
310
|
+
const missingBinding = await runTool(world4.ctx, 'gotry_motivation_save', {
|
|
311
|
+
profile: { homeCity: '北京', evidence: ['用户原话:我搬去北京', '用户原话:这次还要控制预算'] },
|
|
312
|
+
});
|
|
313
|
+
const missingValue = resultValue(missingBinding);
|
|
314
|
+
check(missingValue.ok === false, '改城市缺失 homeCityEvidence 应走结构化失败');
|
|
315
|
+
check(ledger.readEvents('motivation.patch', 50).length === countAfterFirst, '缺失 binding 零新 ledger 事件');
|
|
316
|
+
const orphanBinding = await runTool(world4.ctx, 'gotry_motivation_save', {
|
|
317
|
+
profile: { evidence: [homeQuote], homeCityEvidence: homeQuote },
|
|
318
|
+
});
|
|
319
|
+
const orphanValue = resultValue(orphanBinding);
|
|
320
|
+
check(orphanValue.ok === false, 'homeCityEvidence 脱离 homeCity 应走结构化失败');
|
|
321
|
+
check(ledger.readEvents('motivation.patch', 50).length === countAfterFirst, 'orphan binding 零新 ledger 事件');
|
|
322
|
+
const mismatchedBinding = await runTool(world4.ctx, 'gotry_motivation_save', {
|
|
323
|
+
profile: { homeCity: '北京', evidence: ['用户原话:我搬去北京'], homeCityEvidence: '用户原话:未出现在本次 evidence' },
|
|
324
|
+
});
|
|
325
|
+
const mismatchedValue = resultValue(mismatchedBinding);
|
|
326
|
+
check(mismatchedValue.ok === false, 'mismatched homeCityEvidence 应走结构化失败');
|
|
327
|
+
check(ledger.readEvents('motivation.patch', 50).length === countAfterFirst, 'mismatched binding 零新 ledger 事件');
|
|
328
|
+
const staleBinding = resultValue(await runTool(world4.ctx, 'gotry_motivation_save', {
|
|
329
|
+
profile: { homeCity: '北京', evidence: [homeQuote], homeCityEvidence: homeQuote },
|
|
330
|
+
}));
|
|
331
|
+
check(staleBinding.saved === false, '改城市绑定已存在的 stale quote 不应落账');
|
|
332
|
+
check(ledger.readEvents('motivation.patch', 50).length === countAfterFirst, 'stale binding 零新 ledger 事件');
|
|
333
|
+
const second = resultValue(await runTool(world4.ctx, 'gotry_motivation_save', {
|
|
334
|
+
profile: { homeCity: '上海', evidence: [homeQuote], homeCityEvidence: homeQuote },
|
|
335
|
+
}));
|
|
336
|
+
check(second.saved === false, '同值同 evidence 重复保存 saved=false (P0 守门)');
|
|
337
|
+
const countAfterSecond = ledger.readEvents('motivation.patch', 50).length;
|
|
338
|
+
check(countAfterSecond === countAfterFirst, `幂等:无新 motivation.patch 事件(实际差 ${countAfterSecond - countAfterFirst})`);
|
|
339
|
+
// 同值 + genuinely new bound home-city quote 可更新 preference/timestamp;
|
|
340
|
+
// evidence 数组中故意把无关事实放在前面,绑定明确指向第二条。
|
|
341
|
+
const refreshedQuote = '用户原话:我现在仍常驻上海';
|
|
342
|
+
const refreshed = resultValue(await runTool(world4.ctx, 'gotry_motivation_save', {
|
|
343
|
+
profile: { homeCity: '上海', evidence: ['用户原话:孩子要上学', refreshedQuote], homeCityEvidence: refreshedQuote },
|
|
344
|
+
}));
|
|
345
|
+
check(refreshed.saved === true, '同值 + 新 bound home-city quote 允许落盘');
|
|
346
|
+
const prefAfterBound = ledger.readMotivation()?.homeCityPreference;
|
|
347
|
+
const latestBoundEvent = ledger.readEvents('motivation.patch', 50)[0];
|
|
348
|
+
check(prefAfterBound?.evidence === refreshedQuote, '新绑定 exact quote 更新 preference.evidence');
|
|
349
|
+
check(prefAfterBound?.updated_at === latestBoundEvent?.ts, '新绑定 preference.updated_at 等于事件行 timestamp');
|
|
350
|
+
check(prefAfterBound?.updated_at !== prefFirst?.updated_at || prefAfterBound?.evidence !== prefFirst?.evidence, '新绑定改变 preference 记录而非仅追加全局 evidence');
|
|
351
|
+
const clearStale = resultValue(await runTool(world4.ctx, 'gotry_motivation_save', {
|
|
352
|
+
profile: { homeCity: null, evidence: [refreshedQuote], homeCityEvidence: refreshedQuote },
|
|
353
|
+
}));
|
|
354
|
+
check(clearStale.saved === false, '清除绑定 stale quote 不应落账');
|
|
355
|
+
const countAfterStaleClear = ledger.readEvents('motivation.patch', 50).length;
|
|
356
|
+
check(countAfterStaleClear === countAfterSecond + 1, `stale clear 零新 ledger 事件(实际 ${countAfterStaleClear})`);
|
|
357
|
+
// 仅无关 patch 仍可追加全局 evidence,但绝不改写 homeCityPreference。
|
|
358
|
+
const unrelated = resultValue(await runTool(world4.ctx, 'gotry_motivation_save', {
|
|
359
|
+
profile: { evidence: ['用户原话:后续无关事实:孩子要上学'] },
|
|
360
|
+
}));
|
|
361
|
+
check(unrelated.saved === true, '未传 homeCity 的无关 patch 仍可追加');
|
|
362
|
+
const afterUnrelated = ledger.readEvents('motivation.patch', 50).length;
|
|
363
|
+
check(afterUnrelated === countAfterStaleClear + 1, '无关 patch 产生 1 条新事件');
|
|
364
|
+
const prefAfter = ledger.readMotivation()?.homeCityPreference;
|
|
365
|
+
check(prefAfter?.value === '上海', '无关 evidence 追加后 homeCityPreference.value 不变');
|
|
366
|
+
check(prefAfter?.evidence === prefAfterBound?.evidence, '无关 evidence 追加不污染 homeCityPreference.evidence');
|
|
367
|
+
check(prefAfter?.updated_at === prefAfterBound?.updated_at, '无关 evidence 追加不改写 homeCityPreference.updated_at');
|
|
368
|
+
}
|
|
369
|
+
finally {
|
|
370
|
+
if (world4)
|
|
371
|
+
await world4.close();
|
|
372
|
+
rmSync(root4, { recursive: true, force: true });
|
|
373
|
+
}
|
|
374
|
+
// ===== acceptance #5:跨 tenant 不可见 =====
|
|
375
|
+
console.log('\n=== acceptance #5:跨 tenant 不可见 ===');
|
|
376
|
+
const root5 = mkdtempSync(join(tmpdir(), 'gotry-338-5-'));
|
|
377
|
+
try {
|
|
378
|
+
// local tenant 写入 homeCity
|
|
379
|
+
const localLedger = ensureLedger(root5, 'local');
|
|
380
|
+
localLedger.appendMotivationPatch({
|
|
381
|
+
homeCity: '上海', evidence: ['用户原话:常驻上海'], homeCityEvidence: '用户原话:常驻上海',
|
|
382
|
+
});
|
|
383
|
+
// tenant-a 视角:账本物理文件存在,但 motivation 投影按 tenant_id 列过滤 → null
|
|
384
|
+
const tenantA = openLedgerIfExists(root5, 'tenant-a');
|
|
385
|
+
check(!!tenantA, '账本物理文件存在(共享 SQLite,按 tenant_id 列隔离)');
|
|
386
|
+
const motivationA = tenantA?.readMotivation() ?? null;
|
|
387
|
+
check(motivationA === null, `tenant-a 读 motivation 必须 null(实际 ${JSON.stringify(motivationA)})`);
|
|
388
|
+
// tenant-a 第一次写自己的 homeCity(物理同文件,逻辑隔离)
|
|
389
|
+
const tenantALedger = ensureLedger(root5, 'tenant-a');
|
|
390
|
+
tenantALedger.appendMotivationPatch({
|
|
391
|
+
homeCity: '北京', evidence: ['用户原话:常驻北京'], homeCityEvidence: '用户原话:常驻北京',
|
|
392
|
+
});
|
|
393
|
+
check(tenantALedger.readMotivation()?.homeCity === '北京', 'tenant-a 视角读到自己的 homeCity');
|
|
394
|
+
// local 仍是上海
|
|
395
|
+
check(localLedger.readMotivation()?.homeCity === '上海', 'local 视角仍是 上海(不被 tenant-a 串位)');
|
|
396
|
+
// resolveDefaultOrigin 在 tenant-a 视角:homeCity=北京
|
|
397
|
+
const resolvedTenantA = resolveDefaultOrigin(undefined, tenantALedger.readMotivation());
|
|
398
|
+
check(resolvedTenantA.source === 'home_default' && resolvedTenantA.origin === '北京', 'tenant-a 解析得 home_default=北京');
|
|
399
|
+
// 同 root 切换回 local:仍是上海
|
|
400
|
+
const resolvedLocal = resolveDefaultOrigin(undefined, localLedger.readMotivation());
|
|
401
|
+
check(resolvedLocal.origin === '上海', 'local 解析得 home_default=上海');
|
|
402
|
+
}
|
|
403
|
+
finally {
|
|
404
|
+
const ledger = openLedgerIfExists(root5, 'local');
|
|
405
|
+
if (ledger)
|
|
406
|
+
ledger.close();
|
|
407
|
+
const tenantLedger = openLedgerIfExists(root5, 'tenant-a');
|
|
408
|
+
if (tenantLedger)
|
|
409
|
+
tenantLedger.close();
|
|
410
|
+
rmSync(root5, { recursive: true, force: true });
|
|
411
|
+
}
|
|
412
|
+
// ===== acceptance #6:显式清除(移除活跃投影,但审计事件保留) =====
|
|
413
|
+
console.log('\n=== acceptance #6:显式清除(移除活跃投影,审计事件保留) ===');
|
|
414
|
+
const root6 = mkdtempSync(join(tmpdir(), 'gotry-338-6-'));
|
|
415
|
+
let world6 = null;
|
|
416
|
+
try {
|
|
417
|
+
world6 = await bootPlugin(root6, loadPersonaPrefix());
|
|
418
|
+
const homeQuote = '用户原话:常驻上海';
|
|
419
|
+
resultValue(await runTool(world6.ctx, 'gotry_motivation_save', {
|
|
420
|
+
profile: { homeCity: '上海', evidence: [homeQuote], homeCityEvidence: homeQuote },
|
|
421
|
+
}));
|
|
422
|
+
const ledger = openLedgerIfExists(root6);
|
|
423
|
+
check(ledger.readMotivation()?.homeCity === '上海', '保存后 homeCity=上海');
|
|
424
|
+
const eventsBeforeClear = ledger.readEvents('motivation.patch', 50).length;
|
|
425
|
+
// 显式清除
|
|
426
|
+
const clearQuote = '用户原话:迁居,不设默认';
|
|
427
|
+
const clearResult = resultValue(await runTool(world6.ctx, 'gotry_motivation_save', {
|
|
428
|
+
profile: { homeCity: null, evidence: [clearQuote], homeCityEvidence: clearQuote },
|
|
429
|
+
}));
|
|
430
|
+
check(clearResult.saved === true, '清除请求应 saved=true(发生变更)');
|
|
431
|
+
check(clearResult.profile?.homeCity === null, `清除后画像 homeCity=null(实际 ${String(clearResult.profile?.homeCity)})`);
|
|
432
|
+
const eventsAfterClear = ledger.readEvents('motivation.patch', 50).length;
|
|
433
|
+
check(eventsAfterClear === eventsBeforeClear + 1, `清除产生 1 条 motivation.patch 事件(差 ${eventsAfterClear - eventsBeforeClear})`);
|
|
434
|
+
// 活跃投影:homeCity 为 null
|
|
435
|
+
const ledgerAfterClear = ledger.readMotivation();
|
|
436
|
+
check(ledgerAfterClear.homeCity === null, '账本投影 homeCity=null(已清除)');
|
|
437
|
+
// typed preference 记录清除态(value=null,但 evidence + updated_at 留存)
|
|
438
|
+
check(ledgerAfterClear.homeCityPreference?.value === null, 'typed preference.value=null(清除态)');
|
|
439
|
+
check(ledgerAfterClear.homeCityPreference?.evidence === clearQuote, 'typed preference.evidence 锁定到本次清除原话');
|
|
440
|
+
check(typeof ledgerAfterClear.homeCityPreference?.updated_at === 'string', 'typed preference.updated_at 留存');
|
|
441
|
+
// 审计事件保留:清除 patch 事件依然存在
|
|
442
|
+
const clearEvent = ledger.readEvents('motivation.patch', 50).find(e => {
|
|
443
|
+
const p = JSON.parse(e.payload);
|
|
444
|
+
return p.patch?.homeCity === null;
|
|
445
|
+
});
|
|
446
|
+
check(!!clearEvent, '清除事件在 append-only 事件流中可审计');
|
|
447
|
+
// evidence 列表包含清除原话(溯源 P0:谁、何时、说了什么)
|
|
448
|
+
check((ledgerAfterClear.evidence ?? []).includes('用户原话:迁居,不设默认'), 'evidence append-only 保留清除原话');
|
|
449
|
+
// 读回变量更新:motivation_brief 反映 missing 状态
|
|
450
|
+
const clearPrompt = renderPrompt(await world6.ctx.systemPrompt.assemble());
|
|
451
|
+
check(clearPrompt.includes('已显式清除'), `真实渲染 system prompt 在清除后反映 missing(实际:${clearPrompt.slice(0, 200)})`);
|
|
452
|
+
// ===== acceptance #7:清除后 resolveDefaultOrigin 返回 missing(不猜测) =====
|
|
453
|
+
console.log('\n=== acceptance #7:清除后 resolveDefaultOrigin 返回 missing ===');
|
|
454
|
+
const resolved = resolveDefaultOrigin(undefined, ledgerAfterClear);
|
|
455
|
+
check(resolved.source === 'missing' && resolved.origin === null, `清除后必须 missing(实际 source=${resolved.source}, origin=${resolved.origin})`);
|
|
456
|
+
// 显式 origin 仍赢
|
|
457
|
+
const resolvedExplicit = resolveDefaultOrigin('深圳', ledgerAfterClear);
|
|
458
|
+
check(resolvedExplicit.source === 'explicit' && resolvedExplicit.origin === '深圳', '清除后显式 origin 仍赢');
|
|
459
|
+
// fold 重建仍正确反映清除状态(账本/投影永不分叉)
|
|
460
|
+
ledger.rebuildProjections();
|
|
461
|
+
const afterRebuild = ledger.readMotivation();
|
|
462
|
+
check(afterRebuild.homeCity === null, 'fold 重建后 homeCity 仍为 null(账本=权威面)');
|
|
463
|
+
check(afterRebuild.homeCityPreference?.value === null, 'fold 重建后 typed preference.value 仍为 null');
|
|
464
|
+
check(afterRebuild.homeCityPreference?.evidence === clearQuote, 'fold 重建后 typed preference.evidence 不丢');
|
|
465
|
+
check(afterRebuild.homeCityPreference?.updated_at === ledgerAfterClear.homeCityPreference?.updated_at, 'fold 重建后 typed preference.updated_at 与直读一致(无 clock 漂移)');
|
|
466
|
+
check(JSON.stringify(afterRebuild) === JSON.stringify(ledgerAfterClear), '重建后 projection JSON 与重建前 byte-equivalent');
|
|
467
|
+
// 重复清除幂等
|
|
468
|
+
const dupClear = resultValue(await runTool(world6.ctx, 'gotry_motivation_save', {
|
|
469
|
+
profile: { homeCity: null, evidence: [clearQuote], homeCityEvidence: clearQuote },
|
|
470
|
+
}));
|
|
471
|
+
check(dupClear.saved === false, '同 null + 同 evidence 重复清除幂等(saved=false)');
|
|
472
|
+
// 空白 homeCity 入口拒收,零事件落账
|
|
473
|
+
// (registerGuarded → guardToolExecute 兜成结构化失败,不再向上抛)
|
|
474
|
+
const beforeBlank = ledger.readEvents('motivation.patch', 50).length;
|
|
475
|
+
const blankResult = await runTool(world6.ctx, 'gotry_motivation_save', { profile: { homeCity: ' ', evidence: ['用户原话:空字符串不该落账'], homeCityEvidence: '用户原话:空字符串不该落账' } });
|
|
476
|
+
const blankValue = resultValue(blankResult);
|
|
477
|
+
check(blankValue.ok === false, `空白 homeCity 应被结构化拒绝(ok=false,实际 ${blankValue.ok})`);
|
|
478
|
+
const failureMsg = String(blankValue.summary ?? '');
|
|
479
|
+
check(/空白/.test(failureMsg), `空白 homeCity 拒绝信息明示「空白」(实际:${failureMsg})`);
|
|
480
|
+
const afterBlank = ledger.readEvents('motivation.patch', 50).length;
|
|
481
|
+
check(afterBlank === beforeBlank, `空白 homeCity 拒绝后零新事件(实际差 ${afterBlank - beforeBlank})`);
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
if (world6)
|
|
485
|
+
await world6.close();
|
|
486
|
+
rmSync(root6, { recursive: true, force: true });
|
|
487
|
+
}
|
|
488
|
+
// ===== acceptance #8:默认永不筛候选/不改硬约束 =====
|
|
489
|
+
console.log('\n=== acceptance #8:默认永不筛候选/不改硬约束 ===');
|
|
490
|
+
const root8 = mkdtempSync(join(tmpdir(), 'gotry-338-8-'));
|
|
491
|
+
let world8 = null;
|
|
492
|
+
let world8b = null;
|
|
493
|
+
let root8bInner = null;
|
|
494
|
+
try {
|
|
495
|
+
// 1) 无 homeCity 基线(fresh stateRoot:账本无记录 → profile.homeCity=undefined)
|
|
496
|
+
const basePayload = loadFixturePayload();
|
|
497
|
+
world8 = await bootPlugin(root8);
|
|
498
|
+
const baseline = resultValue(await runTool(world8.ctx, 'gotry_feasibility_check', { payload: basePayload }));
|
|
499
|
+
check(baseline.ok === true, 'baseline feasibility 应成功');
|
|
500
|
+
const baselineCount = (baseline.verdicts ?? []).length;
|
|
501
|
+
const baselineIds = (baseline.verdicts ?? []).map(v => String(v['candidate_id'])).sort();
|
|
502
|
+
const baselineRecommended = baseline.recommended;
|
|
503
|
+
await world8.close();
|
|
504
|
+
world8 = null;
|
|
505
|
+
rmSync(root8, { recursive: true, force: true });
|
|
506
|
+
// 2) 同 payload + 持久 homeCity='上海'(stateRoot 写过 homeCity=上海)
|
|
507
|
+
const root8b = mkdtempSync(join(tmpdir(), 'gotry-338-8b-'));
|
|
508
|
+
root8bInner = root8b;
|
|
509
|
+
try {
|
|
510
|
+
world8b = await bootPlugin(root8b);
|
|
511
|
+
const homeQuote = '用户原话:常驻上海';
|
|
512
|
+
resultValue(await runTool(world8b.ctx, 'gotry_motivation_save', {
|
|
513
|
+
profile: { homeCity: '上海', evidence: [homeQuote], homeCityEvidence: homeQuote },
|
|
514
|
+
}));
|
|
515
|
+
const withHome = resultValue(await runTool(world8b.ctx, 'gotry_feasibility_check', { payload: basePayload }));
|
|
516
|
+
check(withHome.ok === true, '设置 homeCity 后 feasibility 仍成功');
|
|
517
|
+
const withHomeCount = (withHome.verdicts ?? []).length;
|
|
518
|
+
const withHomeIds = (withHome.verdicts ?? []).map(v => String(v['candidate_id'])).sort();
|
|
519
|
+
check(withHomeCount === baselineCount, `候选数量不变(baseline=${baselineCount}, with_home=${withHomeCount})`);
|
|
520
|
+
check(withHomeIds.join(',') === baselineIds.join(','), `候选 id 集合不变(${withHomeIds.join(',')})`);
|
|
521
|
+
check(withHome.recommended === baselineRecommended, `推荐结果不变(baseline=${baselineRecommended}, with_home=${withHome.recommended})`);
|
|
522
|
+
// 3) 同 payload + 显式 homeCity=null(已清除)
|
|
523
|
+
const clearQuote = '用户原话:取消常驻';
|
|
524
|
+
resultValue(await runTool(world8b.ctx, 'gotry_motivation_save', {
|
|
525
|
+
profile: { homeCity: null, evidence: [clearQuote], homeCityEvidence: clearQuote },
|
|
526
|
+
}));
|
|
527
|
+
const withCleared = resultValue(await runTool(world8b.ctx, 'gotry_feasibility_check', { payload: basePayload }));
|
|
528
|
+
const clearedCount = (withCleared.verdicts ?? []).length;
|
|
529
|
+
const clearedIds = (withCleared.verdicts ?? []).map(v => String(v['candidate_id'])).sort();
|
|
530
|
+
check(clearedCount === baselineCount, `清除后候选数量仍不变(${clearedCount})`);
|
|
531
|
+
check(clearedIds.join(',') === baselineIds.join(','), '清除后候选 id 集合仍不变');
|
|
532
|
+
check(withCleared.recommended === baselineRecommended, '清除后推荐结果仍不变');
|
|
533
|
+
// 4) 守护:持久默认只供读回/规划提示,不进入 feasibility/evaluate
|
|
534
|
+
// 的候选过滤或硬约束路径;因此与上海无关的候选仍保留。
|
|
535
|
+
check(baselineIds.includes('qiandao') && baselineIds.includes('taihu'), 'baseline 候选包含 qiandao/taihu(候选未按持久默认过滤)');
|
|
536
|
+
// 5) equality 只证明 persisted default 不 hard-filter 或改变确定性
|
|
537
|
+
// candidates/recommendation;不声称 feasibility_check 消费 resolver。
|
|
538
|
+
const withHomeJson = JSON.stringify({
|
|
539
|
+
ok: withHome.ok, recommended: withHome.recommended,
|
|
540
|
+
verdictIds: (withHome.verdicts ?? []).map(v => String(v['candidate_id'])).sort(),
|
|
541
|
+
});
|
|
542
|
+
const withClearedJson = JSON.stringify({
|
|
543
|
+
ok: withCleared.ok, recommended: withCleared.recommended,
|
|
544
|
+
verdictIds: (withCleared.verdicts ?? []).map(v => String(v['candidate_id'])).sort(),
|
|
545
|
+
});
|
|
546
|
+
check(withHomeJson === withClearedJson, '同 payload 在 homeCity=上海 与 homeCity=null 下产出完全一致(候选不依赖 homeCity)');
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
if (world8b)
|
|
550
|
+
await world8b.close();
|
|
551
|
+
world8b = null;
|
|
552
|
+
rmSync(root8b, { recursive: true, force: true });
|
|
553
|
+
root8bInner = null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
finally {
|
|
557
|
+
if (world8)
|
|
558
|
+
await world8.close();
|
|
559
|
+
if (root8bInner)
|
|
560
|
+
rmSync(root8bInner, { recursive: true, force: true });
|
|
561
|
+
if (existsSync(root8))
|
|
562
|
+
rmSync(root8, { recursive: true, force: true });
|
|
563
|
+
}
|
|
564
|
+
console.log(`\nISSUE-#338 HOME-CITY E2E: ${pass} ok, ${fail} fail`);
|
|
565
|
+
if (fail > 0)
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
main().catch((err) => {
|
|
569
|
+
console.error(err);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
});
|
|
572
|
+
// 该文件不导出符号(测试自身为脚本进程)。
|
|
573
|
+
// 显式存在性检查保留:读取 model.ts 物理存在以证明 issue #338 不动 model.ts 算术/求解路径。
|
|
574
|
+
if (!existsSync(join(import.meta.dirname, '..', 'src', 'model.ts'))) {
|
|
575
|
+
console.error('FAIL: model.ts 不存在(该 E2E 假设 model.ts 不被改动)');
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|