@danceiny/gotry 0.0.1-rc.16 → 0.0.1-rc.17
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 +47 -13
- package/README.zh-CN.md +20 -10
- package/bin/gotry-booking-copilot.js +53 -0
- package/bin/gotry-bootstrap.js +111 -51
- package/bin/gotry-inner.js +442 -60
- package/bin/gotry-runtime-resolution.d.ts +27 -0
- package/bin/gotry-runtime-resolution.js +50 -0
- package/bin/gotry.js +1 -1
- package/cordis.gotry-patch.yml +5 -1
- package/dist/capabilities/agent-reach-deep.js +1 -1
- package/dist/capabilities/agent-reach.js +1 -1
- package/dist/capabilities/anything.js +1 -1
- package/dist/capabilities/artifacts.js +1 -1
- package/dist/capabilities/effect.js +1 -1
- package/dist/capabilities/fact-log.js +1 -1
- package/dist/capabilities/flyai.js +20 -6
- package/dist/capabilities/hbcli.js +1 -1
- package/dist/capabilities/incident-log.js +1 -1
- package/dist/capabilities/model-override.js +18 -0
- package/dist/capabilities/opensky.js +1 -1
- package/dist/capabilities/resilience.js +1 -1
- package/dist/capabilities/session/action-cache.js +1 -1
- package/dist/capabilities/session/adapters/ctrip-flight.js +1 -1
- package/dist/capabilities/session/adapters/meituan-local.js +1 -1
- package/dist/capabilities/session/benchmark.js +1 -1
- package/dist/capabilities/session/extension-bridge.js +19 -6
- package/dist/capabilities/session/extension-channel.js +3 -3
- package/dist/capabilities/session/extension-distribution.js +234 -0
- package/dist/capabilities/session/extract.js +1 -1
- package/dist/capabilities/session/golden-score.js +92 -0
- package/dist/capabilities/session/health-watch.js +1 -1
- package/dist/capabilities/session/read-guard.js +1 -1
- package/dist/capabilities/session/static-flight-golden.js +137 -0
- package/dist/capabilities/session/transport.js +1 -1
- package/dist/capabilities/session/wizard.js +21 -251
- package/dist/capabilities/session-consent.js +1 -1
- package/dist/capabilities/session-login.js +11 -5
- package/dist/capabilities/session-search.js +46 -4
- package/dist/capabilities/weather.js +168 -46
- package/dist/data/session-golden-20.json +25 -0
- package/dist/data/sf-golden-manifest.json +102 -0
- package/dist/data/sf-static-routes.json +91 -0
- package/dist/scripts/action-cache-tests.js +1 -1
- package/dist/scripts/agent-planning-budget-e2e.js +227 -0
- package/dist/scripts/agent-planning-budget-tests.js +173 -0
- package/dist/scripts/agent-reach-deep-tests.js +1 -1
- package/dist/scripts/agent-reach-tests.js +1 -1
- package/dist/scripts/agent-reach-wrapper-tests.js +1 -1
- package/dist/scripts/anything-tests.js +1 -1
- package/dist/scripts/async-collect.js +1 -1
- package/dist/scripts/benchmark-environment-bridge-e2e.js +981 -0
- package/dist/scripts/benchmark-environment-bridge-tests.js +2544 -0
- package/dist/scripts/booking-copilot-availability-ledger-binding-tests.js +335 -0
- package/dist/scripts/booking-copilot-availability-policy-v2-tests.js +1355 -0
- package/dist/scripts/booking-copilot-bin-proof-tests.js +89 -0
- package/dist/scripts/booking-copilot-crossrepo-fixture-server.js +113 -0
- package/dist/scripts/booking-copilot-dsh-core-proof-tests.js +356 -0
- package/dist/scripts/booking-copilot-dsh-planner-proof-tests.js +557 -0
- package/dist/scripts/booking-copilot-dsh-plugin-proof-tests.js +140 -0
- package/dist/scripts/booking-copilot-event-sequence-concurrency-proof-tests.js +202 -0
- package/dist/scripts/booking-copilot-gap-code-contract-proof-tests.js +102 -0
- package/dist/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.js +374 -0
- package/dist/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.js +447 -0
- package/dist/scripts/booking-copilot-runtime-proof-tests.js +205 -0
- package/dist/scripts/booking-copilot-server-proof-tests.js +317 -0
- package/dist/scripts/booking-copilot-startup-proof-tests.js +283 -0
- package/dist/scripts/booking-copilot-v2-runtime-proof-tests.js +3935 -0
- package/dist/scripts/booking-saga-tests.js +1 -1
- package/dist/scripts/booking-surface-contract-proof-tests.js +393 -0
- package/dist/scripts/booking-surface-v2-contract-proof-tests.js +1174 -0
- package/dist/scripts/bootstrap-tests.js +30 -10
- package/dist/scripts/build-changelog.js +1 -1
- package/dist/scripts/changelog-tests.js +1 -1
- package/dist/scripts/companion-tests.js +1 -1
- package/dist/scripts/diff-test.js +1 -1
- package/dist/scripts/dsh-runtime-closure-tests.js +225 -0
- package/dist/scripts/dsh-runtime-closure.js +150 -0
- package/dist/scripts/effect-tests.js +1 -1
- package/dist/scripts/engine-run.js +1 -1
- package/dist/scripts/engine-tests.js +1 -1
- package/dist/scripts/evaluation-cadence-tests.js +347 -0
- package/dist/scripts/evaluation-contract-tests.js +574 -0
- package/dist/scripts/extension-distribution-cli.js +35 -0
- package/dist/scripts/extension-distribution-tests.js +341 -0
- package/dist/scripts/extension-tests.js +128 -23
- package/dist/scripts/fact-gate-tests.js +1 -1
- package/dist/scripts/flyai-tests.js +1 -1
- package/dist/scripts/hbcli-e2e-tests.js +1 -1
- package/dist/scripts/hbcli-tests.js +1 -1
- package/dist/scripts/health-watch-cli.js +1 -1
- package/dist/scripts/i18n-tests.js +1 -1
- package/dist/scripts/incident-tests.js +1 -1
- package/dist/scripts/journey-tests.js +1 -1
- package/dist/scripts/ledger-tests.js +1 -1
- package/dist/scripts/ledger-workflow-crash.js +1 -1
- package/dist/scripts/memory-capture-tests.js +1 -1
- package/dist/scripts/memory-decay-tests.js +1 -1
- package/dist/scripts/memory-metrics.js +1 -1
- package/dist/scripts/memory-value-report.js +1 -1
- package/dist/scripts/model-override-e2e.js +176 -0
- package/dist/scripts/nightly-evidence-tests.js +1 -1
- package/dist/scripts/nightly-evidence.js +1 -1
- package/dist/scripts/nudge-digest.js +1 -1
- package/dist/scripts/onboarding-tests.js +21 -53
- package/dist/scripts/opensky-check.js +1 -1
- package/dist/scripts/opensky-tests.js +1 -1
- package/dist/scripts/pnpm-dsh-closure-proof.js +20 -0
- package/dist/scripts/price-drift-tests.js +1 -1
- package/dist/scripts/price-drift-watch.js +1 -1
- package/dist/scripts/probe-poi-tests.js +1 -1
- package/dist/scripts/product-metrics.js +1 -1
- package/dist/scripts/publish-preverify.js +40 -4
- package/dist/scripts/realtime-pricing-tests.js +1 -1
- package/dist/scripts/replay-async.js +1 -1
- package/dist/scripts/replay-real.js +1 -1
- package/dist/scripts/replay.js +1 -1
- package/dist/scripts/session-attach-diagnose.js +1 -1
- package/dist/scripts/session-attach-poc.js +1 -1
- package/dist/scripts/session-benchmark.js +1 -1
- package/dist/scripts/session-extract-tests.js +1 -1
- package/dist/scripts/session-login.js +1 -1
- package/dist/scripts/session-tests.js +70 -20
- package/dist/scripts/sf-live-benchmark.js +338 -0
- package/dist/scripts/sf-live-cli-tests.js +21 -0
- package/dist/scripts/sf-soft-score-tests.js +108 -0
- package/dist/scripts/sf-summary.js +93 -0
- package/dist/scripts/skeleton-check.js +1 -1
- package/dist/scripts/skeleton-integration-test.js +1 -1
- package/dist/scripts/skills-contract-tests.js +1 -1
- package/dist/scripts/smoke-session-gate-tests.js +29 -0
- package/dist/scripts/smoke.js +79 -33
- package/dist/scripts/state-cli-tests.js +1 -1
- package/dist/scripts/state-cli.js +1 -1
- package/dist/scripts/static-golden-tests.js +299 -0
- package/dist/scripts/time-eval-tests.js +1 -1
- package/dist/scripts/travel-timeline-tests.js +1 -1
- package/dist/scripts/unified-tests.js +1 -1
- package/dist/scripts/weather-tests.js +694 -44
- package/dist/scripts/z3-race-tests.js +1 -1
- package/dist/src/artifact-gate.js +1 -1
- package/dist/src/benchmark-agent-conformance.js +370 -0
- package/dist/src/benchmark-environment-bridge.js +384 -0
- package/dist/src/benchmark-headless-child-diagnostics.js +173 -0
- package/dist/src/benchmark-tool-isolation.js +124 -0
- package/dist/src/bookable-facts.js +1 -1
- package/dist/src/booking-saga.js +1 -1
- package/dist/src/booking-surface/availability-policy-v2.js +830 -0
- package/dist/src/booking-surface/canonical-schema.js +113 -0
- package/dist/src/booking-surface/contracts-v2.js +89 -0
- package/dist/src/booking-surface/contracts.js +46 -0
- package/dist/src/booking-surface/dsh-planner.js +453 -0
- package/dist/src/booking-surface/dsh-plugin.js +93 -0
- package/dist/src/booking-surface/error-codes.js +94 -0
- package/dist/src/booking-surface/index.js +15 -0
- package/dist/src/booking-surface/profile.js +68 -0
- package/dist/src/booking-surface/runtime-v2.js +1771 -0
- package/dist/src/booking-surface/runtime.js +351 -0
- package/dist/src/booking-surface/server-v2.js +334 -0
- package/dist/src/booking-surface/server.js +302 -0
- package/dist/src/booking-surface/startup.js +159 -0
- package/dist/src/booking-surface/validation-v2.js +319 -0
- package/dist/src/booking-surface/validation.js +809 -0
- package/dist/src/bridge.js +1 -1
- package/dist/src/companions.js +1 -1
- package/dist/src/contracts.js +1 -1
- package/dist/src/dsh-llm.js +1 -1
- package/dist/src/engine.js +1 -1
- package/dist/src/evaluation-cadence.js +234 -0
- package/dist/src/evaluation-contracts.js +906 -0
- package/dist/src/i18n.js +1 -1
- package/dist/src/index.js +50 -19
- package/dist/src/journey.js +1 -1
- package/dist/src/loop.js +1 -1
- package/dist/src/memory-capture.js +1 -1
- package/dist/src/memory-decay.js +1 -1
- package/dist/src/memory-utility.js +1 -1
- package/dist/src/mock-llm.js +1 -1
- package/dist/src/model.js +1 -1
- package/dist/src/realtime-pricing.js +1 -1
- package/dist/src/slot-spec.js +1 -1
- package/dist/src/state-ledger.js +2 -1
- package/dist/src/time-anchor.js +1 -1
- package/dist/src/tool-budget.js +136 -0
- package/dist/src/tool-packet.js +1 -1
- package/dist/src/travel-slots.js +1 -1
- package/dist/src/travel-timeline.js +1 -1
- package/dist/src/unified.js +1 -1
- package/dist/src/wish-pool.js +1 -1
- package/dist/src/z3-shared.js +1 -1
- package/extension/README.md +31 -7
- package/package.json +286 -11
- package/schemas/booking.surface.v1.schema.json +927 -0
- package/schemas/booking.surface.v2.schema.json +61 -0
- package/ts/capabilities/flyai.ts +16 -3
- package/ts/capabilities/session/extension-bridge.ts +37 -14
- package/ts/capabilities/session/extension-channel.ts +6 -3
- package/ts/capabilities/session/extension-distribution.ts +264 -0
- package/ts/capabilities/session/golden-score.ts +139 -0
- package/ts/capabilities/session/health-watch.ts +1 -1
- package/ts/capabilities/session/static-flight-golden.ts +209 -0
- package/ts/capabilities/session/wizard.ts +34 -176
- package/ts/capabilities/session-login.ts +15 -5
- package/ts/capabilities/session-search.ts +40 -3
- package/ts/capabilities/weather.ts +141 -52
- package/ts/package.json +3 -3
- package/ts/src/benchmark-agent-conformance.ts +448 -0
- package/ts/src/benchmark-environment-bridge.ts +348 -0
- package/ts/src/benchmark-headless-child-diagnostics.ts +184 -0
- package/ts/src/benchmark-tool-isolation.ts +166 -0
- package/ts/src/booking-surface/availability-policy-v2.ts +523 -0
- package/ts/src/booking-surface/canonical-schema.js +113 -0
- package/ts/src/booking-surface/contracts-v2.ts +118 -0
- package/ts/src/booking-surface/contracts.ts +380 -0
- package/ts/src/booking-surface/dsh-planner.ts +452 -0
- package/ts/src/booking-surface/dsh-plugin.js +93 -0
- package/ts/src/booking-surface/error-codes.ts +101 -0
- package/ts/src/booking-surface/index.ts +12 -0
- package/ts/src/booking-surface/profile.ts +42 -0
- package/ts/src/booking-surface/runtime-v2.ts +1466 -0
- package/ts/src/booking-surface/runtime.ts +483 -0
- package/ts/src/booking-surface/server-v2.ts +247 -0
- package/ts/src/booking-surface/server.ts +324 -0
- package/ts/src/booking-surface/startup.ts +196 -0
- package/ts/src/booking-surface/validation-v2.ts +205 -0
- package/ts/src/booking-surface/validation.ts +453 -0
- package/ts/src/index.ts +64 -11
- package/ts/src/state-ledger.ts +1 -0
- package/ts/src/tool-budget.ts +165 -0
- package/dist/scripts/wizard-bootstrap.js +0 -32
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { ensureLedger } from '../src/state-ledger.js';
|
|
8
|
+
import { BookingCopilotTaskRuntime } from '../src/booking-surface/runtime.js';
|
|
9
|
+
import { validateActionReceiptV1 } from '../src/booking-surface/validation.js';
|
|
10
|
+
const childIndex = process.argv.indexOf('--child');
|
|
11
|
+
const child = childIndex >= 0;
|
|
12
|
+
const root = child ? process.argv[childIndex + 1] : undefined;
|
|
13
|
+
const actionId = child ? process.argv[childIndex + 2] : undefined;
|
|
14
|
+
const variant = child ? process.argv[childIndex + 3] : undefined;
|
|
15
|
+
const release = root ? join(root, 'release') : '';
|
|
16
|
+
const workspace = {
|
|
17
|
+
schemaVersion: 'booking.surface.v1',
|
|
18
|
+
contextRef: 'ctx-concurrency-opaque',
|
|
19
|
+
surface: 'tenant',
|
|
20
|
+
revision: 0,
|
|
21
|
+
locale: 'en-US',
|
|
22
|
+
currency: 'AED',
|
|
23
|
+
searchDraft: {},
|
|
24
|
+
results: {
|
|
25
|
+
status: 'idle'
|
|
26
|
+
},
|
|
27
|
+
visibleHotels: [],
|
|
28
|
+
loadedOffers: [],
|
|
29
|
+
shortlistedOfferRefs: [],
|
|
30
|
+
capabilities: {
|
|
31
|
+
surface: 'tenant',
|
|
32
|
+
allowedActions: [
|
|
33
|
+
'search.run'
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const action = {
|
|
38
|
+
schemaVersion: 'booking.surface.v1',
|
|
39
|
+
kind: 'search.run',
|
|
40
|
+
actionId: 'action-concurrency-opaque',
|
|
41
|
+
contextRef: workspace.contextRef,
|
|
42
|
+
expectedRevision: 0,
|
|
43
|
+
reason: 'opaque reason',
|
|
44
|
+
factRefs: [
|
|
45
|
+
'fact-opaque'
|
|
46
|
+
],
|
|
47
|
+
input: {}
|
|
48
|
+
};
|
|
49
|
+
function receipt(which) {
|
|
50
|
+
const value = {
|
|
51
|
+
schemaVersion: 'booking.surface.v1',
|
|
52
|
+
kind: 'action.receipt',
|
|
53
|
+
actionId: action.actionId,
|
|
54
|
+
contextRef: workspace.contextRef,
|
|
55
|
+
status: 'applied',
|
|
56
|
+
revision: 1,
|
|
57
|
+
observation: {
|
|
58
|
+
kind: 'search.state',
|
|
59
|
+
searchSessionRef: `session-${which === 'same-a' || which === 'same-b' ? 'same' : which}`,
|
|
60
|
+
resultCount: 1
|
|
61
|
+
},
|
|
62
|
+
resultContract: {
|
|
63
|
+
outcome: 'complete',
|
|
64
|
+
hardCriteriaMet: true,
|
|
65
|
+
factRefs: [
|
|
66
|
+
`fact-${which === 'same-a' || which === 'same-b' ? 'same' : which}`
|
|
67
|
+
],
|
|
68
|
+
gapCodes: []
|
|
69
|
+
},
|
|
70
|
+
undoToken: `undo-${which === 'same-a' || which === 'same-b' ? 'same' : which}`
|
|
71
|
+
};
|
|
72
|
+
if (which === 'same-b') return {
|
|
73
|
+
undoToken: value.undoToken,
|
|
74
|
+
resultContract: value.resultContract,
|
|
75
|
+
observation: value.observation,
|
|
76
|
+
revision: value.revision,
|
|
77
|
+
status: value.status,
|
|
78
|
+
contextRef: value.contextRef,
|
|
79
|
+
actionId: value.actionId,
|
|
80
|
+
kind: value.kind,
|
|
81
|
+
schemaVersion: value.schemaVersion
|
|
82
|
+
};
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
function continuation(r, taskId = 'task-concurrency-opaque') {
|
|
86
|
+
return {
|
|
87
|
+
schemaVersion: 'booking.surface.v1',
|
|
88
|
+
kind: 'action.receipt.continuation',
|
|
89
|
+
taskId,
|
|
90
|
+
workspace: {
|
|
91
|
+
...workspace,
|
|
92
|
+
revision: 1
|
|
93
|
+
},
|
|
94
|
+
receipt: r
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (process.argv.includes('--replay')) {
|
|
98
|
+
const replayRoot = process.argv[process.argv.indexOf('--replay') + 1];
|
|
99
|
+
const replayVariant = process.argv[process.argv.indexOf('--replay') + 2];
|
|
100
|
+
const replayLedger = ensureLedger(replayRoot);
|
|
101
|
+
try {
|
|
102
|
+
new BookingCopilotTaskRuntime(replayLedger).continueWithReceipt(continuation(receipt(replayVariant)));
|
|
103
|
+
console.log(JSON.stringify({
|
|
104
|
+
result: 'success'
|
|
105
|
+
}));
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.log(JSON.stringify({
|
|
108
|
+
result: 'error',
|
|
109
|
+
error: error instanceof Error ? error.message : String(error)
|
|
110
|
+
}));
|
|
111
|
+
}
|
|
112
|
+
replayLedger.close();
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
if (child) {
|
|
116
|
+
const ledger = ensureLedger(root);
|
|
117
|
+
const r = new BookingCopilotTaskRuntime(ledger);
|
|
118
|
+
writeFileSync(join(root, `started-${variant}`), 'started');
|
|
119
|
+
const startDeadline = Date.now() + 10_000;
|
|
120
|
+
while(!existsSync(join(root, 'start'))){
|
|
121
|
+
if (Date.now() > startDeadline) throw new Error('child_start_timeout');
|
|
122
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
123
|
+
}
|
|
124
|
+
const originalResume = r.resumeTask.bind(r);
|
|
125
|
+
let firstPendingRead = true;
|
|
126
|
+
r.resumeTask = (taskId)=>{
|
|
127
|
+
const state = originalResume(taskId);
|
|
128
|
+
if (firstPendingRead && state?.pendingAction) {
|
|
129
|
+
firstPendingRead = false;
|
|
130
|
+
writeFileSync(join(root, `ready-${variant}`), 'ready');
|
|
131
|
+
const deadline = Date.now() + 10_000;
|
|
132
|
+
while(!existsSync(release)){
|
|
133
|
+
if (Date.now() > deadline) throw new Error('child_timeout');
|
|
134
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return state;
|
|
138
|
+
};
|
|
139
|
+
try {
|
|
140
|
+
r.continueWithReceipt(continuation(receipt(variant)));
|
|
141
|
+
console.log(JSON.stringify({
|
|
142
|
+
result: 'success',
|
|
143
|
+
variant
|
|
144
|
+
}));
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.log(JSON.stringify({
|
|
147
|
+
result: 'error',
|
|
148
|
+
variant,
|
|
149
|
+
error: error instanceof Error ? error.message : String(error)
|
|
150
|
+
}));
|
|
151
|
+
}
|
|
152
|
+
ledger.close();
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
function waitFor(predicate, timeoutMs) {
|
|
156
|
+
return new Promise((resolve, reject)=>{
|
|
157
|
+
const deadline = Date.now() + timeoutMs;
|
|
158
|
+
const tick = ()=>predicate() ? resolve() : Date.now() > deadline ? reject(new Error('proof_timeout')) : setTimeout(tick, 10);
|
|
159
|
+
tick();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async function runPair(stateRoot, left, right) {
|
|
163
|
+
const script = fileURLToPath(import.meta.url);
|
|
164
|
+
const args = [
|
|
165
|
+
script,
|
|
166
|
+
'--child',
|
|
167
|
+
stateRoot,
|
|
168
|
+
action.actionId
|
|
169
|
+
];
|
|
170
|
+
const tsxBin = fileURLToPath(new URL('../node_modules/tsx/dist/cli.mjs', import.meta.url));
|
|
171
|
+
const children = [
|
|
172
|
+
left,
|
|
173
|
+
right
|
|
174
|
+
].map((which)=>spawn(tsxBin, [
|
|
175
|
+
...args,
|
|
176
|
+
which
|
|
177
|
+
], {
|
|
178
|
+
cwd: process.cwd(),
|
|
179
|
+
stdio: [
|
|
180
|
+
'ignore',
|
|
181
|
+
'pipe',
|
|
182
|
+
'pipe'
|
|
183
|
+
]
|
|
184
|
+
}));
|
|
185
|
+
const outputs = children.map(()=>'');
|
|
186
|
+
children.forEach((p, i)=>{
|
|
187
|
+
p.stdout.on('data', (b)=>{
|
|
188
|
+
outputs[i] += b.toString();
|
|
189
|
+
});
|
|
190
|
+
p.stderr.on('data', (b)=>{
|
|
191
|
+
outputs[i] += b.toString();
|
|
192
|
+
});
|
|
193
|
+
p.on('error', (e)=>{
|
|
194
|
+
outputs[i] += `spawn_error:${e.message}`;
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
await waitFor(()=>existsSync(join(stateRoot, `started-${left}`)) && existsSync(join(stateRoot, `started-${right}`)), 10_000);
|
|
199
|
+
writeFileSync(join(stateRoot, 'start'), 'go');
|
|
200
|
+
await waitFor(()=>existsSync(join(stateRoot, `ready-${left}`)) || existsSync(join(stateRoot, `ready-${right}`)), 10_000);
|
|
201
|
+
await new Promise((resolve)=>setTimeout(resolve, 250));
|
|
202
|
+
assert.equal(existsSync(join(stateRoot, `ready-${left}`)) && existsSync(join(stateRoot, `ready-${right}`)), false, 'only one serialized pending read may pass the barrier');
|
|
203
|
+
} catch (error) {
|
|
204
|
+
children.forEach((p)=>p.kill('SIGKILL'));
|
|
205
|
+
throw new Error(`${error instanceof Error ? error.message : error}:${outputs.join('|')}`);
|
|
206
|
+
}
|
|
207
|
+
writeFileSync(join(stateRoot, 'release'), 'go');
|
|
208
|
+
await Promise.all(children.map((p)=>new Promise((resolve, reject)=>{
|
|
209
|
+
const t = setTimeout(()=>{
|
|
210
|
+
p.kill('SIGKILL');
|
|
211
|
+
reject(new Error('child_process_timeout'));
|
|
212
|
+
}, 10_000);
|
|
213
|
+
p.on('exit', (code)=>{
|
|
214
|
+
clearTimeout(t);
|
|
215
|
+
code === 0 ? resolve() : reject(new Error(`child_exit_${code}:${outputs.join('|')}`));
|
|
216
|
+
});
|
|
217
|
+
})));
|
|
218
|
+
return outputs.map((out)=>JSON.parse(out.trim().split('\n').at(-1)));
|
|
219
|
+
}
|
|
220
|
+
const stateRoot = root ?? mkdtempSync(join(tmpdir(), 'gotry-receipt-concurrency-'));
|
|
221
|
+
if (!child) {
|
|
222
|
+
mkdirSync(stateRoot, {
|
|
223
|
+
recursive: true
|
|
224
|
+
});
|
|
225
|
+
const ledger = ensureLedger(stateRoot);
|
|
226
|
+
const r = new BookingCopilotTaskRuntime(ledger);
|
|
227
|
+
const turn = {
|
|
228
|
+
schemaVersion: 'booking.surface.v1',
|
|
229
|
+
kind: 'user.turn',
|
|
230
|
+
taskId: 'task-concurrency-opaque',
|
|
231
|
+
workspace,
|
|
232
|
+
request: {
|
|
233
|
+
text: 'unique-pii-marker@example.invalid Bearer TEST raw-prompt-marker'
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
const sensitiveAction = {
|
|
237
|
+
...action,
|
|
238
|
+
reason: 'unique-pii-marker@example.invalid Bearer TEST raw-prompt-marker',
|
|
239
|
+
input: {}
|
|
240
|
+
};
|
|
241
|
+
r.startTask(turn);
|
|
242
|
+
r.issueOperation(turn.taskId, sensitiveAction);
|
|
243
|
+
assert.throws(()=>r.continueWithReceipt(continuation({
|
|
244
|
+
...receipt('a'),
|
|
245
|
+
undoToken: 'malicious@example.invalid'
|
|
246
|
+
})), /invalid_receipt_continuation/);
|
|
247
|
+
assert.throws(()=>r.continueWithReceipt(continuation({
|
|
248
|
+
...receipt('a'),
|
|
249
|
+
observation: {
|
|
250
|
+
kind: 'gap',
|
|
251
|
+
code: 'raw-prompt-marker user prose',
|
|
252
|
+
factRefs: [
|
|
253
|
+
'numeric-123'
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
})), /invalid_receipt_continuation/);
|
|
257
|
+
assert.equal(validateActionReceiptV1({
|
|
258
|
+
...receipt('a'),
|
|
259
|
+
status: 'no_match',
|
|
260
|
+
observation: {
|
|
261
|
+
kind: 'gap',
|
|
262
|
+
code: 'no_match',
|
|
263
|
+
factRefs: [
|
|
264
|
+
'123'
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
}).ok, true);
|
|
268
|
+
const differing = await runPair(stateRoot, 'a', 'b');
|
|
269
|
+
assert.equal(differing.filter((x)=>x.result === 'success').length, 1);
|
|
270
|
+
assert.equal(differing.filter((x)=>x.error === 'receipt_conflict').length, 1);
|
|
271
|
+
const after = ledger;
|
|
272
|
+
assert.equal(after.db.prepare("SELECT COUNT(*) AS n FROM events WHERE kind = 'booking.copilot.receipt.observed'").get().n, 1);
|
|
273
|
+
const persisted = after.db.prepare("SELECT payload FROM events WHERE kind LIKE 'booking.copilot.%'").all();
|
|
274
|
+
assert.ok(!persisted.some((row)=>/unique-pii-marker|Bearer TEST|raw-prompt-marker/i.test(row.payload)));
|
|
275
|
+
const winner = differing.find((x)=>x.result === 'success');
|
|
276
|
+
assert.ok(winner?.variant);
|
|
277
|
+
assert.deepEqual(new BookingCopilotTaskRuntime(after).resumeTask(turn.taskId)?.lastReceipt, receipt(winner.variant));
|
|
278
|
+
after.close();
|
|
279
|
+
rmSync(stateRoot, {
|
|
280
|
+
recursive: true,
|
|
281
|
+
force: true
|
|
282
|
+
});
|
|
283
|
+
const equalRoot = mkdtempSync(join(tmpdir(), 'gotry-receipt-equal-'));
|
|
284
|
+
const equalLedger = ensureLedger(equalRoot);
|
|
285
|
+
const equalRuntime = new BookingCopilotTaskRuntime(equalLedger);
|
|
286
|
+
equalRuntime.startTask(turn);
|
|
287
|
+
equalRuntime.issueOperation('task-concurrency-opaque', action);
|
|
288
|
+
const equal = await runPair(equalRoot, 'same-a', 'same-b');
|
|
289
|
+
assert.equal(equal.filter((x)=>x.result === 'success').length, 2);
|
|
290
|
+
const equalCheck = equalLedger;
|
|
291
|
+
assert.equal(equalCheck.db.prepare("SELECT COUNT(*) AS n FROM events WHERE kind = 'booking.copilot.receipt.observed'").get().n, 1);
|
|
292
|
+
const equalPayloads = equalCheck.db.prepare("SELECT payload FROM events WHERE kind LIKE 'booking.copilot.%'").all();
|
|
293
|
+
assert.ok(!equalPayloads.some((row)=>/sensitive|Authorization|Bearer|raw prompt|secret/i.test(row.payload)));
|
|
294
|
+
const tampered = equalCheck.db.prepare("SELECT seq, payload FROM events WHERE kind = 'booking.copilot.receipt.observed' LIMIT 1").get();
|
|
295
|
+
equalCheck.db.prepare('UPDATE events SET payload = ? WHERE seq = ?').run(tampered.payload.replace(/"receiptDigest":"[a-f0-9]+"/, '"receiptDigest":"0000000000000000000000000000000000000000000000000000000000000000"'), tampered.seq);
|
|
296
|
+
assert.throws(()=>new BookingCopilotTaskRuntime(equalCheck).resumeTask('task-concurrency-opaque'), /ledger_corrupt/);
|
|
297
|
+
equalCheck.close();
|
|
298
|
+
rmSync(equalRoot, {
|
|
299
|
+
recursive: true,
|
|
300
|
+
force: true
|
|
301
|
+
});
|
|
302
|
+
for(let round = 0; round < 20; round++){
|
|
303
|
+
const loopRoot = mkdtempSync(join(tmpdir(), 'gotry-receipt-round-'));
|
|
304
|
+
const loopLedger = ensureLedger(loopRoot);
|
|
305
|
+
const loopRuntime = new BookingCopilotTaskRuntime(loopLedger);
|
|
306
|
+
loopRuntime.startTask(turn);
|
|
307
|
+
loopRuntime.issueOperation('task-concurrency-opaque', action);
|
|
308
|
+
const result = await runPair(loopRoot, 'a', 'b');
|
|
309
|
+
assert.equal(result.filter((x)=>x.result === 'success').length, 1);
|
|
310
|
+
assert.equal(result.filter((x)=>x.error === 'receipt_conflict').length, 1);
|
|
311
|
+
loopLedger.close();
|
|
312
|
+
rmSync(loopRoot, {
|
|
313
|
+
recursive: true,
|
|
314
|
+
force: true
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const replayRoot = mkdtempSync(join(tmpdir(), 'gotry-receipt-replay-'));
|
|
318
|
+
const replayLedger = ensureLedger(replayRoot);
|
|
319
|
+
const replayRuntime = new BookingCopilotTaskRuntime(replayLedger);
|
|
320
|
+
replayRuntime.startTask(turn);
|
|
321
|
+
replayRuntime.issueOperation('task-concurrency-opaque', action);
|
|
322
|
+
replayRuntime.continueWithReceipt(continuation(receipt('same')));
|
|
323
|
+
const nextAction = {
|
|
324
|
+
...action,
|
|
325
|
+
actionId: 'action-next-opaque',
|
|
326
|
+
expectedRevision: 1
|
|
327
|
+
};
|
|
328
|
+
replayRuntime.issueOperation('task-concurrency-opaque', nextAction);
|
|
329
|
+
const beforeReplay = replayLedger.countEvents();
|
|
330
|
+
const tsxBin = process.env.PATH?.split(':').map((dir)=>join(dir, 'tsx')).find(existsSync);
|
|
331
|
+
assert.ok(tsxBin);
|
|
332
|
+
for (const replayVariant of [
|
|
333
|
+
'same',
|
|
334
|
+
'conflict'
|
|
335
|
+
]){
|
|
336
|
+
const replayChild = spawn(tsxBin, [
|
|
337
|
+
fileURLToPath(import.meta.url),
|
|
338
|
+
'--replay',
|
|
339
|
+
replayRoot,
|
|
340
|
+
replayVariant
|
|
341
|
+
], {
|
|
342
|
+
cwd: process.cwd(),
|
|
343
|
+
stdio: [
|
|
344
|
+
'ignore',
|
|
345
|
+
'pipe',
|
|
346
|
+
'pipe'
|
|
347
|
+
]
|
|
348
|
+
});
|
|
349
|
+
let replayOutput = '';
|
|
350
|
+
replayChild.stdout.on('data', (b)=>{
|
|
351
|
+
replayOutput += b.toString();
|
|
352
|
+
});
|
|
353
|
+
replayChild.stderr.on('data', (b)=>{
|
|
354
|
+
replayOutput += b.toString();
|
|
355
|
+
});
|
|
356
|
+
await new Promise((resolve, reject)=>{
|
|
357
|
+
const timer = setTimeout(()=>{
|
|
358
|
+
replayChild.kill('SIGKILL');
|
|
359
|
+
reject(new Error('replay_timeout'));
|
|
360
|
+
}, 10_000);
|
|
361
|
+
replayChild.on('exit', (code)=>{
|
|
362
|
+
clearTimeout(timer);
|
|
363
|
+
code === 0 ? resolve() : reject(new Error(`replay_exit_${code}`));
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
const replayResult = JSON.parse(replayOutput.trim().split('\n').at(-1));
|
|
367
|
+
assert.equal(replayResult.result, replayVariant === 'same' ? 'success' : 'error');
|
|
368
|
+
if (replayVariant === 'conflict') assert.equal(replayResult.error, 'receipt_conflict');
|
|
369
|
+
}
|
|
370
|
+
const replayCheck = replayLedger;
|
|
371
|
+
assert.equal(replayCheck.countEvents(), beforeReplay);
|
|
372
|
+
assert.equal(new BookingCopilotTaskRuntime(replayCheck).resumeTask('task-concurrency-opaque')?.pendingAction?.actionId, nextAction.actionId);
|
|
373
|
+
replayCheck.close();
|
|
374
|
+
rmSync(replayRoot, {
|
|
375
|
+
recursive: true,
|
|
376
|
+
force: true
|
|
377
|
+
});
|
|
378
|
+
const reuseRoot = mkdtempSync(join(tmpdir(), 'gotry-receipt-reuse-'));
|
|
379
|
+
const reuseLedger = ensureLedger(reuseRoot);
|
|
380
|
+
const reuseRuntime = new BookingCopilotTaskRuntime(reuseLedger);
|
|
381
|
+
reuseRuntime.startTask(turn);
|
|
382
|
+
reuseRuntime.issueOperation('task-concurrency-opaque', action);
|
|
383
|
+
reuseRuntime.continueWithReceipt(continuation(receipt('same')));
|
|
384
|
+
const actionB = {
|
|
385
|
+
...action,
|
|
386
|
+
actionId: 'action-b-opaque',
|
|
387
|
+
expectedRevision: 1
|
|
388
|
+
};
|
|
389
|
+
reuseRuntime.issueOperation('task-concurrency-opaque', actionB);
|
|
390
|
+
reuseRuntime.continueWithReceipt(continuation({
|
|
391
|
+
...receipt('same'),
|
|
392
|
+
actionId: actionB.actionId
|
|
393
|
+
}));
|
|
394
|
+
const reuseEvents = reuseLedger.countEvents();
|
|
395
|
+
const reuseState = reuseRuntime.resumeTask('task-concurrency-opaque');
|
|
396
|
+
assert.throws(()=>reuseRuntime.issueOperation('task-concurrency-opaque', action), /action_already_receipted/);
|
|
397
|
+
assert.equal(reuseLedger.countEvents(), reuseEvents);
|
|
398
|
+
assert.deepEqual(reuseRuntime.resumeTask('task-concurrency-opaque'), reuseState);
|
|
399
|
+
reuseLedger.close();
|
|
400
|
+
rmSync(reuseRoot, {
|
|
401
|
+
recursive: true,
|
|
402
|
+
force: true
|
|
403
|
+
});
|
|
404
|
+
const tupleRoot = mkdtempSync(join(tmpdir(), 'gotry-receipt-tuple-'));
|
|
405
|
+
const tupleLedger = ensureLedger(tupleRoot);
|
|
406
|
+
const tupleRuntime = new BookingCopilotTaskRuntime(tupleLedger);
|
|
407
|
+
for (const [taskId, actionId] of [
|
|
408
|
+
[
|
|
409
|
+
'task:a',
|
|
410
|
+
'b'
|
|
411
|
+
],
|
|
412
|
+
[
|
|
413
|
+
'task',
|
|
414
|
+
'a:b'
|
|
415
|
+
]
|
|
416
|
+
]){
|
|
417
|
+
tupleRuntime.startTask({
|
|
418
|
+
...turn,
|
|
419
|
+
taskId
|
|
420
|
+
});
|
|
421
|
+
tupleRuntime.issueOperation(taskId, {
|
|
422
|
+
...action,
|
|
423
|
+
actionId
|
|
424
|
+
});
|
|
425
|
+
tupleRuntime.continueWithReceipt(continuation({
|
|
426
|
+
...receipt('same'),
|
|
427
|
+
actionId
|
|
428
|
+
}, taskId));
|
|
429
|
+
}
|
|
430
|
+
assert.equal(tupleLedger.db.prepare("SELECT COUNT(*) AS n FROM events WHERE kind = 'booking.copilot.action.issued'").get().n, 2);
|
|
431
|
+
assert.equal(tupleLedger.db.prepare("SELECT COUNT(DISTINCT idem_key) AS n FROM events WHERE kind = 'booking.copilot.action.issued'").get().n, 2);
|
|
432
|
+
assert.equal(tupleLedger.db.prepare("SELECT COUNT(*) AS n FROM events WHERE kind = 'booking.copilot.receipt.observed'").get().n, 2);
|
|
433
|
+
assert.equal(tupleLedger.db.prepare("SELECT COUNT(DISTINCT idem_key) AS n FROM events WHERE kind = 'booking.copilot.receipt.observed'").get().n, 2);
|
|
434
|
+
for (const taskId of [
|
|
435
|
+
'task:a',
|
|
436
|
+
'task'
|
|
437
|
+
])assert.equal(tupleRuntime.resumeTask(taskId)?.phase, 'planning');
|
|
438
|
+
tupleLedger.close();
|
|
439
|
+
rmSync(tupleRoot, {
|
|
440
|
+
recursive: true,
|
|
441
|
+
force: true
|
|
442
|
+
});
|
|
443
|
+
console.log('BOOKING COPILOT RECEIPT LEDGER CONCURRENCY PROOF: winner/conflict/atomic persistence OK');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
//# sourceURL=ts/scripts/booking-copilot-receipt-ledger-concurrency-proof-tests.ts
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { ensureLedger } from '../src/state-ledger.js';
|
|
6
|
+
import { BookingCopilotTaskRuntime } from '../src/booking-surface/runtime.js';
|
|
7
|
+
const stateRoot = mkdtempSync(join(tmpdir(), 'gotry-booking-copilot-'));
|
|
8
|
+
const ledger = ensureLedger(stateRoot);
|
|
9
|
+
let id = 0;
|
|
10
|
+
const runtimeOptions = {
|
|
11
|
+
idFactory: (prefix)=>`${prefix}-${++id}`,
|
|
12
|
+
now: ()=>'2026-08-30T12:00:00.000Z'
|
|
13
|
+
};
|
|
14
|
+
const strictWorkspace = {
|
|
15
|
+
schemaVersion: 'booking.surface.v1',
|
|
16
|
+
contextRef: 'ctx-server-opaque-1',
|
|
17
|
+
surface: 'tenant',
|
|
18
|
+
revision: 0,
|
|
19
|
+
locale: 'zh-CN',
|
|
20
|
+
currency: 'AED',
|
|
21
|
+
searchDraft: {},
|
|
22
|
+
results: {
|
|
23
|
+
status: 'idle'
|
|
24
|
+
},
|
|
25
|
+
visibleHotels: [],
|
|
26
|
+
loadedOffers: [],
|
|
27
|
+
shortlistedOfferRefs: [],
|
|
28
|
+
capabilities: {
|
|
29
|
+
surface: 'tenant',
|
|
30
|
+
allowedActions: [
|
|
31
|
+
'search.patch',
|
|
32
|
+
'search.run',
|
|
33
|
+
'results.view.patch',
|
|
34
|
+
'hotel.focus'
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const startTurn = {
|
|
39
|
+
schemaVersion: 'booking.surface.v1',
|
|
40
|
+
kind: 'user.turn',
|
|
41
|
+
taskId: 'task-opaque-1',
|
|
42
|
+
workspace: {
|
|
43
|
+
...strictWorkspace,
|
|
44
|
+
capabilities: {
|
|
45
|
+
...strictWorkspace.capabilities,
|
|
46
|
+
allowedActions: [
|
|
47
|
+
...strictWorkspace.capabilities.allowedActions
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
request: {
|
|
52
|
+
text: '联系 sensitive@example.com,找 13800001111 附近的酒店'
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
const runtime = new BookingCopilotTaskRuntime(ledger, runtimeOptions);
|
|
56
|
+
const started = runtime.startTask(startTurn);
|
|
57
|
+
assert.equal(started.phase, 'planning');
|
|
58
|
+
assert.equal(started.taskId, 'task-opaque-1');
|
|
59
|
+
assert.equal(started.contextRef, strictWorkspace.contextRef);
|
|
60
|
+
assert.equal(started.userTurnCount, 1, 'the initial user turn is a durable task-scoped turn observation');
|
|
61
|
+
const afterRestart = new BookingCopilotTaskRuntime(ledger, runtimeOptions);
|
|
62
|
+
assert.deepEqual(afterRestart.resumeTask('task-opaque-1'), started, 'task resumes from durable ledger state');
|
|
63
|
+
const action = {
|
|
64
|
+
schemaVersion: 'booking.surface.v1',
|
|
65
|
+
kind: 'search.patch',
|
|
66
|
+
actionId: 'action-opaque-1',
|
|
67
|
+
contextRef: strictWorkspace.contextRef,
|
|
68
|
+
expectedRevision: 0,
|
|
69
|
+
reason: 'private villa for sensitive@example.com',
|
|
70
|
+
factRefs: [
|
|
71
|
+
'fact-opaque-1'
|
|
72
|
+
],
|
|
73
|
+
input: {
|
|
74
|
+
patch: {
|
|
75
|
+
destination: {
|
|
76
|
+
query: '13800001111 private address'
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const operation = afterRestart.issueOperation('task-opaque-1', action);
|
|
82
|
+
assert.equal(operation.kind, 'operation');
|
|
83
|
+
assert.equal(operation.contextRef, strictWorkspace.contextRef);
|
|
84
|
+
assert.deepEqual(operation.action, action);
|
|
85
|
+
assert.equal(afterRestart.resumeTask('task-opaque-1')?.phase, 'waiting_receipt');
|
|
86
|
+
assert.throws(()=>afterRestart.issueOperation('task-opaque-1', {
|
|
87
|
+
...action,
|
|
88
|
+
actionId: 'action-opaque-2'
|
|
89
|
+
}), /receipt_required/, 'one task cannot issue another operation before the current receipt');
|
|
90
|
+
assert.throws(()=>afterRestart.startTask({
|
|
91
|
+
...startTurn,
|
|
92
|
+
request: {
|
|
93
|
+
text: '先继续筛选'
|
|
94
|
+
}
|
|
95
|
+
}), /receipt_required/, 'a fresh user turn cannot bypass the pending operation receipt');
|
|
96
|
+
const receipt = {
|
|
97
|
+
schemaVersion: 'booking.surface.v1',
|
|
98
|
+
kind: 'action.receipt',
|
|
99
|
+
actionId: action.actionId,
|
|
100
|
+
contextRef: strictWorkspace.contextRef,
|
|
101
|
+
status: 'applied',
|
|
102
|
+
revision: 1,
|
|
103
|
+
observation: {
|
|
104
|
+
kind: 'search.state',
|
|
105
|
+
searchSessionRef: 'search-opaque-1',
|
|
106
|
+
resultCount: 12
|
|
107
|
+
},
|
|
108
|
+
resultContract: {
|
|
109
|
+
outcome: 'complete',
|
|
110
|
+
hardCriteriaMet: true,
|
|
111
|
+
factRefs: [
|
|
112
|
+
'fact-opaque-2'
|
|
113
|
+
],
|
|
114
|
+
gapCodes: []
|
|
115
|
+
},
|
|
116
|
+
undoToken: 'undo-opaque-1'
|
|
117
|
+
};
|
|
118
|
+
const continuation = {
|
|
119
|
+
schemaVersion: 'booking.surface.v1',
|
|
120
|
+
kind: 'action.receipt.continuation',
|
|
121
|
+
taskId: 'task-opaque-1',
|
|
122
|
+
workspace: {
|
|
123
|
+
...startTurn.workspace,
|
|
124
|
+
revision: 1,
|
|
125
|
+
results: {
|
|
126
|
+
status: 'ready',
|
|
127
|
+
resultCount: 12,
|
|
128
|
+
searchSessionRef: 'search-opaque-1'
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
receipt
|
|
132
|
+
};
|
|
133
|
+
assert.throws(()=>afterRestart.continueWithReceipt({
|
|
134
|
+
...continuation,
|
|
135
|
+
workspace: {
|
|
136
|
+
...continuation.workspace,
|
|
137
|
+
contextRef: 'ctx-other'
|
|
138
|
+
},
|
|
139
|
+
receipt: {
|
|
140
|
+
...receipt,
|
|
141
|
+
contextRef: 'ctx-other'
|
|
142
|
+
}
|
|
143
|
+
}), /context_mismatch/, 'a receipt cannot cross task context');
|
|
144
|
+
const continued = afterRestart.continueWithReceipt(continuation);
|
|
145
|
+
assert.equal(continued.phase, 'planning');
|
|
146
|
+
assert.equal(continued.revision, 1);
|
|
147
|
+
assert.deepEqual(continued.lastReceipt, receipt);
|
|
148
|
+
const eventCountAfterReceipt = ledger.countEvents();
|
|
149
|
+
assert.deepEqual(afterRestart.continueWithReceipt(continuation), continued, 'identical receipt continuation is idempotent');
|
|
150
|
+
assert.equal(ledger.countEvents(), eventCountAfterReceipt, 'duplicate receipt writes no second ledger event');
|
|
151
|
+
const nextUserTurn = {
|
|
152
|
+
...startTurn,
|
|
153
|
+
workspace: continuation.workspace,
|
|
154
|
+
request: {
|
|
155
|
+
text: '继续筛选,只保留含早和免费取消的报价'
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const continuedTask = afterRestart.startTask(nextUserTurn);
|
|
159
|
+
assert.equal(continuedTask.userTurnCount, 2, 'one task accepts a new user intent after its receipt');
|
|
160
|
+
assert.notEqual(continuedTask.lastUserTurnDigest, started.lastUserTurnDigest, 'each user turn stores its own digest');
|
|
161
|
+
assert.throws(()=>afterRestart.startTask({
|
|
162
|
+
...nextUserTurn,
|
|
163
|
+
workspace: {
|
|
164
|
+
...nextUserTurn.workspace,
|
|
165
|
+
revision: 0
|
|
166
|
+
}
|
|
167
|
+
}), /task_conflict:revision_mismatch/, 'a user turn must continue from the current semantic revision');
|
|
168
|
+
assert.throws(()=>afterRestart.startTask({
|
|
169
|
+
...nextUserTurn,
|
|
170
|
+
workspace: {
|
|
171
|
+
...nextUserTurn.workspace,
|
|
172
|
+
capabilities: {
|
|
173
|
+
...nextUserTurn.workspace.capabilities,
|
|
174
|
+
allowedActions: [
|
|
175
|
+
'search.run'
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}), /task_conflict:capability_mismatch/, 'a task cannot silently broaden or shrink its surface allowlist');
|
|
180
|
+
const resumedAgain = new BookingCopilotTaskRuntime(ledger, runtimeOptions).resumeTask('task-opaque-1');
|
|
181
|
+
assert.deepEqual(resumedAgain?.lastReceipt, receipt, 'process-style restart recovers the last receipt checkpoint');
|
|
182
|
+
assert.equal(resumedAgain?.userTurnCount, 2, 'process-style restart recovers every user-turn checkpoint');
|
|
183
|
+
const status = new BookingCopilotTaskRuntime(ledger, runtimeOptions).emitEvent('task-opaque-1', {
|
|
184
|
+
kind: 'status',
|
|
185
|
+
status: 'working'
|
|
186
|
+
});
|
|
187
|
+
assert.ok(status.sequence > operation.sequence, 'event sequence remains monotonic after runtime restart');
|
|
188
|
+
const persisted = ledger.db.prepare(`SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind LIKE 'booking.copilot.%' ORDER BY seq`).all(ledger.tenant, 'task-opaque-1');
|
|
189
|
+
const persistedText = persisted.map((row)=>row.payload).join('\n');
|
|
190
|
+
assert.ok(!persistedText.includes('sensitive@example.com'), 'raw user/action prose is not persisted');
|
|
191
|
+
assert.ok(!persistedText.includes('13800001111'), 'raw criteria payload is not persisted');
|
|
192
|
+
assert.ok(!persistedText.includes('private villa'), 'action reason is persisted only as a digest');
|
|
193
|
+
assert.match(persistedText, /requestDigest/, 'redacted request digest is durable');
|
|
194
|
+
assert.match(persistedText, /inputDigest/, 'redacted action input digest is durable');
|
|
195
|
+
const userTurnCountRow = ledger.db.prepare(`SELECT COUNT(*) AS count FROM events WHERE tenant_id = ? AND run_id = ? AND kind = 'booking.copilot.user.turn.observed'`).get(ledger.tenant, 'task-opaque-1');
|
|
196
|
+
assert.equal(Number(userTurnCountRow?.count ?? 0), 2, 'task.started is separate from one digest-only row per accepted user turn');
|
|
197
|
+
ledger.close();
|
|
198
|
+
rmSync(stateRoot, {
|
|
199
|
+
recursive: true,
|
|
200
|
+
force: true
|
|
201
|
+
});
|
|
202
|
+
console.log('BOOKING COPILOT RUNTIME PROOF: task/operation/receipt/restart/idempotency/redaction OK');
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
//# sourceURL=ts/scripts/booking-copilot-runtime-proof-tests.ts
|