@modelprofile.com/flexharness 11.3.1 → 12.0.0
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/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.flexharness.core.d.ts +4 -0
- package/dist_ts/classes.flexharness.core.js +69 -25
- package/dist_ts/classes.flexharness.imports.js +4 -3
- package/dist_ts/classes.flexharness.reversions.d.ts +7 -1
- package/dist_ts/classes.flexharness.reversions.js +43 -29
- package/dist_ts/classes.flexharness.sessions.js +2 -6
- package/dist_ts/classes.stores.js +20 -18
- package/dist_ts/index.d.ts +1 -0
- package/dist_ts/index.js +2 -1
- package/dist_ts/interfaces.d.ts +24 -2
- package/dist_ts/interfaces.flexharness.d.ts +8 -1
- package/dist_ts/interfaces.js +1 -1
- package/dist_ts/plugins.d.ts +3 -3
- package/dist_ts/plugins.js +3 -3
- package/dist_ts/utils.projectionchange.d.ts +34 -0
- package/dist_ts/utils.projectionchange.js +135 -0
- package/dist_ts/utils.storecodecs.d.ts +10 -3
- package/dist_ts/utils.storecodecs.js +29 -4
- package/dist_ts/utils.storerules.d.ts +1 -1
- package/dist_ts/utils.storerules.js +1 -1
- package/dist_ts_migration/v3_legacyflexharness.js +6 -5
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +5 -4
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +44 -25
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +152 -20
- package/dist_ts_stores_nosqldb/classes.recordstore.js +369 -66
- package/dist_ts_stores_nosqldb/index.d.ts +1 -1
- package/dist_ts_stores_nosqldb/index.js +2 -2
- package/dist_ts_stores_nosqldb/plugins.d.ts +1 -1
- package/dist_ts_stores_nosqldb/plugins.js +1 -1
- package/dist_ts_stores_testing/contract.d.ts +33 -0
- package/dist_ts_stores_testing/contract.js +566 -0
- package/dist_ts_stores_testing/fixtures.d.ts +20 -0
- package/dist_ts_stores_testing/fixtures.js +112 -0
- package/dist_ts_stores_testing/index.d.ts +2 -0
- package/dist_ts_stores_testing/index.js +3 -0
- package/dist_ts_stores_testing/plugins.d.ts +5 -0
- package/dist_ts_stores_testing/plugins.js +7 -0
- package/package.json +9 -3
- package/readme.md +6 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.flexharness.core.ts +74 -24
- package/ts/classes.flexharness.imports.ts +3 -2
- package/ts/classes.flexharness.reversions.ts +53 -28
- package/ts/classes.flexharness.sessions.ts +2 -5
- package/ts/classes.stores.ts +40 -37
- package/ts/index.ts +5 -0
- package/ts/interfaces.flexharness.ts +8 -1
- package/ts/interfaces.ts +27 -2
- package/ts/plugins.ts +12 -7
- package/ts/readme.md +6 -3
- package/ts/utils.projectionchange.ts +190 -0
- package/ts/utils.storecodecs.ts +42 -7
- package/ts/utils.storerules.ts +2 -2
- package/ts_migration/v3_legacyflexharness.ts +11 -6
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +59 -46
- package/ts_stores_nosqldb/classes.recordstore.ts +445 -64
- package/ts_stores_nosqldb/index.ts +1 -0
- package/ts_stores_nosqldb/plugins.ts +4 -3
- package/ts_stores_nosqldb/readme.md +6 -2
- package/ts_stores_testing/contract.ts +776 -0
- package/ts_stores_testing/fixtures.ts +127 -0
- package/ts_stores_testing/index.ts +17 -0
- package/ts_stores_testing/plugins.ts +20 -0
- package/ts_stores_testing/readme.md +62 -0
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
FlexHarnessStoreConflictError,
|
|
4
|
+
FlexHarnessStoreFormatError,
|
|
5
|
+
FlexHarnessValidationError,
|
|
6
|
+
} from '../ts/errors.js';
|
|
7
|
+
import type {
|
|
8
|
+
IFlexHarnessStores,
|
|
9
|
+
IFlexProjectManagementSnapshot,
|
|
10
|
+
IFlexProjectManagementTombstone,
|
|
11
|
+
IFlexProjectionChange,
|
|
12
|
+
IFlexProjectionSnapshotCurrent,
|
|
13
|
+
IFlexScopeSnapshot,
|
|
14
|
+
} from '../ts/interfaces.js';
|
|
15
|
+
import { createWholeProjectionChange } from '../ts/utils.projectionchange.js';
|
|
16
|
+
import {
|
|
17
|
+
agentEvent,
|
|
18
|
+
appendEvents,
|
|
19
|
+
finishedJob,
|
|
20
|
+
permissionSnapshot,
|
|
21
|
+
projectionSnapshot,
|
|
22
|
+
projectionWithRuns,
|
|
23
|
+
runMessages,
|
|
24
|
+
runSegment,
|
|
25
|
+
scopeSnapshot,
|
|
26
|
+
} from './fixtures.js';
|
|
27
|
+
|
|
28
|
+
/** One store bundle over an isolated, initially empty backing store. */
|
|
29
|
+
export interface IFlexStoreContractFixture {
|
|
30
|
+
stores: IFlexHarnessStores;
|
|
31
|
+
/**
|
|
32
|
+
* Opens another bundle over the same backing store, as a second process would. Required when
|
|
33
|
+
* the backend declares `sharedBackingStore`.
|
|
34
|
+
*/
|
|
35
|
+
openPeer?(): Promise<IFlexHarnessStores>;
|
|
36
|
+
/** Releases every bundle and removes the backing store. */
|
|
37
|
+
close(): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface IFlexStoreContractBackend {
|
|
41
|
+
/** Names the backend in every test title. */
|
|
42
|
+
name: string;
|
|
43
|
+
/** Several bundles can open one backing store, as separate processes would. */
|
|
44
|
+
sharedBackingStore: boolean;
|
|
45
|
+
/** The Agent event and tool job providers implement `releaseSession`. */
|
|
46
|
+
releasesSessions: boolean;
|
|
47
|
+
/** The bundle provides `results`. */
|
|
48
|
+
storesResults: boolean;
|
|
49
|
+
open(): Promise<IFlexStoreContractFixture>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** One case of the store contract. It opens and closes its own fixture and throws when it fails. */
|
|
53
|
+
export interface IFlexStoreContractTest {
|
|
54
|
+
title: string;
|
|
55
|
+
run(): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type TErrorClass = abstract new (...args: never[]) => Error;
|
|
59
|
+
|
|
60
|
+
function liveProjectState(
|
|
61
|
+
generation: string,
|
|
62
|
+
revision = 1,
|
|
63
|
+
sequence = 1,
|
|
64
|
+
): IFlexProjectManagementSnapshot {
|
|
65
|
+
return {
|
|
66
|
+
schemaVersion: 1,
|
|
67
|
+
revision,
|
|
68
|
+
sessionGenerationId: generation,
|
|
69
|
+
sessionGenerationSequence: sequence,
|
|
70
|
+
scratchpad: '',
|
|
71
|
+
tasks: [],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function projectTombstone(
|
|
76
|
+
generation: string,
|
|
77
|
+
revision = 1,
|
|
78
|
+
sequence = 1,
|
|
79
|
+
): IFlexProjectManagementTombstone {
|
|
80
|
+
return {
|
|
81
|
+
schemaVersion: 1,
|
|
82
|
+
revision,
|
|
83
|
+
sessionGenerationId: generation,
|
|
84
|
+
sessionGenerationSequence: sequence,
|
|
85
|
+
deletedAt: '2026-08-14T00:00:00.000Z',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Exactly one of `attempts` succeeded and another was rejected with `errorClass`. */
|
|
90
|
+
function assertOneWinner(attempts: PromiseSettledResult<unknown>[], errorClass: TErrorClass): void {
|
|
91
|
+
plugins.assert.equal(attempts.filter((result) => result.status === 'fulfilled').length, 1);
|
|
92
|
+
const rejection = attempts.find((result): result is PromiseRejectedResult => result.status === 'rejected');
|
|
93
|
+
plugins.assert.ok(rejection?.reason instanceof errorClass, `expected a ${errorClass.name}, got ${String(rejection?.reason)}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `operation` fails as an awaiting caller sees it: FlexHarness awaits every store call, so a store
|
|
98
|
+
* may reject or throw.
|
|
99
|
+
*/
|
|
100
|
+
function assertRejects(
|
|
101
|
+
operation: () => unknown,
|
|
102
|
+
expected?: plugins.assert.AssertPredicate,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
return expected === undefined
|
|
105
|
+
? plugins.assert.rejects(async () => operation())
|
|
106
|
+
: plugins.assert.rejects(async () => operation(), expected);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** `operation` rejects with `errorClass` and a message that contains `text`. */
|
|
110
|
+
async function assertRejectsWith(
|
|
111
|
+
operation: () => Promise<unknown>,
|
|
112
|
+
errorClass: TErrorClass,
|
|
113
|
+
text: string,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
await assertRejects(operation, (error: unknown) => {
|
|
116
|
+
plugins.assert.ok(error instanceof errorClass, `expected a ${errorClass.name}, got ${String(error)}`);
|
|
117
|
+
plugins.assert.ok(error.message.includes(text), `expected "${text}" in: ${error.message}`);
|
|
118
|
+
return true;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* A store may check a change against the stored value when it saves it or when it next loads the
|
|
124
|
+
* value; either way the change must not load. Returns which of the two rejected it.
|
|
125
|
+
*/
|
|
126
|
+
async function assertChangeRejected(
|
|
127
|
+
save: () => Promise<unknown>,
|
|
128
|
+
load: () => Promise<unknown>,
|
|
129
|
+
): Promise<'save' | 'load'> {
|
|
130
|
+
try {
|
|
131
|
+
await save();
|
|
132
|
+
} catch (error) {
|
|
133
|
+
plugins.assert.ok(error instanceof FlexHarnessStoreFormatError, `expected a format error, got ${String(error)}`);
|
|
134
|
+
return 'save';
|
|
135
|
+
}
|
|
136
|
+
await assertRejects(load, FlexHarnessStoreFormatError);
|
|
137
|
+
return 'load';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The behaviour every `IFlexHarnessStores` implementation shares, as test cases for any runner.
|
|
142
|
+
* Each case opens its own fixture, so backends need no cleanup between cases.
|
|
143
|
+
*/
|
|
144
|
+
export function createFlexStoreContractTests(
|
|
145
|
+
backend: IFlexStoreContractBackend,
|
|
146
|
+
): IFlexStoreContractTest[] {
|
|
147
|
+
const tests: IFlexStoreContractTest[] = [];
|
|
148
|
+
const contractTest = (
|
|
149
|
+
title: string,
|
|
150
|
+
body: (fixture: IFlexStoreContractFixture) => Promise<void>,
|
|
151
|
+
): void => {
|
|
152
|
+
tests.push({
|
|
153
|
+
title: `${backend.name}: ${title}`,
|
|
154
|
+
async run() {
|
|
155
|
+
const fixture = await backend.open();
|
|
156
|
+
try {
|
|
157
|
+
await body(fixture);
|
|
158
|
+
} finally {
|
|
159
|
+
await fixture.close();
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
contractTest('scope store clones snapshots and enforces CAS', async ({ stores }) => {
|
|
166
|
+
plugins.assert.equal(await stores.scopes.load('scope'), undefined);
|
|
167
|
+
const first = scopeSnapshot(1);
|
|
168
|
+
await stores.scopes.save('scope', first, 0);
|
|
169
|
+
first.sessions[0].status = 'failed';
|
|
170
|
+
const loaded = await stores.scopes.load('scope');
|
|
171
|
+
plugins.assert.ok(loaded);
|
|
172
|
+
plugins.assert.equal(loaded.sessions[0].status, 'idle');
|
|
173
|
+
loaded.sessions[0].status = 'failed';
|
|
174
|
+
plugins.assert.equal((await stores.scopes.load('scope'))?.sessions[0].status, 'idle');
|
|
175
|
+
|
|
176
|
+
assertOneWinner(await Promise.allSettled([
|
|
177
|
+
stores.scopes.save('scope', scopeSnapshot(2, 'session-a'), 1),
|
|
178
|
+
stores.scopes.save('scope', scopeSnapshot(2, 'session-b'), 1),
|
|
179
|
+
]), FlexHarnessStoreConflictError);
|
|
180
|
+
plugins.assert.equal((await stores.scopes.load('scope'))?.revision, 2);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
contractTest('saves require the next revision and report the stored one', async ({ stores }) => {
|
|
184
|
+
await assertRejects(() => stores.scopes.save('scope', scopeSnapshot(2), 0), FlexHarnessStoreFormatError);
|
|
185
|
+
await assertRejects(() => stores.scopes.save('scope', scopeSnapshot(0), -1), FlexHarnessValidationError);
|
|
186
|
+
plugins.assert.equal(await stores.scopes.load('scope'), undefined);
|
|
187
|
+
|
|
188
|
+
const saveProjection = (revision: number, expectedRevision: number) => stores.projections.save(
|
|
189
|
+
'scope',
|
|
190
|
+
'session-1',
|
|
191
|
+
createWholeProjectionChange(projectionSnapshot(revision)),
|
|
192
|
+
expectedRevision,
|
|
193
|
+
);
|
|
194
|
+
await saveProjection(1, 0);
|
|
195
|
+
await saveProjection(2, 1);
|
|
196
|
+
await assertRejects(() => saveProjection(4, 2), FlexHarnessStoreFormatError);
|
|
197
|
+
await assertRejects(() => saveProjection(2, 1), (error: unknown) => {
|
|
198
|
+
plugins.assert.ok(error instanceof FlexHarnessStoreConflictError);
|
|
199
|
+
plugins.assert.equal(error.expectedRevision, 1);
|
|
200
|
+
plugins.assert.equal(error.actualRevision, 2);
|
|
201
|
+
return true;
|
|
202
|
+
});
|
|
203
|
+
plugins.assert.equal((await stores.projections.load('scope', 'session-1'))?.revision, 2);
|
|
204
|
+
|
|
205
|
+
await stores.permissions.save('scope', 'session-1', permissionSnapshot(1), 0);
|
|
206
|
+
await assertRejects(
|
|
207
|
+
() => stores.permissions.save('scope', 'session-1', permissionSnapshot(1), 0),
|
|
208
|
+
FlexHarnessStoreConflictError,
|
|
209
|
+
);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
contractTest('session records are exact per storage key and session', async ({ stores }) => {
|
|
213
|
+
const projection = createWholeProjectionChange(projectionSnapshot(1));
|
|
214
|
+
await stores.projections.save('namespace-a', 'session', projection, 0);
|
|
215
|
+
await stores.permissions.save('namespace-a', 'session', permissionSnapshot(1), 0);
|
|
216
|
+
plugins.assert.equal(await stores.projections.load('namespace-b', 'session'), undefined);
|
|
217
|
+
plugins.assert.equal(await stores.permissions.load('namespace-b', 'session'), undefined);
|
|
218
|
+
plugins.assert.equal(await stores.projections.load('namespace-a', 'other'), undefined);
|
|
219
|
+
await stores.projections.save('namespace-b', 'session', projection, 0);
|
|
220
|
+
|
|
221
|
+
const eventsA = await stores.agentEvents.getStore('namespace-a', 'session');
|
|
222
|
+
const eventsB = await stores.agentEvents.getStore('namespace-b', 'session');
|
|
223
|
+
await eventsA.save('session', appendEvents(agentEvent('event-a')), 0);
|
|
224
|
+
plugins.assert.equal(await eventsB.load('session'), undefined);
|
|
225
|
+
await stores.projections.deleteSession('namespace-a', 'session');
|
|
226
|
+
plugins.assert.equal(await stores.projections.load('namespace-a', 'session'), undefined);
|
|
227
|
+
plugins.assert.equal((await stores.projections.load('namespace-b', 'session'))?.revision, 1);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
contractTest('split session domains stay independent', async ({ stores }) => {
|
|
231
|
+
await stores.scopes.save('scope', scopeSnapshot(1), 0);
|
|
232
|
+
await stores.projections.save('scope', 'session-1', createWholeProjectionChange(projectionSnapshot(1)), 0);
|
|
233
|
+
await stores.permissions.save('scope', 'session-1', permissionSnapshot(1), 0);
|
|
234
|
+
const eventStore = await stores.agentEvents.getStore('scope', 'session-1');
|
|
235
|
+
const jobStore = await stores.jobs.getStore('scope', 'session-1');
|
|
236
|
+
plugins.assert.equal(await eventStore.save('session-1', appendEvents(), 0), 1);
|
|
237
|
+
plugins.assert.equal(await jobStore.save([], 0), 1);
|
|
238
|
+
|
|
239
|
+
plugins.assert.equal((await stores.scopes.load('scope'))?.sessions.length, 1);
|
|
240
|
+
plugins.assert.equal((await stores.projections.load('scope', 'session-1'))?.revision, 1);
|
|
241
|
+
plugins.assert.deepEqual(
|
|
242
|
+
(await stores.permissions.load('scope', 'session-1'))?.rememberedPermissionKeys,
|
|
243
|
+
['shell:approved'],
|
|
244
|
+
);
|
|
245
|
+
plugins.assert.equal((await eventStore.load('session-1'))?.revision, 1);
|
|
246
|
+
plugins.assert.equal((await jobStore.load())?.revision, 1);
|
|
247
|
+
plugins.assert.equal(await stores.projections.load('scope', 'another-session'), undefined);
|
|
248
|
+
|
|
249
|
+
await Promise.all([
|
|
250
|
+
stores.projections.deleteSession('scope', 'session-1'),
|
|
251
|
+
stores.permissions.deleteSession('scope', 'session-1'),
|
|
252
|
+
stores.agentEvents.deleteSession('scope', 'session-1'),
|
|
253
|
+
stores.jobs.deleteSession('scope', 'session-1'),
|
|
254
|
+
]);
|
|
255
|
+
plugins.assert.equal(await stores.projections.load('scope', 'session-1'), undefined);
|
|
256
|
+
plugins.assert.equal(await stores.permissions.load('scope', 'session-1'), undefined);
|
|
257
|
+
plugins.assert.equal(await eventStore.load('session-1'), undefined);
|
|
258
|
+
plugins.assert.equal(await jobStore.load(), undefined);
|
|
259
|
+
plugins.assert.notEqual(await stores.agentEvents.getStore('scope', 'session-1'), eventStore);
|
|
260
|
+
plugins.assert.notEqual(await stores.jobs.getStore('scope', 'session-1'), jobStore);
|
|
261
|
+
plugins.assert.equal((await stores.scopes.load('scope'))?.sessions.length, 1);
|
|
262
|
+
|
|
263
|
+
await stores.agentEvents.deleteSession('scope', 'unknown');
|
|
264
|
+
await stores.jobs.deleteSession('scope', 'unknown');
|
|
265
|
+
const unknownEventStore = await stores.agentEvents.getStore('scope', 'unknown');
|
|
266
|
+
const unknownJobStore = await stores.jobs.getStore('scope', 'unknown');
|
|
267
|
+
await stores.agentEvents.deleteSession('scope', 'unknown');
|
|
268
|
+
await stores.jobs.deleteSession('scope', 'unknown');
|
|
269
|
+
plugins.assert.notEqual(await stores.agentEvents.getStore('scope', 'unknown'), unknownEventStore);
|
|
270
|
+
plugins.assert.notEqual(await stores.jobs.getStore('scope', 'unknown'), unknownJobStore);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
contractTest('stores reject non-serializable snapshots at their boundary', async ({ stores }) => {
|
|
274
|
+
const invalid = scopeSnapshot(1) as unknown as Record<string, unknown>;
|
|
275
|
+
invalid.nonJson = new Date();
|
|
276
|
+
await assertRejects(
|
|
277
|
+
() => stores.scopes.save('scope', invalid as unknown as IFlexScopeSnapshot, 0),
|
|
278
|
+
FlexHarnessStoreFormatError,
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
contractTest('projection saves apply list changes to the stored lists', async ({ stores }) => {
|
|
283
|
+
const save = (change: IFlexProjectionChange, expectedRevision: number) =>
|
|
284
|
+
stores.projections.save('scope', 'session-1', change, expectedRevision);
|
|
285
|
+
const load = () => stores.projections.load('scope', 'session-1');
|
|
286
|
+
await save(createWholeProjectionChange(projectionWithRuns(1, ['run-1'])), 0);
|
|
287
|
+
|
|
288
|
+
const appended = projectionWithRuns(2, ['run-1', 'run-2']);
|
|
289
|
+
await save({
|
|
290
|
+
...appended,
|
|
291
|
+
messages: { start: 0, end: 2, items: runMessages('run-2') },
|
|
292
|
+
reversionSegments: { start: 0, end: 1, items: [runSegment('run-2')] },
|
|
293
|
+
}, 1);
|
|
294
|
+
plugins.assert.deepEqual(await load(), appended);
|
|
295
|
+
|
|
296
|
+
const trimmed = projectionWithRuns(3, ['run-2', 'run-3']);
|
|
297
|
+
await save({
|
|
298
|
+
...trimmed,
|
|
299
|
+
messages: { start: 2, end: 4, items: runMessages('run-3') },
|
|
300
|
+
reversionSegments: { start: 1, end: 2, items: [runSegment('run-3')] },
|
|
301
|
+
}, 2);
|
|
302
|
+
plugins.assert.deepEqual(await load(), trimmed);
|
|
303
|
+
|
|
304
|
+
const replaced = projectionWithRuns(4, ['run-4']);
|
|
305
|
+
await save(createWholeProjectionChange(replaced), 3);
|
|
306
|
+
plugins.assert.deepEqual(await load(), replaced);
|
|
307
|
+
|
|
308
|
+
const emptied = projectionSnapshot(5);
|
|
309
|
+
await save(createWholeProjectionChange(emptied), 4);
|
|
310
|
+
plugins.assert.deepEqual(await load(), emptied);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
contractTest('projection changes are checked in form and against the stored lists', async ({ stores }) => {
|
|
314
|
+
const save = (change: IFlexProjectionChange, expectedRevision: number) =>
|
|
315
|
+
stores.projections.save('scope', 'session-1', change, expectedRevision);
|
|
316
|
+
await save(createWholeProjectionChange(projectionWithRuns(1, ['run-1'])), 0);
|
|
317
|
+
const next = projectionWithRuns(2, ['run-1', 'run-2']);
|
|
318
|
+
const whole = createWholeProjectionChange(next);
|
|
319
|
+
const malformed: IFlexProjectionChange[] = [
|
|
320
|
+
{ ...whole, messages: { start: 1, end: 0, items: next.messages } },
|
|
321
|
+
{ ...whole, messages: { start: -1, end: 0, items: next.messages } },
|
|
322
|
+
{ ...whole, reversionSegments: { start: 0, end: 0, items: 'segments' as never } },
|
|
323
|
+
{ ...whole, schemaVersion: 2 as never },
|
|
324
|
+
];
|
|
325
|
+
for (const change of malformed) {
|
|
326
|
+
await assertRejects(() => save(change, 1), FlexHarnessStoreFormatError);
|
|
327
|
+
}
|
|
328
|
+
plugins.assert.equal((await stores.projections.load('scope', 'session-1'))?.revision, 1);
|
|
329
|
+
|
|
330
|
+
const rejectedBy = await assertChangeRejected(
|
|
331
|
+
() => save({
|
|
332
|
+
...next,
|
|
333
|
+
messages: { start: 0, end: 4, items: [] },
|
|
334
|
+
reversionSegments: { start: 0, end: 2, items: [] },
|
|
335
|
+
}, 1),
|
|
336
|
+
() => stores.projections.load('scope', 'session-1'),
|
|
337
|
+
);
|
|
338
|
+
if (rejectedBy === 'save') {
|
|
339
|
+
plugins.assert.equal((await stores.projections.load('scope', 'session-1'))?.revision, 1);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
contractTest('projection validation rejects public attachment payload fields', async ({ stores }) => {
|
|
344
|
+
const invalid: IFlexProjectionSnapshotCurrent = {
|
|
345
|
+
...projectionSnapshot(1),
|
|
346
|
+
messages: [
|
|
347
|
+
{
|
|
348
|
+
messageId: 'message',
|
|
349
|
+
sessionId: 'session-1',
|
|
350
|
+
runId: 'run',
|
|
351
|
+
role: 'user',
|
|
352
|
+
status: 'completed',
|
|
353
|
+
createdAt: '2026-08-05T00:00:00.000Z',
|
|
354
|
+
parts: [
|
|
355
|
+
{
|
|
356
|
+
partId: 'attachment',
|
|
357
|
+
type: 'attachment',
|
|
358
|
+
attachmentType: 'file',
|
|
359
|
+
source: 'inline-base64',
|
|
360
|
+
data: 'c2VjcmV0',
|
|
361
|
+
} as never,
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
};
|
|
366
|
+
await assertRejects(
|
|
367
|
+
() => stores.projections.save('scope', 'session-1', createWholeProjectionChange(invalid), 0),
|
|
368
|
+
FlexHarnessStoreFormatError,
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
contractTest('projection validation rejects malformed reversion ownership and correlation', async ({ stores }) => {
|
|
373
|
+
const base = projectionWithRuns(1, ['run']);
|
|
374
|
+
const segment = base.reversionSegments[0];
|
|
375
|
+
const invalidSnapshots: IFlexProjectionSnapshotCurrent[] = [
|
|
376
|
+
{
|
|
377
|
+
...structuredClone(base),
|
|
378
|
+
reversionSegments: [{ ...segment, workspaceCaptured: true, captureId: 'capture' }],
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
...structuredClone(base),
|
|
382
|
+
reversionSegments: [{
|
|
383
|
+
...segment,
|
|
384
|
+
status: 'capturing',
|
|
385
|
+
workspaceCaptured: true,
|
|
386
|
+
captureId: 'capture',
|
|
387
|
+
}],
|
|
388
|
+
revertCursor: 0,
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
...structuredClone(base),
|
|
392
|
+
messages: [...runMessages('failed'), ...runMessages('run')],
|
|
393
|
+
reversionSegments: [
|
|
394
|
+
{ ...runSegment('failed'), status: 'failed', eventIds: ['event-shared'] },
|
|
395
|
+
{ ...segment, eventIds: ['event-shared'] },
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
...structuredClone(base),
|
|
400
|
+
messages: [...runMessages('failed'), ...runMessages('run')],
|
|
401
|
+
reversionSegments: [{ ...runSegment('failed'), status: 'failed' }, segment],
|
|
402
|
+
pendingReversion: {
|
|
403
|
+
kind: 'apply',
|
|
404
|
+
operationId: 'apply',
|
|
405
|
+
direction: 'undo',
|
|
406
|
+
fromCursor: 1,
|
|
407
|
+
toCursor: 0,
|
|
408
|
+
segmentRunIds: ['failed', 'run'],
|
|
409
|
+
appliedRunIds: ['failed'],
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
];
|
|
413
|
+
for (let index = 0; index < invalidSnapshots.length; index++) {
|
|
414
|
+
await assertRejects(() => stores.projections.save(
|
|
415
|
+
`invalid-reversion-${index}`,
|
|
416
|
+
'session-1',
|
|
417
|
+
createWholeProjectionChange(invalidSnapshots[index]),
|
|
418
|
+
0,
|
|
419
|
+
), FlexHarnessStoreFormatError);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
contractTest('projection schema 3 validates protocol-2 metadata, dispositions, and release ownership', async ({ stores }) => {
|
|
424
|
+
const valid: IFlexProjectionSnapshotCurrent = {
|
|
425
|
+
...projectionWithRuns(1, ['run']),
|
|
426
|
+
reversionSegments: [{
|
|
427
|
+
...runSegment('run'),
|
|
428
|
+
workspaceCaptured: true,
|
|
429
|
+
protocolVersion: 2,
|
|
430
|
+
provenance: 'workspace',
|
|
431
|
+
disposition: 'no-change',
|
|
432
|
+
affectedWorkspaces: [],
|
|
433
|
+
}],
|
|
434
|
+
};
|
|
435
|
+
await stores.projections.save('valid-schema-3', 'session-1', createWholeProjectionChange(valid), 0);
|
|
436
|
+
plugins.assert.deepEqual(await stores.projections.load('valid-schema-3', 'session-1'), valid);
|
|
437
|
+
|
|
438
|
+
const segment = valid.reversionSegments[0];
|
|
439
|
+
const invalid: IFlexProjectionSnapshotCurrent[] = [
|
|
440
|
+
{ ...structuredClone(valid), reversionSegments: [{ ...segment, disposition: 'pending' }] },
|
|
441
|
+
{ ...structuredClone(valid), reversionSegments: [{ ...segment, disposition: 'nonrevertible' }] },
|
|
442
|
+
{
|
|
443
|
+
...structuredClone(valid),
|
|
444
|
+
reversionSegments: [{
|
|
445
|
+
...segment,
|
|
446
|
+
affectedWorkspaces: [{ id: 'workspace', label: 'Workspace' }],
|
|
447
|
+
}],
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
...structuredClone(valid),
|
|
451
|
+
reversionSegments: [{
|
|
452
|
+
...segment,
|
|
453
|
+
disposition: 'revertible',
|
|
454
|
+
captureId: 'capture',
|
|
455
|
+
workspaceReference: { capture: true },
|
|
456
|
+
affectedWorkspaces: [{ id: 'workspace', label: 'Workspace' }],
|
|
457
|
+
}],
|
|
458
|
+
pendingReversionReleases: [{
|
|
459
|
+
runId: 'run',
|
|
460
|
+
captureId: 'capture',
|
|
461
|
+
reference: { capture: true },
|
|
462
|
+
protocolVersion: 2,
|
|
463
|
+
}],
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
...structuredClone(valid),
|
|
467
|
+
reversionSegments: [{
|
|
468
|
+
...segment,
|
|
469
|
+
disposition: 'revertible',
|
|
470
|
+
captureId: 'capture-whitespace-id',
|
|
471
|
+
workspaceReference: { capture: true },
|
|
472
|
+
affectedWorkspaces: [{ id: ' ', label: 'Workspace' }],
|
|
473
|
+
}],
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
...structuredClone(valid),
|
|
477
|
+
reversionSegments: [{
|
|
478
|
+
...segment,
|
|
479
|
+
disposition: 'revertible',
|
|
480
|
+
captureId: 'capture-whitespace-label',
|
|
481
|
+
workspaceReference: { capture: true },
|
|
482
|
+
affectedWorkspaces: [{ id: 'workspace', label: '\t' }],
|
|
483
|
+
}],
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
...structuredClone(valid),
|
|
487
|
+
reversionSegments: [{ ...segment, disposition: 'nonrevertible', reasonCode: 'git conflict' }],
|
|
488
|
+
},
|
|
489
|
+
];
|
|
490
|
+
for (let index = 0; index < invalid.length; index++) {
|
|
491
|
+
await assertRejects(() => stores.projections.save(
|
|
492
|
+
`invalid-schema-3-${index}`,
|
|
493
|
+
'session-1',
|
|
494
|
+
createWholeProjectionChange(invalid[index]),
|
|
495
|
+
0,
|
|
496
|
+
), FlexHarnessStoreFormatError);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
contractTest('Agent event saves apply removals, replacements and appends', async ({ stores }) => {
|
|
501
|
+
const eventStore = await stores.agentEvents.getStore('scope', 'session');
|
|
502
|
+
plugins.assert.equal(await eventStore.save('session', appendEvents(agentEvent('first'), agentEvent('second')), 0), 1);
|
|
503
|
+
const replacement = agentEvent('second', 'Replaced prompt');
|
|
504
|
+
plugins.assert.equal(await eventStore.save('session', {
|
|
505
|
+
remove: ['first'],
|
|
506
|
+
replace: [replacement],
|
|
507
|
+
append: [agentEvent('third')],
|
|
508
|
+
}, 1), 2);
|
|
509
|
+
const snapshot = await eventStore.load('session');
|
|
510
|
+
plugins.assert.ok(snapshot);
|
|
511
|
+
plugins.assert.equal(snapshot.revision, 2);
|
|
512
|
+
plugins.assert.deepEqual(snapshot.events, [replacement, agentEvent('third')]);
|
|
513
|
+
|
|
514
|
+
const rejectedBy = await assertChangeRejected(
|
|
515
|
+
() => eventStore.save('session', { remove: ['first'], replace: [], append: [] }, 2),
|
|
516
|
+
() => eventStore.load('session'),
|
|
517
|
+
);
|
|
518
|
+
if (rejectedBy === 'save') plugins.assert.equal((await eventStore.load('session'))?.revision, 2);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
contractTest('Agent stores validate every change after strict JSON safety', async ({ stores }) => {
|
|
522
|
+
const eventStore = await stores.agentEvents.getStore('scope', 'session');
|
|
523
|
+
plugins.assert.ok(eventStore.archive, `${backend.name} Agent event stores must archive.`);
|
|
524
|
+
const canonicalEvent = agentEvent();
|
|
525
|
+
|
|
526
|
+
plugins.assert.equal(canonicalEvent.sequence, undefined);
|
|
527
|
+
plugins.assert.equal(await eventStore.save('session', appendEvents(canonicalEvent), 0), 1);
|
|
528
|
+
await eventStore.archive({
|
|
529
|
+
schemaVersion: 2,
|
|
530
|
+
sessionId: 'session',
|
|
531
|
+
archiveId: 'archive',
|
|
532
|
+
createdAt: 1,
|
|
533
|
+
events: [canonicalEvent],
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
const malformedEvent = { ...canonicalEvent, id: 'malformed', content: 42 } as unknown as plugins.TAgentEvent;
|
|
537
|
+
await assertRejectsWith(
|
|
538
|
+
() => eventStore.save('session', appendEvents(malformedEvent), 1),
|
|
539
|
+
FlexHarnessStoreFormatError,
|
|
540
|
+
'append[0].content',
|
|
541
|
+
);
|
|
542
|
+
await assertRejectsWith(() => eventStore.archive!({
|
|
543
|
+
schemaVersion: 2,
|
|
544
|
+
sessionId: 'session',
|
|
545
|
+
archiveId: 'malformed',
|
|
546
|
+
createdAt: 1,
|
|
547
|
+
events: [malformedEvent],
|
|
548
|
+
}), FlexHarnessStoreFormatError, 'Agent event archive events[0].content');
|
|
549
|
+
|
|
550
|
+
const unsafeEvent = { ...canonicalEvent, id: 'unsafe', content: new Date() } as unknown as plugins.TAgentEvent;
|
|
551
|
+
await assertRejectsWith(
|
|
552
|
+
() => eventStore.save('session', appendEvents(unsafeEvent), 1),
|
|
553
|
+
FlexHarnessStoreFormatError,
|
|
554
|
+
'$change.append[0].content',
|
|
555
|
+
);
|
|
556
|
+
plugins.assert.equal((await eventStore.load('session'))?.revision, 1);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
contractTest('Agent event stores are session-bound, CAS-safe, and archive once', async ({ stores }) => {
|
|
560
|
+
const eventStore = await stores.agentEvents.getStore('scope', 'session');
|
|
561
|
+
plugins.assert.ok(eventStore.archive && eventStore.loadArchive, `${backend.name} Agent event stores must archive.`);
|
|
562
|
+
await assertRejects(() => eventStore.load('other-session'), FlexHarnessValidationError);
|
|
563
|
+
plugins.assert.equal(await eventStore.save('session', appendEvents(agentEvent('first')), 0), 1);
|
|
564
|
+
await assertRejects(
|
|
565
|
+
() => eventStore.save('session', appendEvents(agentEvent('second')), 0),
|
|
566
|
+
(error: unknown) => {
|
|
567
|
+
plugins.assert.ok(error instanceof plugins.AgentEventStoreConflictError);
|
|
568
|
+
plugins.assert.equal(error.actualRevision, 1);
|
|
569
|
+
return true;
|
|
570
|
+
},
|
|
571
|
+
);
|
|
572
|
+
plugins.assert.equal(await eventStore.save('session', appendEvents(agentEvent('second')), 1), 2);
|
|
573
|
+
const snapshot = await eventStore.load('session');
|
|
574
|
+
plugins.assert.ok(snapshot);
|
|
575
|
+
plugins.assert.equal(snapshot.schemaVersion, 2);
|
|
576
|
+
plugins.assert.equal(snapshot.revision, 2);
|
|
577
|
+
plugins.assert.deepEqual(snapshot.events.map((event) => event.id), ['first', 'second']);
|
|
578
|
+
|
|
579
|
+
const archive: plugins.IAgentEventArchive = {
|
|
580
|
+
schemaVersion: 2,
|
|
581
|
+
sessionId: 'session',
|
|
582
|
+
archiveId: 'archive',
|
|
583
|
+
createdAt: 1,
|
|
584
|
+
events: [agentEvent('first')],
|
|
585
|
+
};
|
|
586
|
+
plugins.assert.equal(await eventStore.loadArchive('session', 'archive'), undefined);
|
|
587
|
+
await eventStore.archive(archive);
|
|
588
|
+
await eventStore.archive(structuredClone(archive));
|
|
589
|
+
plugins.assert.deepEqual(await eventStore.loadArchive('session', 'archive'), archive);
|
|
590
|
+
await assertRejects(() => eventStore.archive!({ ...archive, events: [agentEvent('second')] }));
|
|
591
|
+
plugins.assert.equal((await eventStore.loadArchive('session', 'archive'))?.events[0].id, 'first');
|
|
592
|
+
|
|
593
|
+
await stores.agentEvents.deleteSession('scope', 'session');
|
|
594
|
+
const reopened = await stores.agentEvents.getStore('scope', 'session');
|
|
595
|
+
plugins.assert.equal(await reopened.load('session'), undefined);
|
|
596
|
+
plugins.assert.equal(await reopened.loadArchive?.('session', 'archive'), undefined);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
contractTest('tool job stores are CAS-safe and honour abort signals', async ({ stores }) => {
|
|
600
|
+
const jobStore = await stores.jobs.getStore('scope', 'session');
|
|
601
|
+
plugins.assert.equal(await jobStore.save([finishedJob()], 0), 1);
|
|
602
|
+
await assertRejects(() => jobStore.save([], 0), (error: unknown) => {
|
|
603
|
+
plugins.assert.ok(error instanceof plugins.ToolJobStoreConflictError);
|
|
604
|
+
plugins.assert.equal(error.actualRevision, 1);
|
|
605
|
+
return true;
|
|
606
|
+
});
|
|
607
|
+
const snapshot = await jobStore.load();
|
|
608
|
+
plugins.assert.ok(snapshot);
|
|
609
|
+
plugins.assert.equal(snapshot.revision, 1);
|
|
610
|
+
plugins.assert.deepEqual(snapshot.jobs, [finishedJob()]);
|
|
611
|
+
|
|
612
|
+
const aborted = AbortSignal.abort(new Error('caller aborted'));
|
|
613
|
+
await assertRejects(() => jobStore.load(aborted), { message: 'caller aborted' });
|
|
614
|
+
await assertRejects(() => jobStore.save([], 1, aborted), { message: 'caller aborted' });
|
|
615
|
+
plugins.assert.equal((await jobStore.load())?.revision, 1);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
contractTest('project stores fence session generations and tombstones', async ({ stores }) => {
|
|
619
|
+
const store = stores.projectManagement;
|
|
620
|
+
const application = { actor: 'application' as const };
|
|
621
|
+
await store.save('namespace', 'session', liveProjectState('generation-a'), 0, application);
|
|
622
|
+
await assertRejects(
|
|
623
|
+
() => store.save('namespace', 'session', liveProjectState('generation-a'), 0, application),
|
|
624
|
+
FlexHarnessStoreConflictError,
|
|
625
|
+
);
|
|
626
|
+
await store.save('namespace', 'session', liveProjectState('generation-a', 2), 1, {
|
|
627
|
+
actor: 'agent',
|
|
628
|
+
runId: 'run',
|
|
629
|
+
toolCallId: 'tool-call',
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
await store.tombstoneSession('namespace', 'session', projectTombstone('generation-a', 3), 2);
|
|
633
|
+
await store.tombstoneSession('namespace', 'session', projectTombstone('generation-a', 3), 2);
|
|
634
|
+
plugins.assert.deepEqual(await store.load('namespace', 'session'), projectTombstone('generation-a', 3));
|
|
635
|
+
await assertRejects(
|
|
636
|
+
() => store.save('namespace', 'session', liveProjectState('generation-a', 4), 3, application),
|
|
637
|
+
FlexHarnessStoreConflictError,
|
|
638
|
+
);
|
|
639
|
+
|
|
640
|
+
const replacement = liveProjectState('generation-b', 1, 2);
|
|
641
|
+
await store.save('namespace', 'session', replacement, 0, application);
|
|
642
|
+
plugins.assert.deepEqual(await store.load('namespace', 'session'), replacement);
|
|
643
|
+
await assertRejects(
|
|
644
|
+
() => store.save('namespace', 'session', liveProjectState('generation-c'), 0, application),
|
|
645
|
+
FlexHarnessStoreConflictError,
|
|
646
|
+
);
|
|
647
|
+
await assertRejects(
|
|
648
|
+
() => store.tombstoneSession('namespace', 'session', projectTombstone('generation-a'), 0),
|
|
649
|
+
FlexHarnessStoreConflictError,
|
|
650
|
+
);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
contractTest('project stores purge one exact namespace without affecting another', async ({ stores }) => {
|
|
654
|
+
const store = stores.projectManagement;
|
|
655
|
+
const application = { actor: 'application' as const };
|
|
656
|
+
await store.save('namespace-a', 'same-id', liveProjectState('a-live'), 0, application);
|
|
657
|
+
await store.tombstoneSession('namespace-a', 'tombstoned', projectTombstone('a-tombstone'), 0);
|
|
658
|
+
await store.save('namespace-b', 'same-id', liveProjectState('b-live'), 0, application);
|
|
659
|
+
await store.tombstoneSession('namespace-b', 'tombstoned', projectTombstone('b-tombstone'), 0);
|
|
660
|
+
await store.tombstoneSession(
|
|
661
|
+
'namespace-b',
|
|
662
|
+
'generation-fence',
|
|
663
|
+
projectTombstone('old-generation'),
|
|
664
|
+
0,
|
|
665
|
+
);
|
|
666
|
+
await store.save(
|
|
667
|
+
'namespace-b',
|
|
668
|
+
'generation-fence',
|
|
669
|
+
liveProjectState('new-generation', 1, 2),
|
|
670
|
+
0,
|
|
671
|
+
application,
|
|
672
|
+
);
|
|
673
|
+
await assertRejects(() => store.tombstoneSession(
|
|
674
|
+
'namespace-b',
|
|
675
|
+
'generation-fence',
|
|
676
|
+
projectTombstone('old-generation'),
|
|
677
|
+
0,
|
|
678
|
+
), FlexHarnessStoreConflictError);
|
|
679
|
+
|
|
680
|
+
await store.purgeNamespace('namespace-a');
|
|
681
|
+
plugins.assert.equal(await store.load('namespace-a', 'same-id'), undefined);
|
|
682
|
+
plugins.assert.equal(await store.load('namespace-a', 'tombstoned'), undefined);
|
|
683
|
+
plugins.assert.notEqual(await store.load('namespace-b', 'same-id'), undefined);
|
|
684
|
+
plugins.assert.notEqual(await store.load('namespace-b', 'tombstoned'), undefined);
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
if (backend.storesResults) contractTest('result stores write each handle once, clone values, list and delete them', async ({ stores }) => {
|
|
688
|
+
const results = stores.results;
|
|
689
|
+
plugins.assert.ok(results, `${backend.name} declares storesResults without a result store.`);
|
|
690
|
+
const large = { items: Array.from({ length: 2000 }, (_, index) => ({ index, text: 'x'.repeat(600) })) };
|
|
691
|
+
plugins.assert.equal(await results.load('scope', 'session-1', 'res_1'), undefined);
|
|
692
|
+
await results.save('scope', 'session-1', 'res_1', large);
|
|
693
|
+
await results.save('scope', 'session-2', 'res_1', 'other session');
|
|
694
|
+
const loaded = (await results.load('scope', 'session-1', 'res_1')) as typeof large;
|
|
695
|
+
plugins.assert.deepEqual(loaded, large);
|
|
696
|
+
loaded.items[0].text = 'changed';
|
|
697
|
+
plugins.assert.equal(
|
|
698
|
+
((await results.load('scope', 'session-1', 'res_1')) as typeof large).items[0].text,
|
|
699
|
+
'x'.repeat(600),
|
|
700
|
+
);
|
|
701
|
+
await assertRejects(() => results.save('scope', 'session-1', 'res_1', 'again'), FlexHarnessStoreConflictError);
|
|
702
|
+
await results.save('scope', 'session-1', 'res_2', 2);
|
|
703
|
+
await results.save('scope', 'session-1', 'res_3', 3);
|
|
704
|
+
plugins.assert.deepEqual(await results.list('scope', 'session-1'), ['res_1', 'res_2', 'res_3']);
|
|
705
|
+
await results.delete('scope', 'session-1', 'res_2');
|
|
706
|
+
plugins.assert.deepEqual(await results.list('scope', 'session-1'), ['res_1', 'res_3']);
|
|
707
|
+
plugins.assert.equal(await results.load('scope', 'session-1', 'res_2'), undefined);
|
|
708
|
+
plugins.assert.equal(await results.load('scope', 'session-1', 'res_3'), 3);
|
|
709
|
+
await results.deleteSession('scope', 'session-1');
|
|
710
|
+
plugins.assert.equal(await results.load('scope', 'session-1', 'res_1'), undefined);
|
|
711
|
+
plugins.assert.deepEqual(await results.list('scope', 'session-1'), []);
|
|
712
|
+
plugins.assert.equal(await results.load('scope', 'session-2', 'res_1'), 'other session');
|
|
713
|
+
plugins.assert.deepEqual(await results.list('scope', 'session-2'), ['res_1']);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
if (backend.releasesSessions) contractTest('provider release evicts wrappers without deleting persisted session data', async ({ stores }) => {
|
|
717
|
+
const eventStore = await stores.agentEvents.getStore('release', 'session');
|
|
718
|
+
const jobStore = await stores.jobs.getStore('release', 'session');
|
|
719
|
+
await eventStore.save('session', appendEvents(agentEvent()), 0);
|
|
720
|
+
await jobStore.save([], 0);
|
|
721
|
+
plugins.assert.ok(
|
|
722
|
+
stores.agentEvents.releaseSession && stores.jobs.releaseSession,
|
|
723
|
+
`${backend.name} declares releasesSessions without releaseSession hooks.`,
|
|
724
|
+
);
|
|
725
|
+
|
|
726
|
+
await stores.agentEvents.releaseSession('release', 'session');
|
|
727
|
+
await stores.jobs.releaseSession('release', 'session');
|
|
728
|
+
const reloadedEventStore = await stores.agentEvents.getStore('release', 'session');
|
|
729
|
+
const reloadedJobStore = await stores.jobs.getStore('release', 'session');
|
|
730
|
+
plugins.assert.notEqual(reloadedEventStore, eventStore);
|
|
731
|
+
plugins.assert.notEqual(reloadedJobStore, jobStore);
|
|
732
|
+
plugins.assert.equal((await reloadedEventStore.load('session'))?.revision, 1);
|
|
733
|
+
plugins.assert.equal((await reloadedJobStore.load())?.revision, 1);
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
if (backend.sharedBackingStore) contractTest('bundles over one backing store share CAS revisions', async (fixture) => {
|
|
737
|
+
plugins.assert.ok(fixture.openPeer, `${backend.name} declares sharedBackingStore without openPeer.`);
|
|
738
|
+
const first = fixture.stores;
|
|
739
|
+
const second = await fixture.openPeer();
|
|
740
|
+
assertOneWinner(await Promise.allSettled([
|
|
741
|
+
first.scopes.save('shared', scopeSnapshot(1, 'first'), 0),
|
|
742
|
+
second.scopes.save('shared', scopeSnapshot(1, 'second'), 0),
|
|
743
|
+
]), FlexHarnessStoreConflictError);
|
|
744
|
+
plugins.assert.deepEqual(await first.scopes.load('shared'), await second.scopes.load('shared'));
|
|
745
|
+
|
|
746
|
+
const firstEvents = await first.agentEvents.getStore('shared', 'session');
|
|
747
|
+
const secondEvents = await second.agentEvents.getStore('shared', 'session');
|
|
748
|
+
await firstEvents.save('session', appendEvents(agentEvent('from-first')), 0);
|
|
749
|
+
plugins.assert.equal((await secondEvents.load('session'))?.events[0].id, 'from-first');
|
|
750
|
+
await assertRejects(
|
|
751
|
+
() => secondEvents.save('session', appendEvents(agentEvent('stale')), 0),
|
|
752
|
+
plugins.AgentEventStoreConflictError,
|
|
753
|
+
);
|
|
754
|
+
await secondEvents.save('session', appendEvents(agentEvent('from-second')), 1);
|
|
755
|
+
plugins.assert.deepEqual(
|
|
756
|
+
(await firstEvents.load('session'))?.events.map((event) => event.id),
|
|
757
|
+
['from-first', 'from-second'],
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
const projection = projectionWithRuns(1, ['run-1']);
|
|
761
|
+
await first.projections.save('shared', 'session-1', createWholeProjectionChange(projection), 0);
|
|
762
|
+
const appended = projectionWithRuns(2, ['run-1', 'run-2']);
|
|
763
|
+
await second.projections.save('shared', 'session-1', {
|
|
764
|
+
...appended,
|
|
765
|
+
messages: { start: 0, end: 2, items: runMessages('run-2') },
|
|
766
|
+
reversionSegments: { start: 0, end: 1, items: [runSegment('run-2')] },
|
|
767
|
+
}, 1);
|
|
768
|
+
plugins.assert.deepEqual(await first.projections.load('shared', 'session-1'), appended);
|
|
769
|
+
|
|
770
|
+
await first.permissions.save('shared', 'session', permissionSnapshot(1), 0);
|
|
771
|
+
await second.permissions.deleteSession('shared', 'session');
|
|
772
|
+
plugins.assert.equal(await first.permissions.load('shared', 'session'), undefined);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
return tests;
|
|
776
|
+
}
|