@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,202 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, 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
|
+
const taskId = 'task-event-sequence-atomicity';
|
|
10
|
+
const workspace = {
|
|
11
|
+
schemaVersion: 'booking.surface.v1',
|
|
12
|
+
contextRef: 'ctx-event-sequence-atomicity',
|
|
13
|
+
surface: 'tenant',
|
|
14
|
+
revision: 0,
|
|
15
|
+
locale: 'en-US',
|
|
16
|
+
currency: 'AED',
|
|
17
|
+
searchDraft: {},
|
|
18
|
+
results: {
|
|
19
|
+
status: 'idle'
|
|
20
|
+
},
|
|
21
|
+
visibleHotels: [],
|
|
22
|
+
loadedOffers: [],
|
|
23
|
+
shortlistedOfferRefs: [],
|
|
24
|
+
capabilities: {
|
|
25
|
+
surface: 'tenant',
|
|
26
|
+
allowedActions: [
|
|
27
|
+
'search.run'
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const turn = {
|
|
32
|
+
schemaVersion: 'booking.surface.v1',
|
|
33
|
+
kind: 'user.turn',
|
|
34
|
+
taskId,
|
|
35
|
+
workspace,
|
|
36
|
+
request: {
|
|
37
|
+
text: 'opaque user turn'
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const childIndex = process.argv.indexOf('--child');
|
|
41
|
+
const child = childIndex >= 0;
|
|
42
|
+
const childRoot = child ? process.argv[childIndex + 1] : undefined;
|
|
43
|
+
const childMode = child ? process.argv[childIndex + 2] : undefined;
|
|
44
|
+
const childVariant = child ? process.argv[childIndex + 3] : undefined;
|
|
45
|
+
function sleepWhile(predicate, timeoutMs = 10_000) {
|
|
46
|
+
const deadline = Date.now() + timeoutMs;
|
|
47
|
+
while(!predicate()){
|
|
48
|
+
if (Date.now() > deadline) throw new Error('proof_barrier_timeout');
|
|
49
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function waitForFile(path, timeoutMs = 10_000) {
|
|
53
|
+
return new Promise((resolve, reject)=>{
|
|
54
|
+
const deadline = Date.now() + timeoutMs;
|
|
55
|
+
const poll = ()=>{
|
|
56
|
+
if (existsSync(path)) return resolve();
|
|
57
|
+
if (Date.now() > deadline) return reject(new Error(`proof_file_timeout:${path}`));
|
|
58
|
+
setTimeout(poll, 5);
|
|
59
|
+
};
|
|
60
|
+
poll();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function runChild(root, mode, variant) {
|
|
64
|
+
const script = fileURLToPath(import.meta.url);
|
|
65
|
+
const tsx = fileURLToPath(new URL('../node_modules/tsx/dist/cli.mjs', import.meta.url));
|
|
66
|
+
const processChild = spawn(process.execPath, [
|
|
67
|
+
tsx,
|
|
68
|
+
script,
|
|
69
|
+
'--child',
|
|
70
|
+
root,
|
|
71
|
+
mode,
|
|
72
|
+
variant
|
|
73
|
+
], {
|
|
74
|
+
cwd: process.cwd(),
|
|
75
|
+
stdio: [
|
|
76
|
+
'ignore',
|
|
77
|
+
'pipe',
|
|
78
|
+
'pipe'
|
|
79
|
+
]
|
|
80
|
+
});
|
|
81
|
+
let output = '';
|
|
82
|
+
processChild.stdout.on('data', (chunk)=>{
|
|
83
|
+
output += chunk.toString();
|
|
84
|
+
});
|
|
85
|
+
processChild.stderr.on('data', (chunk)=>{
|
|
86
|
+
output += chunk.toString();
|
|
87
|
+
});
|
|
88
|
+
return new Promise((resolve, reject)=>{
|
|
89
|
+
const timer = setTimeout(()=>{
|
|
90
|
+
processChild.kill('SIGKILL');
|
|
91
|
+
reject(new Error(`child_timeout:${mode}:${variant}:${output}`));
|
|
92
|
+
}, 15_000);
|
|
93
|
+
processChild.on('exit', (code)=>{
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
if (code !== 0) return reject(new Error(`child_exit_${code}:${mode}:${variant}:${output}`));
|
|
96
|
+
try {
|
|
97
|
+
resolve(JSON.parse(output.trim().split('\n').at(-1)));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
reject(new Error(`child_output_invalid:${mode}:${variant}:${output}:${error}`));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (child) {
|
|
105
|
+
const root = childRoot;
|
|
106
|
+
const mode = childMode;
|
|
107
|
+
const variant = childVariant;
|
|
108
|
+
const ledger = ensureLedger(root);
|
|
109
|
+
const runtime = new BookingCopilotTaskRuntime(ledger);
|
|
110
|
+
writeFileSync(join(root, `started-${mode}-${variant}`), 'started');
|
|
111
|
+
sleepWhile(()=>existsSync(join(root, 'start')));
|
|
112
|
+
const originalResume = runtime.resumeTask.bind(runtime);
|
|
113
|
+
let firstRead = true;
|
|
114
|
+
runtime.resumeTask = (id)=>{
|
|
115
|
+
const state = originalResume(id);
|
|
116
|
+
if (firstRead && state) {
|
|
117
|
+
firstRead = false;
|
|
118
|
+
writeFileSync(join(root, `ready-${mode}-${variant}`), 'ready');
|
|
119
|
+
sleepWhile(()=>existsSync(join(root, 'release')));
|
|
120
|
+
}
|
|
121
|
+
return state;
|
|
122
|
+
};
|
|
123
|
+
try {
|
|
124
|
+
if (mode === 'user-turn') runtime.startTask(turn);
|
|
125
|
+
const event = runtime.emitEvent(taskId, {
|
|
126
|
+
kind: 'status',
|
|
127
|
+
status: 'working'
|
|
128
|
+
});
|
|
129
|
+
console.log(JSON.stringify({
|
|
130
|
+
event: {
|
|
131
|
+
sequence: event.sequence
|
|
132
|
+
}
|
|
133
|
+
}));
|
|
134
|
+
} catch (error) {
|
|
135
|
+
console.log(JSON.stringify({
|
|
136
|
+
error: error instanceof Error ? error.message : String(error)
|
|
137
|
+
}));
|
|
138
|
+
process.exitCode = 1;
|
|
139
|
+
} finally{
|
|
140
|
+
ledger.close();
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
async function run(mode) {
|
|
144
|
+
const root = mkdtempSync(join(tmpdir(), `gotry-event-sequence-${mode}-`));
|
|
145
|
+
mkdirSync(root, {
|
|
146
|
+
recursive: true
|
|
147
|
+
});
|
|
148
|
+
const ledger = ensureLedger(root);
|
|
149
|
+
new BookingCopilotTaskRuntime(ledger).startTask(turn);
|
|
150
|
+
ledger.close();
|
|
151
|
+
const children = [
|
|
152
|
+
runChild(root, mode, 'left'),
|
|
153
|
+
runChild(root, mode, 'right')
|
|
154
|
+
];
|
|
155
|
+
try {
|
|
156
|
+
await Promise.all([
|
|
157
|
+
waitForFile(join(root, `started-${mode}-left`)),
|
|
158
|
+
waitForFile(join(root, `started-${mode}-right`))
|
|
159
|
+
]);
|
|
160
|
+
writeFileSync(join(root, 'start'), 'start');
|
|
161
|
+
await Promise.race([
|
|
162
|
+
waitForFile(join(root, `ready-${mode}-left`)),
|
|
163
|
+
waitForFile(join(root, `ready-${mode}-right`))
|
|
164
|
+
]);
|
|
165
|
+
await new Promise((resolve)=>setTimeout(resolve, 250));
|
|
166
|
+
assert.equal(existsSync(join(root, `ready-${mode}-left`)) && existsSync(join(root, `ready-${mode}-right`)), false, `${mode} workers both derived state before the first transaction committed`);
|
|
167
|
+
writeFileSync(join(root, 'release'), 'release');
|
|
168
|
+
const results = await Promise.all(children);
|
|
169
|
+
assert.deepEqual(results.map((result)=>result.error), [
|
|
170
|
+
undefined,
|
|
171
|
+
undefined
|
|
172
|
+
]);
|
|
173
|
+
const check = ensureLedger(root);
|
|
174
|
+
const rows = check.db.prepare("SELECT payload FROM events WHERE tenant_id = ? AND run_id = ? AND kind = 'booking.copilot.event.emitted' ORDER BY seq").all(check.tenant, taskId);
|
|
175
|
+
const sequences = rows.map((row)=>JSON.parse(row.payload).sequence);
|
|
176
|
+
assert.deepEqual(sequences, [
|
|
177
|
+
1,
|
|
178
|
+
2
|
|
179
|
+
], `${mode} durable event sequences must be unique and strictly monotonic`);
|
|
180
|
+
assert.deepEqual(results.map((result)=>result.event?.sequence).sort((a, b)=>a - b), [
|
|
181
|
+
1,
|
|
182
|
+
2
|
|
183
|
+
]);
|
|
184
|
+
if (mode === 'user-turn') {
|
|
185
|
+
const userTurns = check.db.prepare("SELECT COUNT(*) AS n FROM events WHERE tenant_id = ? AND run_id = ? AND kind = 'booking.copilot.user.turn.observed'").get(check.tenant, taskId);
|
|
186
|
+
assert.equal(userTurns.n, 3, 'initial plus two concurrent user turns must all be durable');
|
|
187
|
+
}
|
|
188
|
+
check.close();
|
|
189
|
+
} finally{
|
|
190
|
+
rmSync(root, {
|
|
191
|
+
recursive: true,
|
|
192
|
+
force: true
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
await run('emit');
|
|
197
|
+
await run('user-turn');
|
|
198
|
+
console.log('BOOKING COPILOT EVENT SEQUENCE CONCURRENCY PROOF: emit/user-turn multi-process sequence unique and strictly monotonic');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
//# sourceURL=ts/scripts/booking-copilot-event-sequence-concurrency-proof-tests.ts
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { validateActionReceiptV1 } from '../src/booking-surface/validation.js';
|
|
3
|
+
const receiptBase = {
|
|
4
|
+
schemaVersion: 'booking.surface.v1',
|
|
5
|
+
kind: 'action.receipt',
|
|
6
|
+
actionId: 'offers-query-gap-contract',
|
|
7
|
+
contextRef: 'ctx-gap-contract',
|
|
8
|
+
revision: 1
|
|
9
|
+
};
|
|
10
|
+
const ratesFailedResult = {
|
|
11
|
+
outcome: 'partial',
|
|
12
|
+
requestedCount: 1,
|
|
13
|
+
actualCount: 1,
|
|
14
|
+
hardCriteriaMet: true,
|
|
15
|
+
factRefs: [
|
|
16
|
+
'hotel:hotel-1',
|
|
17
|
+
'hotel:hotel-2',
|
|
18
|
+
'offer:breakfast'
|
|
19
|
+
]
|
|
20
|
+
};
|
|
21
|
+
const ratesFailedObservation = {
|
|
22
|
+
kind: 'offers.state',
|
|
23
|
+
hotelRefs: [
|
|
24
|
+
'hotel-2'
|
|
25
|
+
],
|
|
26
|
+
offerRefs: [
|
|
27
|
+
'breakfast'
|
|
28
|
+
],
|
|
29
|
+
loadedHotelCount: 1
|
|
30
|
+
};
|
|
31
|
+
const oldRatesFailedReceipt = {
|
|
32
|
+
...receiptBase,
|
|
33
|
+
status: 'partial',
|
|
34
|
+
observation: ratesFailedObservation,
|
|
35
|
+
resultContract: {
|
|
36
|
+
...ratesFailedResult,
|
|
37
|
+
gapCodes: [
|
|
38
|
+
'hotel_rates_failed:hotel-1'
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const newRatesFailedReceipt = {
|
|
43
|
+
...oldRatesFailedReceipt,
|
|
44
|
+
resultContract: {
|
|
45
|
+
...ratesFailedResult,
|
|
46
|
+
gapCodes: [
|
|
47
|
+
'hotel_rates_failed'
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
assert.equal(validateActionReceiptV1(oldRatesFailedReceipt).ok, false, 'legacy dynamic supplier gap code must fail validation');
|
|
52
|
+
assert.deepEqual(validateActionReceiptV1(newRatesFailedReceipt), {
|
|
53
|
+
ok: true
|
|
54
|
+
}, 'fixed supplier gap code with hotel fact ref must validate');
|
|
55
|
+
assert.deepEqual(newRatesFailedReceipt.resultContract.factRefs, [
|
|
56
|
+
'hotel:hotel-1',
|
|
57
|
+
'hotel:hotel-2',
|
|
58
|
+
'offer:breakfast'
|
|
59
|
+
]);
|
|
60
|
+
const oldNotVisibleReceipt = {
|
|
61
|
+
...receiptBase,
|
|
62
|
+
actionId: 'offers-query-hidden-hotel-gap-contract',
|
|
63
|
+
status: 'no_match',
|
|
64
|
+
observation: {
|
|
65
|
+
kind: 'offers.state',
|
|
66
|
+
hotelRefs: [],
|
|
67
|
+
offerRefs: [],
|
|
68
|
+
loadedHotelCount: 0
|
|
69
|
+
},
|
|
70
|
+
resultContract: {
|
|
71
|
+
outcome: 'empty',
|
|
72
|
+
requestedCount: 1,
|
|
73
|
+
actualCount: 0,
|
|
74
|
+
hardCriteriaMet: false,
|
|
75
|
+
factRefs: [
|
|
76
|
+
'hotel:not-visible'
|
|
77
|
+
],
|
|
78
|
+
gapCodes: [
|
|
79
|
+
'hotel_not_visible:not-visible'
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const newNotVisibleReceipt = {
|
|
84
|
+
...oldNotVisibleReceipt,
|
|
85
|
+
resultContract: {
|
|
86
|
+
...oldNotVisibleReceipt.resultContract,
|
|
87
|
+
gapCodes: [
|
|
88
|
+
'hotel_not_visible'
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
assert.equal(validateActionReceiptV1(oldNotVisibleReceipt).ok, false, 'legacy dynamic visibility gap code must fail validation');
|
|
93
|
+
assert.deepEqual(validateActionReceiptV1(newNotVisibleReceipt), {
|
|
94
|
+
ok: true
|
|
95
|
+
}, 'fixed visibility gap code with hotel fact ref must validate');
|
|
96
|
+
assert.deepEqual(newNotVisibleReceipt.resultContract.factRefs, [
|
|
97
|
+
'hotel:not-visible'
|
|
98
|
+
]);
|
|
99
|
+
console.log('BOOKING COPILOT GAP CODE CONTRACT PROOF: dynamic legacy gap codes fail; fixed codes with hotel factRefs pass');
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
//# sourceURL=ts/scripts/booking-copilot-gap-code-contract-proof-tests.ts
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { existsSync, mkdtempSync, mkdirSync, 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
|
+
const taskId = 'task-operation-atomicity';
|
|
10
|
+
const workspace = {
|
|
11
|
+
schemaVersion: 'booking.surface.v1',
|
|
12
|
+
contextRef: 'ctx-operation-atomicity',
|
|
13
|
+
surface: 'tenant',
|
|
14
|
+
revision: 0,
|
|
15
|
+
locale: 'en-US',
|
|
16
|
+
currency: 'AED',
|
|
17
|
+
searchDraft: {},
|
|
18
|
+
results: {
|
|
19
|
+
status: 'idle'
|
|
20
|
+
},
|
|
21
|
+
visibleHotels: [],
|
|
22
|
+
loadedOffers: [],
|
|
23
|
+
shortlistedOfferRefs: [],
|
|
24
|
+
capabilities: {
|
|
25
|
+
surface: 'tenant',
|
|
26
|
+
allowedActions: [
|
|
27
|
+
'search.patch',
|
|
28
|
+
'search.run'
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const base = {
|
|
33
|
+
schemaVersion: 'booking.surface.v1',
|
|
34
|
+
kind: 'search.patch',
|
|
35
|
+
actionId: 'action-operation-atomicity',
|
|
36
|
+
contextRef: workspace.contextRef,
|
|
37
|
+
expectedRevision: 0,
|
|
38
|
+
reason: 'raw-reason-marker@example.invalid Bearer RAW_SECRET',
|
|
39
|
+
factRefs: [
|
|
40
|
+
'fact:opaque'
|
|
41
|
+
],
|
|
42
|
+
input: {
|
|
43
|
+
patch: {
|
|
44
|
+
destination: {
|
|
45
|
+
query: 'raw-input-marker@example.invalid'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
const child = process.argv.indexOf('--child') >= 0;
|
|
51
|
+
const replayMode = process.argv.indexOf('--replay') >= 0;
|
|
52
|
+
const root = child ? process.argv[process.argv.indexOf('--child') + 1] : '';
|
|
53
|
+
const variant = child ? process.argv[process.argv.indexOf('--child') + 2] : '';
|
|
54
|
+
if (replayMode) {
|
|
55
|
+
const replayRoot = process.argv[process.argv.indexOf('--replay') + 1];
|
|
56
|
+
const replayVariant = process.argv[process.argv.indexOf('--replay') + 2] ?? 'base';
|
|
57
|
+
const replayLedger = ensureLedger(replayRoot);
|
|
58
|
+
const replayAction = replayVariant === 'conflict' ? action('conflict') : replayVariant === 'different' ? action('different') : base;
|
|
59
|
+
try {
|
|
60
|
+
console.log(JSON.stringify({
|
|
61
|
+
result: 'success',
|
|
62
|
+
event: new BookingCopilotTaskRuntime(replayLedger).issueOperation(taskId, replayAction)
|
|
63
|
+
}));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.log(JSON.stringify({
|
|
66
|
+
result: 'error',
|
|
67
|
+
error: error instanceof Error ? error.message : String(error)
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
70
|
+
replayLedger.close();
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
function waitFile(path, timeout = 30_000) {
|
|
74
|
+
return new Promise((resolve, reject)=>{
|
|
75
|
+
const end = Date.now() + timeout;
|
|
76
|
+
const tick = ()=>existsSync(path) ? resolve() : Date.now() > end ? reject(new Error(`timeout:${path}`)) : setTimeout(tick, 5);
|
|
77
|
+
tick();
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function action(which) {
|
|
81
|
+
return {
|
|
82
|
+
...base,
|
|
83
|
+
kind: which === 'kind' ? 'search.run' : base.kind,
|
|
84
|
+
reason: which === 'conflict' ? 'different reason' : base.reason,
|
|
85
|
+
contextRef: which === 'context' ? 'ctx-other' : base.contextRef,
|
|
86
|
+
expectedRevision: which === 'revision' ? 1 : base.expectedRevision,
|
|
87
|
+
factRefs: which === 'fact-order' ? [
|
|
88
|
+
'fact:other',
|
|
89
|
+
'fact:opaque'
|
|
90
|
+
] : base.factRefs,
|
|
91
|
+
input: which === 'kind' ? {} : which === 'input' ? {
|
|
92
|
+
patch: {
|
|
93
|
+
destination: {
|
|
94
|
+
query: 'other'
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
} : base.input,
|
|
98
|
+
actionId: which === 'different' ? 'action-different' : base.actionId
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (child) {
|
|
102
|
+
const ledger = ensureLedger(root);
|
|
103
|
+
const runtime = new BookingCopilotTaskRuntime(ledger);
|
|
104
|
+
const original = runtime.resumeTask.bind(runtime);
|
|
105
|
+
let first = true;
|
|
106
|
+
writeFileSync(join(root, `started-${variant}`), 'started');
|
|
107
|
+
const gateEnd = Date.now() + 10_000;
|
|
108
|
+
while(!existsSync(join(root, 'start'))){
|
|
109
|
+
if (Date.now() > gateEnd) throw new Error('start_gate_timeout');
|
|
110
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
111
|
+
}
|
|
112
|
+
if (variant !== 'left') Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
113
|
+
runtime.resumeTask = (id)=>{
|
|
114
|
+
const state = original(id);
|
|
115
|
+
if (first && !state?.pendingAction) {
|
|
116
|
+
first = false;
|
|
117
|
+
writeFileSync(join(root, `ready-${variant}`), 'ready');
|
|
118
|
+
const end = Date.now() + 10_000;
|
|
119
|
+
while(!existsSync(join(root, 'release'))){
|
|
120
|
+
if (Date.now() > end) throw new Error('barrier_timeout');
|
|
121
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return state;
|
|
125
|
+
};
|
|
126
|
+
try {
|
|
127
|
+
const event = runtime.issueOperation(taskId, action(variant));
|
|
128
|
+
console.log(JSON.stringify({
|
|
129
|
+
result: 'success',
|
|
130
|
+
event
|
|
131
|
+
}));
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.log(JSON.stringify({
|
|
134
|
+
result: 'error',
|
|
135
|
+
error: error instanceof Error ? error.message : String(error)
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
ledger.close();
|
|
139
|
+
process.exit(0);
|
|
140
|
+
}
|
|
141
|
+
async function pair(root, left, right) {
|
|
142
|
+
const tsx = fileURLToPath(new URL('../node_modules/tsx/dist/cli.mjs', import.meta.url));
|
|
143
|
+
const script = fileURLToPath(import.meta.url);
|
|
144
|
+
const ps = [
|
|
145
|
+
left,
|
|
146
|
+
right
|
|
147
|
+
].map((v)=>spawn(process.execPath, [
|
|
148
|
+
tsx,
|
|
149
|
+
script,
|
|
150
|
+
'--child',
|
|
151
|
+
root,
|
|
152
|
+
v
|
|
153
|
+
], {
|
|
154
|
+
cwd: process.cwd(),
|
|
155
|
+
stdio: [
|
|
156
|
+
'ignore',
|
|
157
|
+
'pipe',
|
|
158
|
+
'pipe'
|
|
159
|
+
]
|
|
160
|
+
}));
|
|
161
|
+
const out = ps.map(()=>'');
|
|
162
|
+
ps.forEach((p, i)=>{
|
|
163
|
+
p.stdout.on('data', (b)=>{
|
|
164
|
+
out[i] += b;
|
|
165
|
+
});
|
|
166
|
+
p.stderr.on('data', (b)=>{
|
|
167
|
+
out[i] += b;
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
await Promise.all([
|
|
171
|
+
waitFile(join(root, `started-${left}`)),
|
|
172
|
+
waitFile(join(root, `started-${right}`))
|
|
173
|
+
]);
|
|
174
|
+
writeFileSync(join(root, 'start'), 'start');
|
|
175
|
+
try {
|
|
176
|
+
await Promise.race([
|
|
177
|
+
waitFile(join(root, `ready-${left}`)),
|
|
178
|
+
waitFile(join(root, `ready-${right}`))
|
|
179
|
+
]);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
ps.forEach((p)=>p.kill('SIGKILL'));
|
|
182
|
+
throw new Error(`workers_did_not_reach_callsite:${out.join('|')}:${error}`);
|
|
183
|
+
}
|
|
184
|
+
await new Promise((resolve)=>setTimeout(resolve, 250));
|
|
185
|
+
assert.equal(existsSync(join(root, `ready-${left}`)) && existsSync(join(root, `ready-${right}`)), false, 'both workers entered transaction before release');
|
|
186
|
+
writeFileSync(join(root, 'release'), 'release');
|
|
187
|
+
try {
|
|
188
|
+
await Promise.all(ps.map((p)=>new Promise((resolve, reject)=>{
|
|
189
|
+
const timer = setTimeout(()=>reject(new Error('worker_timeout')), 10_000);
|
|
190
|
+
p.on('exit', (code)=>{
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
code === 0 ? resolve() : reject(new Error(`worker_exit:${code}:${out.join('|')}`));
|
|
193
|
+
});
|
|
194
|
+
})));
|
|
195
|
+
} finally{
|
|
196
|
+
ps.forEach((p)=>{
|
|
197
|
+
if (p.exitCode === null) p.kill('SIGKILL');
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return out.map((s)=>JSON.parse(s.trim().split('\n').at(-1)));
|
|
201
|
+
}
|
|
202
|
+
async function run(kind, conflictVariant = 'conflict') {
|
|
203
|
+
const root = mkdtempSync(join(tmpdir(), `gotry-operation-${kind}-`));
|
|
204
|
+
mkdirSync(root, {
|
|
205
|
+
recursive: true
|
|
206
|
+
});
|
|
207
|
+
const ledger = ensureLedger(root);
|
|
208
|
+
const runtime = new BookingCopilotTaskRuntime(ledger);
|
|
209
|
+
const turn = {
|
|
210
|
+
schemaVersion: 'booking.surface.v1',
|
|
211
|
+
kind: 'user.turn',
|
|
212
|
+
taskId,
|
|
213
|
+
workspace,
|
|
214
|
+
request: {
|
|
215
|
+
text: 'opaque'
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
runtime.startTask(turn);
|
|
219
|
+
const result = await pair(root, kind === 'different' ? 'different' : 'left', kind === 'conflict' ? conflictVariant : 'right');
|
|
220
|
+
const success = result.filter((r)=>r.result === 'success');
|
|
221
|
+
assert.equal(success.length, kind === 'same' ? 2 : 1);
|
|
222
|
+
if (kind === 'conflict') assert.equal(result.filter((r)=>r.error === 'action_conflict').length, 1, JSON.stringify(result));
|
|
223
|
+
if (kind === 'different') assert.equal(result.filter((r)=>r.error === 'receipt_required').length, 1);
|
|
224
|
+
if (kind === 'same') assert.deepEqual(success[0].event, success[1].event);
|
|
225
|
+
assert.equal(ledger.db.prepare("SELECT COUNT(*) AS n FROM events WHERE kind = 'booking.copilot.action.issued'").get().n, 1);
|
|
226
|
+
const durable = ledger.db.prepare("SELECT payload FROM events WHERE kind = 'booking.copilot.action.issued'").get();
|
|
227
|
+
assert.ok(!/raw-reason-marker|raw-input-marker|Bearer RAW_SECRET|@example|secret/i.test(durable.payload));
|
|
228
|
+
if (kind === 'same') {
|
|
229
|
+
const event = success[0].event;
|
|
230
|
+
ledger.close();
|
|
231
|
+
const tsx = fileURLToPath(new URL('../node_modules/tsx/dist/cli.mjs', import.meta.url));
|
|
232
|
+
async function replayVariant(v) {
|
|
233
|
+
const replayChild = spawn(process.execPath, [
|
|
234
|
+
tsx,
|
|
235
|
+
fileURLToPath(import.meta.url),
|
|
236
|
+
'--replay',
|
|
237
|
+
root,
|
|
238
|
+
v
|
|
239
|
+
], {
|
|
240
|
+
cwd: process.cwd(),
|
|
241
|
+
stdio: [
|
|
242
|
+
'ignore',
|
|
243
|
+
'pipe',
|
|
244
|
+
'pipe'
|
|
245
|
+
]
|
|
246
|
+
});
|
|
247
|
+
let replayOutput = '';
|
|
248
|
+
replayChild.stdout.on('data', (b)=>{
|
|
249
|
+
replayOutput += b;
|
|
250
|
+
});
|
|
251
|
+
replayChild.stderr.on('data', (b)=>{
|
|
252
|
+
replayOutput += b;
|
|
253
|
+
});
|
|
254
|
+
await new Promise((resolve, reject)=>{
|
|
255
|
+
const timer = setTimeout(()=>{
|
|
256
|
+
replayChild.kill('SIGKILL');
|
|
257
|
+
reject(new Error('replay_timeout'));
|
|
258
|
+
}, 10_000);
|
|
259
|
+
replayChild.on('exit', (code)=>{
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
code === 0 ? resolve() : reject(new Error(`replay_exit:${code}:${replayOutput}`));
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
return JSON.parse(replayOutput.trim().split('\n').at(-1));
|
|
265
|
+
}
|
|
266
|
+
const replayResult = await replayVariant('base');
|
|
267
|
+
assert.deepEqual(replayResult.event, event);
|
|
268
|
+
const conflictResult = await replayVariant('conflict');
|
|
269
|
+
assert.equal(conflictResult.error, 'action_conflict');
|
|
270
|
+
const differentResult = await replayVariant('different');
|
|
271
|
+
assert.equal(differentResult.error, 'receipt_required');
|
|
272
|
+
const restarted = ensureLedger(root);
|
|
273
|
+
assert.equal(restarted.db.prepare("SELECT COUNT(*) AS n FROM events WHERE kind = 'booking.copilot.action.issued'").get().n, 1);
|
|
274
|
+
const replay = replayResult.event;
|
|
275
|
+
const pending = new BookingCopilotTaskRuntime(restarted).resumeTask(taskId)?.pendingAction;
|
|
276
|
+
assert.deepEqual(pending && {
|
|
277
|
+
eventId: pending.eventId,
|
|
278
|
+
sequence: pending.sequence,
|
|
279
|
+
emittedAt: pending.emittedAt
|
|
280
|
+
}, {
|
|
281
|
+
eventId: replay.eventId,
|
|
282
|
+
sequence: replay.sequence,
|
|
283
|
+
emittedAt: replay.emittedAt
|
|
284
|
+
});
|
|
285
|
+
restarted.close();
|
|
286
|
+
} else {
|
|
287
|
+
ledger.close();
|
|
288
|
+
}
|
|
289
|
+
rmSync(root, {
|
|
290
|
+
recursive: true,
|
|
291
|
+
force: true
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
if (!child) {
|
|
295
|
+
for(let round = 0; round < 20; round++){
|
|
296
|
+
await run('same');
|
|
297
|
+
await run('conflict', [
|
|
298
|
+
'conflict',
|
|
299
|
+
'kind',
|
|
300
|
+
'context',
|
|
301
|
+
'revision',
|
|
302
|
+
'fact-order',
|
|
303
|
+
'input'
|
|
304
|
+
][round % 6]);
|
|
305
|
+
await run('different');
|
|
306
|
+
}
|
|
307
|
+
const unsafeRoot = mkdtempSync(join(tmpdir(), 'gotry-operation-unsafe-'));
|
|
308
|
+
const unsafeLedger = ensureLedger(unsafeRoot);
|
|
309
|
+
const unsafeRuntime = new BookingCopilotTaskRuntime(unsafeLedger);
|
|
310
|
+
unsafeRuntime.startTask({
|
|
311
|
+
schemaVersion: 'booking.surface.v1',
|
|
312
|
+
kind: 'user.turn',
|
|
313
|
+
taskId,
|
|
314
|
+
workspace,
|
|
315
|
+
request: {
|
|
316
|
+
text: 'opaque'
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
assert.throws(()=>unsafeRuntime.issueOperation(taskId, {
|
|
320
|
+
...base,
|
|
321
|
+
factRefs: [
|
|
322
|
+
'attacker@example.invalid'
|
|
323
|
+
]
|
|
324
|
+
}), /unsafe_fact_ref/);
|
|
325
|
+
assert.throws(()=>unsafeRuntime.issueOperation(taskId, {
|
|
326
|
+
...base,
|
|
327
|
+
actionId: 'attacker@example.invalid'
|
|
328
|
+
}), /unsafe_action_id/);
|
|
329
|
+
assert.throws(()=>unsafeRuntime.issueOperation(taskId, {
|
|
330
|
+
...base,
|
|
331
|
+
actionId: 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.signature'
|
|
332
|
+
}), /unsafe_action_id/);
|
|
333
|
+
assert.equal(unsafeLedger.countEvents(), 2);
|
|
334
|
+
unsafeLedger.close();
|
|
335
|
+
rmSync(unsafeRoot, {
|
|
336
|
+
recursive: true,
|
|
337
|
+
force: true
|
|
338
|
+
});
|
|
339
|
+
for (const field of [
|
|
340
|
+
'actionId',
|
|
341
|
+
'factRefs',
|
|
342
|
+
'expectedRevision'
|
|
343
|
+
]){
|
|
344
|
+
const tamperRoot = mkdtempSync(join(tmpdir(), `gotry-operation-tamper-${field}-`));
|
|
345
|
+
const tamperLedger = ensureLedger(tamperRoot);
|
|
346
|
+
const tamperRuntime = new BookingCopilotTaskRuntime(tamperLedger);
|
|
347
|
+
tamperRuntime.startTask({
|
|
348
|
+
schemaVersion: 'booking.surface.v1',
|
|
349
|
+
kind: 'user.turn',
|
|
350
|
+
taskId,
|
|
351
|
+
workspace,
|
|
352
|
+
request: {
|
|
353
|
+
text: 'opaque'
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
tamperRuntime.issueOperation(taskId, base);
|
|
357
|
+
const row = tamperLedger.db.prepare("SELECT seq, payload FROM events WHERE kind = 'booking.copilot.action.issued'").get();
|
|
358
|
+
const payload = JSON.parse(row.payload);
|
|
359
|
+
payload.action[field] = field === 'factRefs' ? [
|
|
360
|
+
'tampered'
|
|
361
|
+
] : field === 'expectedRevision' ? 99 : 'tampered-action';
|
|
362
|
+
tamperLedger.db.prepare('UPDATE events SET payload = ? WHERE seq = ?').run(JSON.stringify(payload), row.seq);
|
|
363
|
+
assert.throws(()=>new BookingCopilotTaskRuntime(tamperLedger).resumeTask(taskId), /ledger_corrupt/);
|
|
364
|
+
tamperLedger.close();
|
|
365
|
+
rmSync(tamperRoot, {
|
|
366
|
+
recursive: true,
|
|
367
|
+
force: true
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
console.log('BOOKING COPILOT OPERATION LEDGER CONCURRENCY PROOF: 20 rounds atomic');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
//# sourceURL=ts/scripts/booking-copilot-operation-ledger-concurrency-proof-tests.ts
|