@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,10 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 账本回归(ADR-15 执行锚点,run-all §28):
|
|
3
|
+
* 1 事务原子性:事务内注入异常 → 整体回滚,账本无痕
|
|
4
|
+
* 2 红线进事务:conditions 缺失拒绝且零事件落账
|
|
5
|
+
* 3 幂等物理化:愿望同名/效用语义键/行程 trip_id 重放 no-op;wish_id 语义派生稳定
|
|
6
|
+
* 4 守门随账本:权重变更守门/词表外日期拒收/重叠冲突即停/负面清单拒收
|
|
7
|
+
* 5 confirm-outcome 单事务:效用+行程同生(跨文件分叉不可能)
|
|
8
|
+
* 6 fold 重建:DROP 投影 → 重放 → 与直读一致(账本/投影永不分叉)
|
|
9
|
+
* 7 rewind:fold 截到历史 seq → 投影回到历史时点;无参 rebuild 回最新
|
|
10
|
+
* 8 one-shot 迁移:旧 JSON/JSONL → events;快照先行;重复 ensure 不重复导入
|
|
11
|
+
* 9 durable 工单崩溃恢复:子进程 settle 前 exit 9 → 恢复零重算(exactly-once)
|
|
12
|
+
* 10 pending_writes saga:幂等键去重/L3 确认一次/补偿/审计事件链/what-if 分叉隔离
|
|
13
|
+
* 11 ADR-16 tenant scope:append/read/fold/rebuild/迁移边界/同 id/跨进程 reopen 全隔离;
|
|
14
|
+
* 精确 fold 反例、kind+limit 截断、workflow/forget 行归属都必须可负向验红
|
|
15
|
+
* 运行(在 ts/ 下):npx tsx scripts/ledger-tests.ts
|
|
16
|
+
*/
|
|
1
17
|
import Database from 'better-sqlite3';
|
|
2
18
|
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
3
19
|
import { tmpdir } from 'node:os';
|
|
4
20
|
import { join } from 'node:path';
|
|
5
21
|
import { spawnSync } from 'node:child_process';
|
|
6
22
|
import { persistAsyncTicket } from '../src/loop.js';
|
|
7
|
-
import { ensureLedger, openLedgerIfExists } from '../src/state-ledger.js';
|
|
23
|
+
import { ensureLedger, ledgerDbPath, ledgerExists, openLedgerIfExists, readWishPoolWithFallback } from '../src/state-ledger.js';
|
|
8
24
|
import { parseFlightPackToSpec } from '../src/unified.js';
|
|
9
25
|
let pass = 0;
|
|
10
26
|
let fail = 0;
|
|
@@ -12,168 +28,84 @@ function assert(cond, msg) {
|
|
|
12
28
|
if (cond) {
|
|
13
29
|
pass++;
|
|
14
30
|
console.log(` ok - ${msg}`);
|
|
15
|
-
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
16
33
|
fail++;
|
|
17
34
|
console.error(` FAIL - ${msg}`);
|
|
18
35
|
}
|
|
19
36
|
}
|
|
20
37
|
function terminalOutcomeOf(stdout) {
|
|
21
38
|
const line = stdout.trim().split('\n').filter(Boolean).at(-1);
|
|
22
|
-
if (!line)
|
|
39
|
+
if (!line)
|
|
40
|
+
return null;
|
|
23
41
|
try {
|
|
24
42
|
const value = JSON.parse(line);
|
|
25
|
-
if (value.schema !== 'gotry_async_terminal.v1'
|
|
43
|
+
if (value.schema !== 'gotry_async_terminal.v1'
|
|
44
|
+
|| typeof value.ticket_id !== 'string'
|
|
45
|
+
|| (value.status !== 'succeeded' && value.status !== 'failed')
|
|
46
|
+
|| typeof value.passed !== 'number'
|
|
47
|
+
|| typeof value.total !== 'number'
|
|
48
|
+
|| typeof value.checks !== 'object'
|
|
49
|
+
|| !Array.isArray(value.failed_checks))
|
|
50
|
+
return null;
|
|
26
51
|
return value;
|
|
27
|
-
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
28
54
|
return null;
|
|
29
55
|
}
|
|
30
56
|
}
|
|
57
|
+
// ---- 1-7:fresh root 基座面 ------------------------------------------------------
|
|
31
58
|
const root = mkdtempSync(join(tmpdir(), 'gotry-ledger-'));
|
|
32
59
|
const ledger = ensureLedger(root);
|
|
33
60
|
const r1 = ledger.appendMotivationPatch({
|
|
34
|
-
weights: {
|
|
35
|
-
escape_rest: 0.7
|
|
36
|
-
},
|
|
37
|
-
evidence: [
|
|
38
|
-
'用户原话:想去湖边什么都不干'
|
|
39
|
-
],
|
|
40
|
-
hard: {
|
|
41
|
-
wake_not_before: '06:30'
|
|
42
|
-
}
|
|
61
|
+
weights: { escape_rest: 0.7 }, evidence: ['用户原话:想去湖边什么都不干'], hard: { wake_not_before: '06:30' },
|
|
43
62
|
});
|
|
44
63
|
assert(r1.saved === true, '画像补丁落账(saved)');
|
|
45
|
-
const r2 = ledger.appendMotivationPatch({
|
|
46
|
-
evidence: [
|
|
47
|
-
'用户原话:想去湖边什么都不干'
|
|
48
|
-
]
|
|
49
|
-
});
|
|
64
|
+
const r2 = ledger.appendMotivationPatch({ evidence: ['用户原话:想去湖边什么都不干'] });
|
|
50
65
|
assert(r2.saved === false, '同 evidence 重放幂等(mergeProfile 守门 → 零事件)');
|
|
51
66
|
const before = ledger.countEvents();
|
|
52
67
|
let threw = false;
|
|
53
68
|
try {
|
|
54
|
-
ledger.db.transaction(()=>{
|
|
55
|
-
ledger.insertEvent({
|
|
56
|
-
actor: 'test',
|
|
57
|
-
kind: 'probe',
|
|
58
|
-
payload: {
|
|
59
|
-
x: 1
|
|
60
|
-
}
|
|
61
|
-
});
|
|
69
|
+
ledger.db.transaction(() => {
|
|
70
|
+
ledger.insertEvent({ actor: 'test', kind: 'probe', payload: { x: 1 } });
|
|
62
71
|
throw new Error('boom');
|
|
63
72
|
})();
|
|
64
|
-
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
65
75
|
threw = true;
|
|
66
76
|
}
|
|
67
77
|
assert(threw && ledger.countEvents() === before, '事务内异常 → 整体回滚,账本无痕(崩溃一致性的单事务证明)');
|
|
68
78
|
threw = false;
|
|
69
79
|
try {
|
|
70
|
-
ledger.appendWish({
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
conditions: undefined
|
|
74
|
-
});
|
|
75
|
-
} catch {
|
|
80
|
+
ledger.appendWish({ name: '无条件憧憬', reason: '', conditions: undefined });
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
76
83
|
threw = true;
|
|
77
84
|
}
|
|
78
85
|
assert(threw && ledger.countEvents() === before, 'conditions 红线在事务内拒绝且零事件(红线进 schema 层)');
|
|
79
|
-
const w1 = ledger.appendWish({
|
|
80
|
-
|
|
81
|
-
reason: '',
|
|
82
|
-
conditions: {
|
|
83
|
-
days: 5,
|
|
84
|
-
budget_cny: 4950,
|
|
85
|
-
best_months: [
|
|
86
|
-
3,
|
|
87
|
-
4
|
|
88
|
-
]
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
const w2 = ledger.appendWish({
|
|
92
|
-
name: '大理·洱海',
|
|
93
|
-
reason: '两周年想再去',
|
|
94
|
-
conditions: {
|
|
95
|
-
days: 6
|
|
96
|
-
}
|
|
97
|
-
});
|
|
86
|
+
const w1 = ledger.appendWish({ name: '大理·洱海', reason: '', conditions: { days: 5, budget_cny: 4950, best_months: [3, 4] } });
|
|
87
|
+
const w2 = ledger.appendWish({ name: '大理·洱海', reason: '两周年想再去', conditions: { days: 6 } });
|
|
98
88
|
assert(w1.added === true && w2.added === false && w1.wish_id === w2.wish_id, '同名愿望幂等更新,wish_id 语义派生稳定');
|
|
99
89
|
assert(ledger.readWishPool().length === 1, '愿望池投影恰 1 条');
|
|
100
|
-
const u1 = ledger.appendUtilityEvent({
|
|
101
|
-
|
|
102
|
-
kind: 'recalled',
|
|
103
|
-
ts: '2026-08-28T00:00:00Z',
|
|
104
|
-
ctx: 'test'
|
|
105
|
-
});
|
|
106
|
-
const u2 = ledger.appendUtilityEvent({
|
|
107
|
-
wish_id: w1.wish_id,
|
|
108
|
-
kind: 'recalled',
|
|
109
|
-
ts: '2026-08-28T00:00:00Z',
|
|
110
|
-
ctx: 'test'
|
|
111
|
-
});
|
|
90
|
+
const u1 = ledger.appendUtilityEvent({ wish_id: w1.wish_id, kind: 'recalled', ts: '2026-08-28T00:00:00Z', ctx: 'test' });
|
|
91
|
+
const u2 = ledger.appendUtilityEvent({ wish_id: w1.wish_id, kind: 'recalled', ts: '2026-08-28T00:00:00Z', ctx: 'test' });
|
|
112
92
|
assert(u1.appended === true && u2.appended === false, '效用事件语义键幂等(重放 no-op)');
|
|
113
|
-
ledger.appendUtilityEvent({
|
|
114
|
-
wish_id: w1.wish_id,
|
|
115
|
-
kind: 'verified_outcome',
|
|
116
|
-
ts: '2026-08-28T00:00:00Z',
|
|
117
|
-
ctx: 'test-noattr'
|
|
118
|
-
});
|
|
93
|
+
ledger.appendUtilityEvent({ wish_id: w1.wish_id, kind: 'verified_outcome', ts: '2026-08-28T00:00:00Z', ctx: 'test-noattr' });
|
|
119
94
|
assert(ledger.readUtilityEvents().at(-1)?.kind === 'applied', '无归因 verified 降级 applied(六件语义纪律随账本)');
|
|
120
|
-
const t1 = ledger.appendTripEvent({
|
|
121
|
-
|
|
122
|
-
start: '2026-10-01',
|
|
123
|
-
source: 'user-verbatim',
|
|
124
|
-
evidence: '用户原话:国庆去了大理'
|
|
125
|
-
});
|
|
126
|
-
const t1dup = ledger.appendTripEvent({
|
|
127
|
-
destination: '大理',
|
|
128
|
-
start: '2026-10-01',
|
|
129
|
-
source: 'user-verbatim',
|
|
130
|
-
evidence: '用户原话:国庆去了大理'
|
|
131
|
-
});
|
|
95
|
+
const t1 = ledger.appendTripEvent({ destination: '大理', start: '2026-10-01', source: 'user-verbatim', evidence: '用户原话:国庆去了大理' });
|
|
96
|
+
const t1dup = ledger.appendTripEvent({ destination: '大理', start: '2026-10-01', source: 'user-verbatim', evidence: '用户原话:国庆去了大理' });
|
|
132
97
|
assert(t1.appended === true && t1dup.appended === false, '行程 trip_id 幂等');
|
|
133
|
-
const tOverlap = ledger.appendTripEvent({
|
|
134
|
-
destination: '大理',
|
|
135
|
-
start: '2026-10-03',
|
|
136
|
-
source: 'user-verbatim',
|
|
137
|
-
evidence: '重叠探针'
|
|
138
|
-
});
|
|
98
|
+
const tOverlap = ledger.appendTripEvent({ destination: '大理', start: '2026-10-03', source: 'user-verbatim', evidence: '重叠探针' });
|
|
139
99
|
assert(tOverlap.appended === false && /重叠/.test(tOverlap.reason ?? ''), '同目的地日期重叠冲突即停(由人裁决)');
|
|
140
|
-
const tBad = ledger.appendTripEvent({
|
|
141
|
-
destination: 'X地',
|
|
142
|
-
start: '十月一',
|
|
143
|
-
source: 'user-verbatim',
|
|
144
|
-
evidence: 'x'
|
|
145
|
-
});
|
|
100
|
+
const tBad = ledger.appendTripEvent({ destination: 'X地', start: '十月一', source: 'user-verbatim', evidence: 'x' });
|
|
146
101
|
assert(tBad.appended === false, '词表外日期拒收(不猜)');
|
|
147
|
-
const cBad = ledger.appendCompanion({
|
|
148
|
-
label: '爸爸',
|
|
149
|
-
constraints: {
|
|
150
|
-
health: [
|
|
151
|
-
'手机号13800001111'
|
|
152
|
-
]
|
|
153
|
-
},
|
|
154
|
-
evidence: '探针'
|
|
155
|
-
});
|
|
102
|
+
const cBad = ledger.appendCompanion({ label: '爸爸', constraints: { health: ['手机号13800001111'] }, evidence: '探针' });
|
|
156
103
|
assert(cBad.appended === false && /负面清单/.test(cBad.reason ?? ''), '同行人负面清单拒收(证件/手机号不入库)');
|
|
157
|
-
const cOk = ledger.appendCompanion({
|
|
158
|
-
label: '爸爸',
|
|
159
|
-
constraints: {
|
|
160
|
-
health: [
|
|
161
|
-
'轻度高血压'
|
|
162
|
-
]
|
|
163
|
-
},
|
|
164
|
-
evidence: '用户原话:爸爸65轻度高血压'
|
|
165
|
-
});
|
|
104
|
+
const cOk = ledger.appendCompanion({ label: '爸爸', constraints: { health: ['轻度高血压'] }, evidence: '用户原话:爸爸65轻度高血压' });
|
|
166
105
|
assert(cOk.appended === true && ledger.readCompanions().length === 1, '同行人正常入账');
|
|
167
106
|
const co = ledger.confirmOutcome({
|
|
168
|
-
wishId: w1.wish_id,
|
|
169
|
-
|
|
170
|
-
detail: '成了',
|
|
171
|
-
trip: {
|
|
172
|
-
destination: '大理',
|
|
173
|
-
start: '2026-10-01',
|
|
174
|
-
source: 'wish-confirmed',
|
|
175
|
-
evidence: `wish ${w1.wish_id} confirm-outcome(helpful)`
|
|
176
|
-
}
|
|
107
|
+
wishId: w1.wish_id, attribution: 'helpful', detail: '成了',
|
|
108
|
+
trip: { destination: '大理', start: '2026-10-01', source: 'wish-confirmed', evidence: `wish ${w1.wish_id} confirm-outcome(helpful)` },
|
|
177
109
|
});
|
|
178
110
|
assert(co.recorded === true, 'confirm-outcome:verified_outcome 效用事件落账');
|
|
179
111
|
assert(co.trip?.appended === false && /重叠/.test(co.trip?.reason ?? ''), 'confirm-outcome 单事务:行程过守门(重叠拒收,与文件版语义一致)——两写同生或同拒');
|
|
@@ -182,76 +114,25 @@ const profileBefore = ledger.readMotivation();
|
|
|
182
114
|
ledger.db.exec('DELETE FROM projection_docs; DELETE FROM projection_items');
|
|
183
115
|
ledger.rebuildProjections();
|
|
184
116
|
assert(JSON.stringify(ledger.readWishPool()) === poolBefore, 'fold 重建:愿望池与直读逐字节一致');
|
|
185
|
-
assert(JSON.stringify(ledger.readMotivation()?.weights) === JSON.stringify(profileBefore?.weights)
|
|
117
|
+
assert(JSON.stringify(ledger.readMotivation()?.weights) === JSON.stringify(profileBefore?.weights)
|
|
118
|
+
&& (ledger.readMotivation()?.evidence?.length ?? 0) === (profileBefore?.evidence?.length ?? 0), 'fold 重建:画像与直读一致');
|
|
186
119
|
assert(ledger.readCompanions().length === 1 && ledger.readTrips().length === 1, 'fold 重建:同行人投影与行程日志一致');
|
|
187
|
-
const wishAddedSeqs = ledger.readEvents('wish.added', 10).map(
|
|
120
|
+
const wishAddedSeqs = ledger.readEvents('wish.added', 10).map(e => e.seq);
|
|
188
121
|
const firstWishSeq = Math.min(...wishAddedSeqs);
|
|
189
|
-
ledger.appendWish({
|
|
190
|
-
name: '普吉',
|
|
191
|
-
reason: '',
|
|
192
|
-
conditions: {
|
|
193
|
-
days: 5
|
|
194
|
-
}
|
|
195
|
-
});
|
|
122
|
+
ledger.appendWish({ name: '普吉', reason: '', conditions: { days: 5 } });
|
|
196
123
|
ledger.rebuildProjections(firstWishSeq);
|
|
197
124
|
assert(ledger.readWishPool().length === 1, `rewind 至 seq ${firstWishSeq}:投影回到历史时点(LangGraph fork 同构)`);
|
|
198
125
|
ledger.rebuildProjections();
|
|
199
126
|
assert(ledger.readWishPool().length === 2, 'rebuild 无参回到最新(events 是唯一权威,投影随时可重建)');
|
|
127
|
+
// ---- 8:one-shot 迁移 --------------------------------------------------------------
|
|
200
128
|
const mroot = mkdtempSync(join(tmpdir(), 'gotry-migrate-'));
|
|
201
129
|
const mdir = join(mroot, 'gotry-state');
|
|
202
|
-
mkdirSync(mdir, {
|
|
203
|
-
|
|
204
|
-
});
|
|
205
|
-
writeFileSync(join(mdir, '
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
},
|
|
209
|
-
evidence: [
|
|
210
|
-
'原话A'
|
|
211
|
-
],
|
|
212
|
-
hard: {},
|
|
213
|
-
updated_at: '2026-08-01T00:00:00Z'
|
|
214
|
-
}));
|
|
215
|
-
writeFileSync(join(mdir, 'wish-pool.json'), JSON.stringify([
|
|
216
|
-
{
|
|
217
|
-
wish_id: 'wLEGACY1',
|
|
218
|
-
name: '京都',
|
|
219
|
-
conditions: {
|
|
220
|
-
days: 7
|
|
221
|
-
},
|
|
222
|
-
added_at: '2026-08-01T00:00:00Z'
|
|
223
|
-
}
|
|
224
|
-
]));
|
|
225
|
-
writeFileSync(join(mdir, 'memory-utility.jsonl'), JSON.stringify({
|
|
226
|
-
schema: 'memory_utility_observation.v0',
|
|
227
|
-
event_id: 'wLEGACY1|recalled||',
|
|
228
|
-
wish_id: 'wLEGACY1',
|
|
229
|
-
kind: 'recalled',
|
|
230
|
-
ts: '2026-08-02T00:00:00Z'
|
|
231
|
-
}) + '\n');
|
|
232
|
-
writeFileSync(join(mdir, 'trips.jsonl'), JSON.stringify({
|
|
233
|
-
schema: 'travel_timeline.v1',
|
|
234
|
-
trip_id: '京都|2026-04-01|user-verbatim',
|
|
235
|
-
destination: '京都',
|
|
236
|
-
start: '2026-04-01',
|
|
237
|
-
source: 'user-verbatim',
|
|
238
|
-
evidence: '原话',
|
|
239
|
-
ts: '2026-08-01T00:00:00Z'
|
|
240
|
-
}) + '\n');
|
|
241
|
-
writeFileSync(join(mdir, 'companions.json'), JSON.stringify([
|
|
242
|
-
{
|
|
243
|
-
schema: 'companion_profile.v1',
|
|
244
|
-
companion_id: '妈妈',
|
|
245
|
-
label: '妈妈',
|
|
246
|
-
constraints: {
|
|
247
|
-
mobility: '步行≤3h'
|
|
248
|
-
},
|
|
249
|
-
evidence: [
|
|
250
|
-
'原话'
|
|
251
|
-
],
|
|
252
|
-
ts: '2026-08-01T00:00:00Z'
|
|
253
|
-
}
|
|
254
|
-
]));
|
|
130
|
+
mkdirSync(mdir, { recursive: true });
|
|
131
|
+
writeFileSync(join(mdir, 'motivation-profile.json'), JSON.stringify({ weights: { curiosity: 0.6 }, evidence: ['原话A'], hard: {}, updated_at: '2026-08-01T00:00:00Z' }));
|
|
132
|
+
writeFileSync(join(mdir, 'wish-pool.json'), JSON.stringify([{ wish_id: 'wLEGACY1', name: '京都', conditions: { days: 7 }, added_at: '2026-08-01T00:00:00Z' }]));
|
|
133
|
+
writeFileSync(join(mdir, 'memory-utility.jsonl'), JSON.stringify({ schema: 'memory_utility_observation.v0', event_id: 'wLEGACY1|recalled||', wish_id: 'wLEGACY1', kind: 'recalled', ts: '2026-08-02T00:00:00Z' }) + '\n');
|
|
134
|
+
writeFileSync(join(mdir, 'trips.jsonl'), JSON.stringify({ schema: 'travel_timeline.v1', trip_id: '京都|2026-04-01|user-verbatim', destination: '京都', start: '2026-04-01', source: 'user-verbatim', evidence: '原话', ts: '2026-08-01T00:00:00Z' }) + '\n');
|
|
135
|
+
writeFileSync(join(mdir, 'companions.json'), JSON.stringify([{ schema: 'companion_profile.v1', companion_id: '妈妈', label: '妈妈', constraints: { mobility: '步行≤3h' }, evidence: ['原话'], ts: '2026-08-01T00:00:00Z' }]));
|
|
255
136
|
const m1 = ensureLedger(mroot);
|
|
256
137
|
assert(m1.readMotivation()?.weights?.['curiosity'] === 0.6, '迁移:画像整档入账');
|
|
257
138
|
assert(String(m1.readWishPool()[0]?.wish_id) === 'wLEGACY1', '迁移:愿望保留原主键');
|
|
@@ -260,38 +141,104 @@ assert(existsSync(join(mdir, 'pre-ledger-backup', 'wish-pool.json')), '迁移:
|
|
|
260
141
|
const migratedCount = m1.countEvents();
|
|
261
142
|
ensureLedger(mroot);
|
|
262
143
|
assert(m1.countEvents() === migratedCount, '重复 ensure 不重复导入(kv 旗标 + 幂等键双保险)');
|
|
144
|
+
const tenantMroot = mkdtempSync(join(tmpdir(), 'gotry-migrate-tenant-boundary-'));
|
|
145
|
+
const tenantMdir = join(tenantMroot, 'gotry-state');
|
|
146
|
+
mkdirSync(tenantMdir, { recursive: true });
|
|
147
|
+
writeFileSync(join(tenantMdir, 'wish-pool.json'), JSON.stringify([{ wish_id: 'wLOCALLEGACY', name: '本地旧愿望', conditions: { days: 2 }, added_at: '2026-08-01T00:00:00Z' }]));
|
|
148
|
+
assert(!ledgerExists(tenantMroot), '迁移边界:非 local fallback 前 DB 不存在');
|
|
149
|
+
assert(readWishPoolWithFallback(tenantMroot, 'tenant-a').length === 0 && !ledgerExists(tenantMroot), '迁移边界:非 local 只读 fallback 不读取旧本地 JSON,也不创建 DB');
|
|
150
|
+
assert(existsSync(join(tenantMdir, 'wish-pool.json')), '迁移边界:非 local 只读 fallback 不触碰 legacy 文件');
|
|
151
|
+
const tenantFirst = ensureLedger(tenantMroot, 'tenant-a');
|
|
152
|
+
const localLegacy = ensureLedger(tenantMroot);
|
|
153
|
+
assert(tenantFirst.readWishPool().length === 0, '迁移边界:非 local 首次打开不把旧文件猜入该租户');
|
|
154
|
+
assert(localLegacy.readWishPool()[0]?.wish_id === 'wLOCALLEGACY', '迁移边界:旧 JSON/JSONL 只可安全归入默认 local');
|
|
155
|
+
assert(localLegacy.db.prepare('SELECT COUNT(*) AS n FROM events WHERE tenant_id != \'local\'').get().n === 0, '迁移边界:旧文件导入事件 tenant_id 全为 local,不伪造历史归属');
|
|
156
|
+
const v1root = mkdtempSync(join(tmpdir(), 'gotry-v1-tenant-migration-'));
|
|
157
|
+
mkdirSync(join(v1root, 'gotry-state'), { recursive: true });
|
|
158
|
+
const v1db = new Database(ledgerDbPath(v1root));
|
|
159
|
+
v1db.exec(`
|
|
160
|
+
CREATE TABLE events (
|
|
161
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
162
|
+
ts TEXT NOT NULL,
|
|
163
|
+
actor TEXT NOT NULL,
|
|
164
|
+
kind TEXT NOT NULL,
|
|
165
|
+
subject_id TEXT NOT NULL DEFAULT '',
|
|
166
|
+
payload TEXT NOT NULL,
|
|
167
|
+
idem_key TEXT,
|
|
168
|
+
run_id TEXT
|
|
169
|
+
);
|
|
170
|
+
CREATE UNIQUE INDEX events_idem ON events(idem_key) WHERE idem_key IS NOT NULL;
|
|
171
|
+
`);
|
|
172
|
+
v1db.prepare('INSERT INTO events (ts, actor, kind, subject_id, payload, idem_key, run_id) VALUES (?, ?, ?, ?, ?, ?, NULL)')
|
|
173
|
+
.run('2026-08-28T00:00:00.000Z', 'system:v1', 'wish.added', 'wV1', JSON.stringify({ wish: { wish_id: 'wV1', name: 'v1旧愿望', reason: '', conditions: { days: 4 }, added_at: '2026-08-28T00:00:00.000Z' } }), 'v1:wish');
|
|
174
|
+
v1db.close();
|
|
175
|
+
const v1Tenant = ensureLedger(v1root, 'tenant-a');
|
|
176
|
+
const v1Local = ensureLedger(v1root);
|
|
177
|
+
assert(v1Tenant.readWishPool().length === 0, 'schema v1→v2:非 local 打开旧 DB 不吸收 local 事件');
|
|
178
|
+
assert(v1Local.readWishPool()[0]?.wish_id === 'wV1', 'schema v1→v2:旧 events 回填 local 后重建 local 投影');
|
|
179
|
+
assert(v1Local.db.prepare('SELECT COUNT(*) AS n FROM events WHERE tenant_id = \'local\'').get().n === 1, 'schema v1→v2:events tenant_id 回填为 local 且不丢 seq/idempotency');
|
|
180
|
+
const tenantMrootProc = mkdtempSync(join(tmpdir(), 'gotry-migrate-tenant-boundary-proc-'));
|
|
181
|
+
const tenantMdirProc = join(tenantMrootProc, 'gotry-state');
|
|
182
|
+
mkdirSync(tenantMdirProc, { recursive: true });
|
|
183
|
+
writeFileSync(join(tenantMdirProc, 'wish-pool.json'), JSON.stringify([{ wish_id: 'wPROCLEGACY', name: '子进程旧愿望', reason: 'legacy-local', conditions: { days: 3 }, added_at: '2026-08-03T00:00:00Z' }]));
|
|
184
|
+
const legacyProc = spawnSync('npx', ['tsx', '--eval', `
|
|
185
|
+
import { ensureLedger, ledgerExists, readWishPoolWithFallback } from './src/state-ledger.js'
|
|
186
|
+
const root = ${JSON.stringify(tenantMrootProc)}
|
|
187
|
+
if (ledgerExists(root)) process.exit(2)
|
|
188
|
+
if (readWishPoolWithFallback(root, 'tenant-a').length !== 0 || ledgerExists(root)) process.exit(3)
|
|
189
|
+
const tenant = ensureLedger(root, 'tenant-a')
|
|
190
|
+
const local = ensureLedger(root)
|
|
191
|
+
const foreign = local.db.prepare("SELECT COUNT(*) AS n FROM events WHERE tenant_id != 'local'").get().n
|
|
192
|
+
if (tenant.readWishPool().length !== 0 || local.readWishPool()[0]?.wish_id !== 'wPROCLEGACY' || foreign !== 0) process.exit(4)
|
|
193
|
+
`], { encoding: 'utf-8', cwd: process.cwd(), env: process.env });
|
|
194
|
+
assert(legacyProc.status === 0, `legacy JSON 跨进程首次非 local 迁移只归 local 且 fallback 不创建 DB(实际 ${legacyProc.status}:${(legacyProc.stderr ?? '').slice(0, 300)})`);
|
|
195
|
+
const v1rootProc = mkdtempSync(join(tmpdir(), 'gotry-v1-tenant-migration-proc-'));
|
|
196
|
+
mkdirSync(join(v1rootProc, 'gotry-state'), { recursive: true });
|
|
197
|
+
const v1dbProc = new Database(ledgerDbPath(v1rootProc));
|
|
198
|
+
v1dbProc.exec(`
|
|
199
|
+
CREATE TABLE events (
|
|
200
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
201
|
+
ts TEXT NOT NULL,
|
|
202
|
+
actor TEXT NOT NULL,
|
|
203
|
+
kind TEXT NOT NULL,
|
|
204
|
+
subject_id TEXT NOT NULL DEFAULT '',
|
|
205
|
+
payload TEXT NOT NULL,
|
|
206
|
+
idem_key TEXT,
|
|
207
|
+
run_id TEXT
|
|
208
|
+
);
|
|
209
|
+
CREATE UNIQUE INDEX events_idem ON events(idem_key) WHERE idem_key IS NOT NULL;
|
|
210
|
+
`);
|
|
211
|
+
v1dbProc.prepare('INSERT INTO events (ts, actor, kind, subject_id, payload, idem_key, run_id) VALUES (?, ?, ?, ?, ?, ?, NULL)')
|
|
212
|
+
.run('2026-08-28T00:00:00.000Z', 'system:v1', 'wish.added', 'wV1PROC', JSON.stringify({ wish: { wish_id: 'wV1PROC', name: 'v1子进程旧愿望', reason: 'v1-local', conditions: { days: 5 }, added_at: '2026-08-28T00:00:00.000Z' } }), 'v1proc:wish');
|
|
213
|
+
v1dbProc.close();
|
|
214
|
+
const v1Proc = spawnSync('npx', ['tsx', '--eval', `
|
|
215
|
+
import { ensureLedger } from './src/state-ledger.js'
|
|
216
|
+
const root = ${JSON.stringify(v1rootProc)}
|
|
217
|
+
const tenant = ensureLedger(root, 'tenant-a')
|
|
218
|
+
const local = ensureLedger(root)
|
|
219
|
+
const localRows = local.db.prepare("SELECT COUNT(*) AS n FROM events WHERE tenant_id = 'local'").get().n
|
|
220
|
+
if (tenant.readWishPool().length !== 0 || local.readWishPool()[0]?.wish_id !== 'wV1PROC' || localRows !== 1) process.exit(5)
|
|
221
|
+
`], { encoding: 'utf-8', cwd: process.cwd(), env: process.env });
|
|
222
|
+
assert(v1Proc.status === 0, `schema v1→v2 跨进程首次非 local 打开不吸收 local 事件(实际 ${v1Proc.status}:${(v1Proc.stderr ?? '').slice(0, 300)})`);
|
|
223
|
+
// ---- 9:durable 工单崩溃恢复(exactly-once) ----------------------------------------
|
|
263
224
|
const wroot = mkdtempSync(join(tmpdir(), 'gotry-wf-'));
|
|
264
225
|
const countFile = join(wroot, 'solve-count.txt');
|
|
265
226
|
writeFileSync(countFile, '');
|
|
266
|
-
const crash = spawnSync('npx', [
|
|
267
|
-
'tsx',
|
|
268
|
-
'scripts/ledger-workflow-crash.ts',
|
|
269
|
-
wroot,
|
|
270
|
-
'dp-crash1',
|
|
271
|
-
countFile
|
|
272
|
-
], {
|
|
273
|
-
encoding: 'utf-8'
|
|
274
|
-
});
|
|
227
|
+
const crash = spawnSync('npx', ['tsx', 'scripts/ledger-workflow-crash.ts', wroot, 'dp-crash1', countFile], { encoding: 'utf-8' });
|
|
275
228
|
assert(crash.status === 9, `崩溃探针按设计 exit 9(实际 ${crash.status}:${(crash.stderr ?? '').slice(0, 200)})`);
|
|
276
229
|
const runRow = openLedgerIfExists(wroot)?.getWorkflowRun('dp-crash1');
|
|
277
230
|
assert(runRow?.status === 'pending', '崩溃后工单仍 pending(账本权威未损)');
|
|
278
231
|
const count1 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
|
|
279
232
|
assert(count1 === 1, '崩溃前真实求解恰 1 次');
|
|
280
|
-
const resume = spawnSync('npx', [
|
|
281
|
-
'
|
|
282
|
-
'scripts/async-collect.ts',
|
|
283
|
-
'dp-crash1',
|
|
284
|
-
wroot
|
|
285
|
-
], {
|
|
286
|
-
encoding: 'utf-8',
|
|
287
|
-
env: {
|
|
288
|
-
...process.env,
|
|
289
|
-
GOTRY_SOLVE_COUNT_FILE: countFile
|
|
290
|
-
}
|
|
233
|
+
const resume = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', 'dp-crash1', wroot], {
|
|
234
|
+
encoding: 'utf-8', env: { ...process.env, GOTRY_SOLVE_COUNT_FILE: countFile },
|
|
291
235
|
});
|
|
292
236
|
const failedOutcome = terminalOutcomeOf(resume.stdout ?? '');
|
|
293
237
|
assert(resume.status === 2, `非4/4 恢复进程 exit 2(实际 ${resume.status}:${(resume.stderr ?? '').slice(0, 300)})`);
|
|
294
|
-
assert(failedOutcome?.status === 'failed'
|
|
238
|
+
assert(failedOutcome?.status === 'failed'
|
|
239
|
+
&& failedOutcome.ticket_id === 'dp-crash1'
|
|
240
|
+
&& failedOutcome.passed < failedOutcome.total
|
|
241
|
+
&& failedOutcome.failed_checks.length > 0, '非4/4 输出 gotry_async_terminal.v1/failed 与失败项');
|
|
295
242
|
const count2 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
|
|
296
243
|
assert(count2 === 1, '恢复时 done 步骤零重算(exactly-once:求解计数仍 1,不重复花钱)');
|
|
297
244
|
const failedDeliverablePath = join(wroot, 'gotry-state', 'async', 'dp-crash1.deliverable.md');
|
|
@@ -299,77 +246,50 @@ assert(existsSync(failedDeliverablePath), '交付物视图已落盘(.deliverable
|
|
|
299
246
|
const failedLedger = openLedgerIfExists(wroot);
|
|
300
247
|
assert(failedLedger?.getWorkflowRun('dp-crash1')?.status === 'failed', '非4/4 账本终态 failed');
|
|
301
248
|
assert(JSON.stringify(failedLedger?.getWorkflowTerminalOutcome('dp-crash1')) === JSON.stringify(failedOutcome), '非4/4 结构化终态随 async.failed 事件落账');
|
|
302
|
-
const replay = spawnSync('npx', [
|
|
303
|
-
'
|
|
304
|
-
'scripts/async-collect.ts',
|
|
305
|
-
'dp-crash1',
|
|
306
|
-
wroot
|
|
307
|
-
], {
|
|
308
|
-
encoding: 'utf-8',
|
|
309
|
-
env: {
|
|
310
|
-
...process.env,
|
|
311
|
-
GOTRY_SOLVE_COUNT_FILE: countFile
|
|
312
|
-
}
|
|
249
|
+
const replay = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', 'dp-crash1', wroot], {
|
|
250
|
+
encoding: 'utf-8', env: { ...process.env, GOTRY_SOLVE_COUNT_FILE: countFile },
|
|
313
251
|
});
|
|
314
252
|
assert(replay.status === 2 && JSON.stringify(terminalOutcomeOf(replay.stdout ?? '')) === JSON.stringify(failedOutcome), 'failed 终态复诵保持 exit 2 与同一结构化结果');
|
|
315
253
|
const count3 = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
|
|
316
254
|
assert(count3 === 1, '终态复诵零重算');
|
|
317
255
|
const failedDeliverable = readFileSync(failedDeliverablePath, 'utf-8');
|
|
318
256
|
rmSync(failedDeliverablePath);
|
|
319
|
-
const replayAfterMissingView = spawnSync('npx', [
|
|
320
|
-
'
|
|
321
|
-
'scripts/async-collect.ts',
|
|
322
|
-
'dp-crash1',
|
|
323
|
-
wroot
|
|
324
|
-
], {
|
|
325
|
-
encoding: 'utf-8',
|
|
326
|
-
env: {
|
|
327
|
-
...process.env,
|
|
328
|
-
GOTRY_SOLVE_COUNT_FILE: countFile
|
|
329
|
-
}
|
|
257
|
+
const replayAfterMissingView = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', 'dp-crash1', wroot], {
|
|
258
|
+
encoding: 'utf-8', env: { ...process.env, GOTRY_SOLVE_COUNT_FILE: countFile },
|
|
330
259
|
});
|
|
331
|
-
assert(replayAfterMissingView.status === 2
|
|
260
|
+
assert(replayAfterMissingView.status === 2
|
|
261
|
+
&& JSON.stringify(terminalOutcomeOf(replayAfterMissingView.stdout ?? '')) === JSON.stringify(failedOutcome), 'failed 终态缺失视图时仍复诵同一 exit 2 与结构化结果');
|
|
332
262
|
assert(existsSync(failedDeliverablePath) && readFileSync(failedDeliverablePath, 'utf-8') === failedDeliverable, 'failed 终态复诵重建缺失 .deliverable.md 视图');
|
|
333
263
|
const countAfterViewRepair = readFileSync(countFile, 'utf-8').split('\n').filter(Boolean).length;
|
|
334
264
|
assert(countAfterViewRepair === 1, '终态视图修复零重算');
|
|
265
|
+
// state-cli tick 是另一个 durable driver;即使旧调用未显式透传 outcome,
|
|
266
|
+
// settle 层也必须从账本中的权威 state/step 恢复机器终态,不能误结算为成功。
|
|
335
267
|
const tickRoot = mkdtempSync(join(tmpdir(), 'gotry-wf-tick-failed-'));
|
|
336
268
|
const tickTicket = {
|
|
337
269
|
id: 'dp-tick-failed1',
|
|
338
270
|
objective: 'state-cli 非4/4 机器终态回归',
|
|
339
271
|
requestedAt: new Date().toISOString(),
|
|
340
|
-
etaLabel: '秒级'
|
|
272
|
+
etaLabel: '秒级',
|
|
341
273
|
};
|
|
342
274
|
const tickState = {
|
|
343
|
-
calendar: {
|
|
344
|
-
year: 2026,
|
|
345
|
-
assertedWeekdays: {}
|
|
346
|
-
},
|
|
275
|
+
calendar: { year: 2026, assertedWeekdays: {} },
|
|
347
276
|
profile: {},
|
|
348
277
|
gates: [],
|
|
349
|
-
wishes: []
|
|
278
|
+
wishes: [],
|
|
350
279
|
};
|
|
351
280
|
await persistAsyncTicket(tickTicket, tickState, tickRoot);
|
|
352
|
-
const tick = spawnSync('npx', [
|
|
353
|
-
'tsx',
|
|
354
|
-
'scripts/state-cli.ts',
|
|
355
|
-
'tick',
|
|
356
|
-
tickRoot
|
|
357
|
-
], {
|
|
358
|
-
encoding: 'utf-8'
|
|
359
|
-
});
|
|
281
|
+
const tick = spawnSync('npx', ['tsx', 'scripts/state-cli.ts', 'tick', tickRoot], { encoding: 'utf-8' });
|
|
360
282
|
assert(tick.status === 0, `state-cli tick 完成回收(实际 ${tick.status}:${(tick.stderr ?? '').slice(0, 200)})`);
|
|
361
283
|
const tickLedger = openLedgerIfExists(tickRoot);
|
|
362
284
|
const tickOutcome = tickLedger?.getWorkflowTerminalOutcome(tickTicket.id);
|
|
363
|
-
assert(tickLedger?.getWorkflowRun(tickTicket.id)?.status === 'failed'
|
|
364
|
-
|
|
365
|
-
'
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
});
|
|
372
|
-
assert(tickReplay.status === 2 && JSON.stringify(terminalOutcomeOf(tickReplay.stdout ?? '')) === JSON.stringify(tickOutcome), 'state-cli 写入的 failed 终态可由 collector 幂等复诵为同一 outcome/exit 2');
|
|
285
|
+
assert(tickLedger?.getWorkflowRun(tickTicket.id)?.status === 'failed'
|
|
286
|
+
&& tickOutcome?.schema === 'gotry_async_terminal.v1'
|
|
287
|
+
&& tickOutcome.status === 'failed'
|
|
288
|
+
&& tickOutcome.passed === 0
|
|
289
|
+
&& tickOutcome.failed_checks.length === 4, 'state-cli tick 未透传 outcome 时仍从权威账本恢复非4/4 failed 终态');
|
|
290
|
+
const tickReplay = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', tickTicket.id, tickRoot], { encoding: 'utf-8' });
|
|
291
|
+
assert(tickReplay.status === 2
|
|
292
|
+
&& JSON.stringify(terminalOutcomeOf(tickReplay.stdout ?? '')) === JSON.stringify(tickOutcome), 'state-cli 写入的 failed 终态可由 collector 幂等复诵为同一 outcome/exit 2');
|
|
373
293
|
const successRoot = mkdtempSync(join(tmpdir(), 'gotry-wf-success-'));
|
|
374
294
|
const successCountFile = join(successRoot, 'solve-count.txt');
|
|
375
295
|
writeFileSync(successCountFile, '');
|
|
@@ -377,73 +297,49 @@ const successTicket = {
|
|
|
377
297
|
id: 'dp-success1',
|
|
378
298
|
objective: '4/4 机器终态回归',
|
|
379
299
|
requestedAt: new Date().toISOString(),
|
|
380
|
-
etaLabel: '秒级'
|
|
300
|
+
etaLabel: '秒级',
|
|
381
301
|
};
|
|
382
302
|
const successSpec = parseFlightPackToSpec(JSON.parse(readFileSync(join('..', 'data', 'flights_2026.json'), 'utf-8')));
|
|
383
303
|
successSpec.budgetCny = 9000;
|
|
384
304
|
const successState = {
|
|
385
|
-
calendar: {
|
|
386
|
-
year: 2026,
|
|
387
|
-
assertedWeekdays: {}
|
|
388
|
-
},
|
|
305
|
+
calendar: { year: 2026, assertedWeekdays: {} },
|
|
389
306
|
profile: {},
|
|
390
307
|
gates: [],
|
|
391
308
|
wishes: [],
|
|
392
|
-
spec: successSpec
|
|
309
|
+
spec: successSpec,
|
|
393
310
|
};
|
|
394
311
|
await persistAsyncTicket(successTicket, successState, successRoot);
|
|
395
|
-
const success = spawnSync('npx', [
|
|
396
|
-
'
|
|
397
|
-
'scripts/async-collect.ts',
|
|
398
|
-
successTicket.id,
|
|
399
|
-
successRoot
|
|
400
|
-
], {
|
|
401
|
-
encoding: 'utf-8',
|
|
402
|
-
env: {
|
|
403
|
-
...process.env,
|
|
404
|
-
GOTRY_SOLVE_COUNT_FILE: successCountFile
|
|
405
|
-
}
|
|
312
|
+
const success = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', successTicket.id, successRoot], {
|
|
313
|
+
encoding: 'utf-8', env: { ...process.env, GOTRY_SOLVE_COUNT_FILE: successCountFile },
|
|
406
314
|
});
|
|
407
315
|
const successOutcome = terminalOutcomeOf(success.stdout ?? '');
|
|
408
|
-
assert(success.status === 0
|
|
316
|
+
assert(success.status === 0
|
|
317
|
+
&& successOutcome?.status === 'succeeded'
|
|
318
|
+
&& successOutcome.ticket_id === successTicket.id
|
|
319
|
+
&& successOutcome.passed === 4
|
|
320
|
+
&& successOutcome.total === 4
|
|
321
|
+
&& successOutcome.failed_checks.length === 0, `4/4 输出 gotry_async_terminal.v1/succeeded 且 exit 0(实际 ${success.status})`);
|
|
409
322
|
const successLedger = openLedgerIfExists(successRoot);
|
|
410
|
-
assert(successLedger?.getWorkflowRun(successTicket.id)?.status === 'settled'
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
'
|
|
414
|
-
successTicket.id,
|
|
415
|
-
successRoot
|
|
416
|
-
], {
|
|
417
|
-
encoding: 'utf-8',
|
|
418
|
-
env: {
|
|
419
|
-
...process.env,
|
|
420
|
-
GOTRY_SOLVE_COUNT_FILE: successCountFile
|
|
421
|
-
}
|
|
323
|
+
assert(successLedger?.getWorkflowRun(successTicket.id)?.status === 'settled'
|
|
324
|
+
&& successLedger.getWorkflowTerminalOutcome(successTicket.id)?.['status'] === 'succeeded', '4/4 账本终态 settled 且结构化结果随 async.settled 落账');
|
|
325
|
+
const successReplay = spawnSync('npx', ['tsx', 'scripts/async-collect.ts', successTicket.id, successRoot], {
|
|
326
|
+
encoding: 'utf-8', env: { ...process.env, GOTRY_SOLVE_COUNT_FILE: successCountFile },
|
|
422
327
|
});
|
|
423
328
|
const successCount = readFileSync(successCountFile, 'utf-8').split('\n').filter(Boolean).length;
|
|
424
|
-
assert(successReplay.status === 0
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
});
|
|
432
|
-
const pw2 = ledger.requestPendingWrite({
|
|
433
|
-
idemKey: 'booking:demo-1',
|
|
434
|
-
seam: 'flight-order-confirm',
|
|
435
|
-
payload: {
|
|
436
|
-
flight: 'MU123'
|
|
437
|
-
}
|
|
438
|
-
});
|
|
329
|
+
assert(successReplay.status === 0
|
|
330
|
+
&& successCount === 1
|
|
331
|
+
&& JSON.stringify(terminalOutcomeOf(successReplay.stdout ?? '')) === JSON.stringify(successOutcome), 'succeeded 终态复诵保持 exit 0、同一结构化结果且零重算');
|
|
332
|
+
// ---- 10:pending_writes saga + what-if 分叉 -----------------------------------------
|
|
333
|
+
const pw1 = ledger.requestPendingWrite({ idemKey: 'booking:demo-1', seam: 'flight-order-confirm', payload: { flight: 'MU123' } });
|
|
334
|
+
const pw2 = ledger.requestPendingWrite({ idemKey: 'booking:demo-1', seam: 'flight-order-confirm', payload: { flight: 'MU123' } });
|
|
439
335
|
assert(pw1.created === true && pw2.created === false, 'pending_writes 幂等键去重(同一确认不可能登记两次)');
|
|
440
336
|
const cf1 = ledger.confirmPendingWrite('booking:demo-1', 'PNR-ABC');
|
|
441
337
|
const cf2 = ledger.confirmPendingWrite('booking:demo-1', 'PNR-ABC2');
|
|
442
338
|
assert(cf1.ok === true && cf2.ok === false && cf2.status === 'confirmed', 'L3 确认只能发生一次,二连击被拒');
|
|
443
339
|
const cp1 = ledger.compensatePendingWrite('booking:demo-1', '用户改签退款');
|
|
444
340
|
assert(cp1.ok === true && ledger.listPendingWrites()[0]?.status === 'compensated', 'saga 补偿可达(pending/confirmed → compensated)');
|
|
445
|
-
const audit = ledger.readEvents(undefined, 100).filter(
|
|
446
|
-
assert(audit.some(
|
|
341
|
+
const audit = ledger.readEvents(undefined, 100).filter(e => e.kind.startsWith('write.'));
|
|
342
|
+
assert(audit.some(e => e.kind === 'write.pending') && audit.some(e => e.kind === 'write.confirmed') && audit.some(e => e.kind === 'write.compensated'), '写权审计事件链完整(append-only,WriteGate 的 receipt 落点)');
|
|
447
343
|
const forkPath = join(root, 'whatif.db');
|
|
448
344
|
ledger.forkWhatIf(forkPath);
|
|
449
345
|
const forkDb = new Database(forkPath);
|
|
@@ -451,45 +347,134 @@ forkDb.prepare("INSERT INTO events (ts, actor, kind, subject_id, payload) VALUES
|
|
|
451
347
|
const forkCount = forkDb.prepare('SELECT COUNT(*) AS n FROM events').get().n;
|
|
452
348
|
forkDb.close();
|
|
453
349
|
assert(ledger.countEvents() === forkCount - 1, 'what-if 分叉:副本写入不触正本(VACUUM INTO 预演)');
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
localLedger
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
assert(
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
assert(
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
}
|
|
350
|
+
// ---- 11:双形态(ADR-16):tenant scope 贯穿 append/read/fold/rebuild/API ----------------
|
|
351
|
+
const tenantRoot = mkdtempSync(join(tmpdir(), 'gotry-tenant-scope-'));
|
|
352
|
+
const localLedger = ensureLedger(tenantRoot); // 默认 tenant='local'
|
|
353
|
+
const tenantA = ensureLedger(tenantRoot, 'tenant-a');
|
|
354
|
+
const tenantB = ensureLedger(tenantRoot, 'tenant-b');
|
|
355
|
+
const localWish = localLedger.appendWish({ name: '同 id 愿望', reason: 'local0', conditions: { owner: 'local0' } });
|
|
356
|
+
const wishA = tenantA.appendWish({ name: '同 id 愿望', reason: 'a0', conditions: { owner: 'a0' } });
|
|
357
|
+
const wishB = tenantB.appendWish({ name: '同 id 愿望', reason: 'b0', conditions: { owner: 'b0' } });
|
|
358
|
+
tenantA.appendWish({ name: '同 id 愿望', reason: 'a1', conditions: { owner: 'a1' } });
|
|
359
|
+
tenantB.appendWish({ name: '同 id 愿望', reason: 'b1', conditions: { owner: 'b1' } });
|
|
360
|
+
const utilA = tenantA.appendUtilityEvent({ wish_id: wishA.wish_id, kind: 'recalled', ts: '2026-09-08T00:00:00.000Z', ctx: 'same-ctx' });
|
|
361
|
+
const utilB = tenantB.appendUtilityEvent({ wish_id: wishB.wish_id, kind: 'recalled', ts: '2026-09-08T00:00:00.000Z', ctx: 'same-ctx' });
|
|
362
|
+
assert(localWish.wish_id === wishA.wish_id && wishA.wish_id === wishB.wish_id, '同业务 id/wish_id 可跨 local/tenant-a/tenant-b 并存');
|
|
363
|
+
assert(utilA.appended === true && utilB.appended === true, '同 event_id/idempotency key 跨租户独立追加(UNIQUE tenant-scoped)');
|
|
364
|
+
const tenantRows = localLedger.db.prepare('SELECT tenant_id, kind, subject_id, idem_key FROM events WHERE subject_id = ? ORDER BY tenant_id, seq').all(wishA.wish_id);
|
|
365
|
+
assert(tenantRows.filter(r => r.kind === 'wish.added').map(r => r.tenant_id).sort().join(',') === 'local,tenant-a,tenant-b'
|
|
366
|
+
&& tenantRows.filter(r => r.kind === 'wish.updated').map(r => r.tenant_id).sort().join(',') === 'tenant-a,tenant-b', '非 local 写入 events 保留真实 owner,默认 local 兼容不变');
|
|
367
|
+
assert(tenantA.readEvents(undefined, 100).every(e => e.tenant_id === 'tenant-a')
|
|
368
|
+
&& tenantB.readEvents(undefined, 100).every(e => e.tenant_id === 'tenant-b')
|
|
369
|
+
&& localLedger.readEvents(undefined, 100).every(e => e.tenant_id === 'local'), 'readEvents 默认与 kind 过滤前均先按 ledger.tenant 隔离');
|
|
370
|
+
assert(tenantA.readEvents('wish.added', 10).length === 1 && tenantB.readEvents('wish.added', 10).length === 1, 'readEvents(kind) 也按租户过滤');
|
|
371
|
+
assert(!localLedger.readWishPool().some(w => w.conditions.owner === 'a1')
|
|
372
|
+
&& !tenantA.readWishPool().some(w => w.conditions.owner === 'b1'), '直写投影:默认 local/tenant-a/tenant-b 同 id 互不覆盖');
|
|
373
|
+
const localBeforeRebuild = JSON.stringify(localLedger.readWishPool());
|
|
374
|
+
const aBeforeRebuild = JSON.stringify(tenantA.readWishPool());
|
|
375
|
+
const bBeforeRebuild = JSON.stringify(tenantB.readWishPool());
|
|
376
|
+
tenantA.rebuildProjections();
|
|
377
|
+
assert(JSON.stringify(tenantA.readWishPool()) === aBeforeRebuild, 'tenant-a rebuild 只读自己的 events 并还原交错 update 后投影');
|
|
378
|
+
assert(JSON.stringify(tenantB.readWishPool()) === bBeforeRebuild && JSON.stringify(localLedger.readWishPool()) === localBeforeRebuild, 'tenant-a rebuild 不改写 tenant-b/local 投影');
|
|
379
|
+
tenantA.rebuildProjections();
|
|
380
|
+
assert(JSON.stringify(tenantA.readWishPool()) === aBeforeRebuild, 'tenant-a 重复 rebuild 幂等');
|
|
381
|
+
tenantB.rebuildProjections();
|
|
382
|
+
assert(JSON.stringify(tenantB.readWishPool()) === bBeforeRebuild, 'tenant-b rebuild 在 tenant-a 同 item_id 存在时不串读 wish.updated fold 行');
|
|
383
|
+
localLedger.rebuildProjections();
|
|
384
|
+
assert(JSON.stringify(localLedger.readWishPool()) === localBeforeRebuild, '默认 local rebuild 兼容且不吸收非 local events');
|
|
385
|
+
const reopenScript = `
|
|
386
|
+
import { ensureLedger } from './src/state-ledger.js'
|
|
387
|
+
const root = ${JSON.stringify(tenantRoot)}
|
|
388
|
+
const a = ensureLedger(root, 'tenant-a')
|
|
389
|
+
const b = ensureLedger(root, 'tenant-b')
|
|
390
|
+
const local = ensureLedger(root)
|
|
391
|
+
const aWish = a.readWishPool()[0]
|
|
392
|
+
const bWish = b.readWishPool()[0]
|
|
393
|
+
const localWish = local.readWishPool()[0]
|
|
394
|
+
const ok = aWish?.name === '同 id 愿望'
|
|
395
|
+
&& bWish?.name === '同 id 愿望'
|
|
396
|
+
&& localWish?.name === '同 id 愿望'
|
|
397
|
+
&& aWish?.conditions?.owner === 'a1'
|
|
398
|
+
&& bWish?.conditions?.owner === 'b1'
|
|
399
|
+
&& localWish?.conditions?.owner === 'local0'
|
|
400
|
+
&& a.readEvents(undefined, 100).every(e => e.tenant_id === 'tenant-a')
|
|
401
|
+
&& b.readEvents(undefined, 100).every(e => e.tenant_id === 'tenant-b')
|
|
402
|
+
&& local.readEvents(undefined, 100).every(e => e.tenant_id === 'local')
|
|
403
|
+
if (!ok) {
|
|
404
|
+
console.error(JSON.stringify({ aWish, bWish, localWish, aEvents: a.readEvents(undefined, 100), bEvents: b.readEvents(undefined, 100), localEvents: local.readEvents(undefined, 100) }))
|
|
405
|
+
process.exit(1)
|
|
406
|
+
}
|
|
407
|
+
`;
|
|
408
|
+
const reopenTenant = spawnSync('npx', ['tsx', '--eval', reopenScript], { encoding: 'utf-8', cwd: process.cwd(), env: process.env });
|
|
409
|
+
assert(reopenTenant.status === 0, `跨进程 reopen 后租户事件/投影仍隔离(实际 ${reopenTenant.status}:${(reopenTenant.stderr ?? '').slice(0, 300)})`);
|
|
410
|
+
assert(localLedger.tenant === 'local' && tenantA.tenant === 'tenant-a' && tenantB.tenant === 'tenant-b', 'tenant_id 从第一天就是一等字段');
|
|
411
|
+
const probeASeq = tenantA.insertEvent({ actor: 'test:tenant-scope', kind: 'tenant.probe', subjectId: 'probe-a', payload: { tenant: 'A' }, ts: '2026-09-08T00:00:10.000Z' });
|
|
412
|
+
const probeBSeq = tenantB.insertEvent({ actor: 'test:tenant-scope', kind: 'tenant.probe', subjectId: 'probe-b-newer', payload: { tenant: 'B' }, ts: '2026-09-08T00:00:11.000Z' });
|
|
413
|
+
const aProbe = tenantA.readEvents('tenant.probe', 1)[0];
|
|
414
|
+
assert((probeASeq ?? 0) < (probeBSeq ?? 0) && aProbe?.tenant_id === 'tenant-a' && aProbe.subject_id === 'probe-a', 'readEvents(kind, limit) 先按 tenant 过滤再截断:tenant-b 较新事件不抢 tenant-a limit=1');
|
|
415
|
+
const foldFixtureRoot = mkdtempSync(join(tmpdir(), 'gotry-tenant-fold-fixture-'));
|
|
416
|
+
const foldA = ensureLedger(foldFixtureRoot, 'tenant-a');
|
|
417
|
+
const foldB = ensureLedger(foldFixtureRoot, 'tenant-b');
|
|
418
|
+
const sameFoldId = 'w-fold-same-id';
|
|
419
|
+
const foldWishA = { wish_id: sameFoldId, name: '同主键反例', reason: 'A-fixed-reason', conditions: { owner: 'A-fixed', days: 5 }, added_at: '2026-09-08T00:00:00.000Z' };
|
|
420
|
+
const foldWishB = { wish_id: sameFoldId, name: '同主键反例', reason: 'B-fixed-reason', conditions: { owner: 'B-fixed', days: 9 }, added_at: '2026-09-08T00:00:01.000Z' };
|
|
421
|
+
const foldInsert = foldA.db.prepare(`INSERT INTO events (tenant_id, ts, actor, kind, subject_id, payload, idem_key, run_id) VALUES (?, ?, 'test:fold-fixture', ?, ?, ?, ?, NULL)`);
|
|
422
|
+
foldInsert.run('tenant-a', '2026-09-08T00:00:00.000Z', 'wish.added', sameFoldId, JSON.stringify({ wish: foldWishA }), 'fold:a:add');
|
|
423
|
+
foldInsert.run('tenant-b', '2026-09-08T00:00:01.000Z', 'wish.added', sameFoldId, JSON.stringify({ wish: foldWishB }), 'fold:b:add');
|
|
424
|
+
foldInsert.run('tenant-b', '2026-09-08T00:00:02.000Z', 'wish.updated', sameFoldId, JSON.stringify({ wish_id: sameFoldId, patch: { muted: true } }), 'fold:b:muted');
|
|
425
|
+
const fixtureEvents = foldA.db.prepare('SELECT tenant_id, kind, subject_id FROM events WHERE subject_id = ? ORDER BY seq').all(sameFoldId);
|
|
426
|
+
assert(fixtureEvents.map(e => `${e.tenant_id}:${e.kind}`).join(',') === 'tenant-a:wish.added,tenant-b:wish.added,tenant-b:wish.updated', '确定性 fold fixture:同 wish_id 的 A/B events 拥有固定且正确 tenant 归属');
|
|
427
|
+
foldA.rebuildProjections();
|
|
428
|
+
const foldABefore = JSON.stringify(foldA.readWishPool()[0]);
|
|
429
|
+
foldB.rebuildProjections();
|
|
430
|
+
const foldAAfter = foldA.readWishPool()[0];
|
|
431
|
+
const foldBAfter = foldB.readWishPool()[0];
|
|
432
|
+
const unscopedFoldRow = foldB.db.prepare(`SELECT doc FROM projection_items WHERE subject = 'wish_pool' AND item_id = ?`).get(sameFoldId);
|
|
433
|
+
const unscopedFoldDoc = unscopedFoldRow ? JSON.parse(unscopedFoldRow.doc) : null;
|
|
434
|
+
assert(JSON.stringify(foldAAfter) === foldABefore, '精确 fold 反例:A rebuild 后字段固定,B rebuild 不改变 A 投影');
|
|
435
|
+
assert(foldBAfter?.reason === 'B-fixed-reason'
|
|
436
|
+
&& foldBAfter.conditions.owner === 'B-fixed'
|
|
437
|
+
&& foldBAfter.conditions.days === 9
|
|
438
|
+
&& foldBAfter.added_at === '2026-09-08T00:00:01.000Z'
|
|
439
|
+
&& foldBAfter.muted === true, '精确 fold 反例:B 仅 patch muted=true 后仍保留 B 的 reason/conditions/added_at');
|
|
440
|
+
assert(unscopedFoldDoc?.reason === 'A-fixed-reason', '负向控制:若 wish.updated fold 去掉 tenant 条件,该 fixture 会稳定先读 A 行并把 B 变成 A');
|
|
441
|
+
const workflowRoot = mkdtempSync(join(tmpdir(), 'gotry-tenant-workflow-'));
|
|
442
|
+
const wfA = ensureLedger(workflowRoot, 'tenant-a');
|
|
443
|
+
const wfB = ensureLedger(workflowRoot, 'tenant-b');
|
|
444
|
+
wfA.createWorkflowRun({ id: 'wf-same', goal: 'tenant-a-goal', ticket: { tenant: 'A' }, state: { tenant: 'A' } });
|
|
445
|
+
wfB.createWorkflowRun({ id: 'wf-same', goal: 'tenant-b-goal', ticket: { tenant: 'B' }, state: { tenant: 'B' } });
|
|
446
|
+
wfB.markStepIntent('wf-same', 'step-1');
|
|
447
|
+
wfB.markStepDone('wf-same', 'step-1', { ok: true });
|
|
448
|
+
wfB.settleWorkflowRun('wf-same', 'tenant-b deliverable', { schema: 'test', status: 'succeeded' });
|
|
449
|
+
const workflowRows = wfA.db.prepare('SELECT tenant_id, goal, status, deliverable FROM workflow_runs WHERE id = ? ORDER BY tenant_id').all('wf-same');
|
|
450
|
+
assert(wfA.getWorkflowRun('wf-same')?.status === 'pending'
|
|
451
|
+
&& wfB.getWorkflowRun('wf-same')?.status === 'settled'
|
|
452
|
+
&& workflowRows.map(r => `${r.tenant_id}:${r.goal}:${r.status}:${r.deliverable ?? ''}`).join('|') === 'tenant-a:tenant-a-goal:pending:|tenant-b:tenant-b-goal:settled:tenant-b deliverable', '非 local workflow_runs 同 id 行按 tenant 绑定,tenant-b settle 不改 tenant-a pending');
|
|
453
|
+
assert(wfA.getWorkflowStep('wf-same', 'step-1') === undefined && wfB.getWorkflowStep('wf-same', 'step-1')?.status === 'done', '非 local workflow_steps 行按 tenant 绑定');
|
|
454
|
+
const wfBAsyncEvents = wfB.readEvents(undefined, 20).filter(e => e.kind.startsWith('async.'));
|
|
455
|
+
assert(wfBAsyncEvents.some(e => e.kind === 'async.settled') && wfBAsyncEvents.every(e => e.tenant_id === 'tenant-b'), '非 local workflow 审计事件 owner 不回落 local且含 settled 终态');
|
|
456
|
+
const forgetRoot = mkdtempSync(join(tmpdir(), 'gotry-tenant-forget-'));
|
|
457
|
+
const forgetA = ensureLedger(forgetRoot, 'tenant-a');
|
|
458
|
+
const forgetB = ensureLedger(forgetRoot, 'tenant-b');
|
|
459
|
+
const forgetAId = forgetA.appendWish({ name: '同 id 可忘愿望', reason: 'keep-A', conditions: { owner: 'A' } }).wish_id;
|
|
460
|
+
const forgetBId = forgetB.appendWish({ name: '同 id 可忘愿望', reason: 'drop-B', conditions: { owner: 'B' } }).wish_id;
|
|
461
|
+
forgetB.forgetSubject([{ kinds: ['wish.added', 'wish.updated', 'memory_utility.event'], subjectId: forgetBId }]);
|
|
462
|
+
const forgetRows = forgetA.db.prepare('SELECT tenant_id, COUNT(*) AS n FROM events WHERE subject_id = ? GROUP BY tenant_id ORDER BY tenant_id').all(forgetAId);
|
|
463
|
+
assert(forgetAId === forgetBId && forgetA.readWishPool().length === 1 && forgetB.readWishPool().length === 0, '非 local forget 只删目标 tenant 投影,A 同 id 愿望不受影响');
|
|
464
|
+
assert(forgetRows.map(r => `${r.tenant_id}:${r.n}`).join(',') === 'tenant-a:1' && forgetB.readEvents('forget.executed', 1)[0]?.tenant_id === 'tenant-b', '非 local forget 事件删除范围与审计事件 owner 均按 tenant 绑定');
|
|
465
|
+
// ---- 收尾 --------------------------------------------------------------------------
|
|
466
|
+
rmSync(root, { recursive: true, force: true });
|
|
467
|
+
rmSync(mroot, { recursive: true, force: true });
|
|
468
|
+
rmSync(tenantMroot, { recursive: true, force: true });
|
|
469
|
+
rmSync(tenantMrootProc, { recursive: true, force: true });
|
|
470
|
+
rmSync(v1root, { recursive: true, force: true });
|
|
471
|
+
rmSync(v1rootProc, { recursive: true, force: true });
|
|
472
|
+
rmSync(tenantRoot, { recursive: true, force: true });
|
|
473
|
+
rmSync(foldFixtureRoot, { recursive: true, force: true });
|
|
474
|
+
rmSync(workflowRoot, { recursive: true, force: true });
|
|
475
|
+
rmSync(forgetRoot, { recursive: true, force: true });
|
|
476
|
+
rmSync(wroot, { recursive: true, force: true });
|
|
477
|
+
rmSync(successRoot, { recursive: true, force: true });
|
|
491
478
|
console.log(`\nLEDGER TESTS: ${pass} ok, ${fail} fail`);
|
|
492
|
-
if (fail > 0)
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
//# sourceURL=ts/scripts/ledger-tests.ts
|
|
479
|
+
if (fail > 0)
|
|
480
|
+
process.exit(1);
|