@modelprofile.com/flexharness 12.1.0 → 12.1.2
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_stores_nosqldb/classes.nosqldbstores.d.ts +4 -3
- package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +5 -4
- package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +22 -12
- package/dist_ts_stores_nosqldb/classes.recordstore.js +198 -125
- package/dist_ts_stores_testing/contract.d.ts +5 -0
- package/dist_ts_stores_testing/contract.js +42 -23
- package/dist_ts_stores_testing/fixtures.d.ts +2 -2
- package/dist_ts_stores_testing/fixtures.js +4 -4
- package/package.json +3 -3
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts_stores_nosqldb/classes.nosqldbstores.ts +4 -3
- package/ts_stores_nosqldb/classes.recordstore.ts +221 -138
- package/ts_stores_nosqldb/readme.md +11 -5
- package/ts_stores_testing/contract.ts +54 -20
- package/ts_stores_testing/fixtures.ts +3 -3
- package/ts_stores_testing/readme.md +1 -0
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from '../ts/errors.js';
|
|
7
7
|
import type {
|
|
8
8
|
IFlexHarnessStores,
|
|
9
|
+
IFlexProjectManagementSessionContext,
|
|
9
10
|
IFlexProjectManagementSnapshot,
|
|
10
11
|
IFlexProjectManagementTombstone,
|
|
11
12
|
IFlexProjectionChange,
|
|
@@ -46,6 +47,11 @@ export interface IFlexStoreContractBackend {
|
|
|
46
47
|
releasesSessions: boolean;
|
|
47
48
|
/** The bundle provides `results`. */
|
|
48
49
|
storesResults: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* The scope store keeps a session whose `scopeId` differs from the storage key it is saved under,
|
|
52
|
+
* as a resolver that maps several scope IDs to one storage key saves it. Defaults to `true`.
|
|
53
|
+
*/
|
|
54
|
+
storesScopeAliases?: boolean;
|
|
49
55
|
open(): Promise<IFlexStoreContractFixture>;
|
|
50
56
|
}
|
|
51
57
|
|
|
@@ -86,6 +92,17 @@ function projectTombstone(
|
|
|
86
92
|
};
|
|
87
93
|
}
|
|
88
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The session context FlexHarness passes with every project-management `load` and
|
|
97
|
+
* `tombstoneSession` of the session generation `generation`.
|
|
98
|
+
*/
|
|
99
|
+
function projectSessionContext(
|
|
100
|
+
generation: string,
|
|
101
|
+
sequence = 1,
|
|
102
|
+
): Readonly<IFlexProjectManagementSessionContext> {
|
|
103
|
+
return Object.freeze({ sessionGenerationId: generation, sessionGenerationSequence: sequence });
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
/** Exactly one of `attempts` succeeded and another was rejected with `errorClass`. */
|
|
90
107
|
function assertOneWinner(attempts: PromiseSettledResult<unknown>[], errorClass: TErrorClass): void {
|
|
91
108
|
plugins.assert.equal(attempts.filter((result) => result.status === 'fulfilled').length, 1);
|
|
@@ -164,7 +181,7 @@ export function createFlexStoreContractTests(
|
|
|
164
181
|
|
|
165
182
|
contractTest('scope store clones snapshots and enforces CAS', async ({ stores }) => {
|
|
166
183
|
plugins.assert.equal(await stores.scopes.load('scope'), undefined);
|
|
167
|
-
const first = scopeSnapshot(1);
|
|
184
|
+
const first = scopeSnapshot(1, 'session-1', 'scope');
|
|
168
185
|
await stores.scopes.save('scope', first, 0);
|
|
169
186
|
first.sessions[0].status = 'failed';
|
|
170
187
|
const loaded = await stores.scopes.load('scope');
|
|
@@ -174,15 +191,15 @@ export function createFlexStoreContractTests(
|
|
|
174
191
|
plugins.assert.equal((await stores.scopes.load('scope'))?.sessions[0].status, 'idle');
|
|
175
192
|
|
|
176
193
|
assertOneWinner(await Promise.allSettled([
|
|
177
|
-
stores.scopes.save('scope', scopeSnapshot(2, 'session-a'), 1),
|
|
178
|
-
stores.scopes.save('scope', scopeSnapshot(2, 'session-b'), 1),
|
|
194
|
+
stores.scopes.save('scope', scopeSnapshot(2, 'session-a', 'scope'), 1),
|
|
195
|
+
stores.scopes.save('scope', scopeSnapshot(2, 'session-b', 'scope'), 1),
|
|
179
196
|
]), FlexHarnessStoreConflictError);
|
|
180
197
|
plugins.assert.equal((await stores.scopes.load('scope'))?.revision, 2);
|
|
181
198
|
});
|
|
182
199
|
|
|
183
200
|
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);
|
|
201
|
+
await assertRejects(() => stores.scopes.save('scope', scopeSnapshot(2, 'session-1', 'scope'), 0), FlexHarnessStoreFormatError);
|
|
202
|
+
await assertRejects(() => stores.scopes.save('scope', scopeSnapshot(0, 'session-1', 'scope'), -1), FlexHarnessValidationError);
|
|
186
203
|
plugins.assert.equal(await stores.scopes.load('scope'), undefined);
|
|
187
204
|
|
|
188
205
|
const saveProjection = (revision: number, expectedRevision: number) => stores.projections.save(
|
|
@@ -228,7 +245,7 @@ export function createFlexStoreContractTests(
|
|
|
228
245
|
});
|
|
229
246
|
|
|
230
247
|
contractTest('split session domains stay independent', async ({ stores }) => {
|
|
231
|
-
await stores.scopes.save('scope', scopeSnapshot(1), 0);
|
|
248
|
+
await stores.scopes.save('scope', scopeSnapshot(1, 'session-1', 'scope'), 0);
|
|
232
249
|
await stores.projections.save('scope', 'session-1', createWholeProjectionChange(projectionSnapshot(1)), 0);
|
|
233
250
|
await stores.permissions.save('scope', 'session-1', permissionSnapshot(1), 0);
|
|
234
251
|
const eventStore = await stores.agentEvents.getStore('scope', 'session-1');
|
|
@@ -271,7 +288,7 @@ export function createFlexStoreContractTests(
|
|
|
271
288
|
});
|
|
272
289
|
|
|
273
290
|
contractTest('stores reject non-serializable snapshots at their boundary', async ({ stores }) => {
|
|
274
|
-
const invalid = scopeSnapshot(1) as unknown as Record<string, unknown>;
|
|
291
|
+
const invalid = scopeSnapshot(1, 'session-1', 'scope') as unknown as Record<string, unknown>;
|
|
275
292
|
invalid.nonJson = new Date();
|
|
276
293
|
await assertRejects(
|
|
277
294
|
() => stores.scopes.save('scope', invalid as unknown as IFlexScopeSnapshot, 0),
|
|
@@ -618,6 +635,8 @@ export function createFlexStoreContractTests(
|
|
|
618
635
|
contractTest('project stores fence session generations and tombstones', async ({ stores }) => {
|
|
619
636
|
const store = stores.projectManagement;
|
|
620
637
|
const application = { actor: 'application' as const };
|
|
638
|
+
const generationA = projectSessionContext('generation-a');
|
|
639
|
+
const generationB = projectSessionContext('generation-b', 2);
|
|
621
640
|
await store.save('namespace', 'session', liveProjectState('generation-a'), 0, application);
|
|
622
641
|
await assertRejects(
|
|
623
642
|
() => store.save('namespace', 'session', liveProjectState('generation-a'), 0, application),
|
|
@@ -629,9 +648,12 @@ export function createFlexStoreContractTests(
|
|
|
629
648
|
toolCallId: 'tool-call',
|
|
630
649
|
});
|
|
631
650
|
|
|
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(
|
|
651
|
+
await store.tombstoneSession('namespace', 'session', projectTombstone('generation-a', 3), 2, generationA);
|
|
652
|
+
await store.tombstoneSession('namespace', 'session', projectTombstone('generation-a', 3), 2, generationA);
|
|
653
|
+
plugins.assert.deepEqual(
|
|
654
|
+
await store.load('namespace', 'session', generationA),
|
|
655
|
+
projectTombstone('generation-a', 3),
|
|
656
|
+
);
|
|
635
657
|
await assertRejects(
|
|
636
658
|
() => store.save('namespace', 'session', liveProjectState('generation-a', 4), 3, application),
|
|
637
659
|
FlexHarnessStoreConflictError,
|
|
@@ -639,13 +661,13 @@ export function createFlexStoreContractTests(
|
|
|
639
661
|
|
|
640
662
|
const replacement = liveProjectState('generation-b', 1, 2);
|
|
641
663
|
await store.save('namespace', 'session', replacement, 0, application);
|
|
642
|
-
plugins.assert.deepEqual(await store.load('namespace', 'session'), replacement);
|
|
664
|
+
plugins.assert.deepEqual(await store.load('namespace', 'session', generationB), replacement);
|
|
643
665
|
await assertRejects(
|
|
644
666
|
() => store.save('namespace', 'session', liveProjectState('generation-c'), 0, application),
|
|
645
667
|
FlexHarnessStoreConflictError,
|
|
646
668
|
);
|
|
647
669
|
await assertRejects(
|
|
648
|
-
() => store.tombstoneSession('namespace', 'session', projectTombstone('generation-a'), 0),
|
|
670
|
+
() => store.tombstoneSession('namespace', 'session', projectTombstone('generation-a'), 0, generationA),
|
|
649
671
|
FlexHarnessStoreConflictError,
|
|
650
672
|
);
|
|
651
673
|
});
|
|
@@ -653,15 +675,21 @@ export function createFlexStoreContractTests(
|
|
|
653
675
|
contractTest('project stores purge one exact namespace without affecting another', async ({ stores }) => {
|
|
654
676
|
const store = stores.projectManagement;
|
|
655
677
|
const application = { actor: 'application' as const };
|
|
678
|
+
const aLive = projectSessionContext('a-live');
|
|
679
|
+
const aTombstone = projectSessionContext('a-tombstone');
|
|
680
|
+
const bLive = projectSessionContext('b-live');
|
|
681
|
+
const bTombstone = projectSessionContext('b-tombstone');
|
|
682
|
+
const oldGeneration = projectSessionContext('old-generation');
|
|
656
683
|
await store.save('namespace-a', 'same-id', liveProjectState('a-live'), 0, application);
|
|
657
|
-
await store.tombstoneSession('namespace-a', 'tombstoned', projectTombstone('a-tombstone'), 0);
|
|
684
|
+
await store.tombstoneSession('namespace-a', 'tombstoned', projectTombstone('a-tombstone'), 0, aTombstone);
|
|
658
685
|
await store.save('namespace-b', 'same-id', liveProjectState('b-live'), 0, application);
|
|
659
|
-
await store.tombstoneSession('namespace-b', 'tombstoned', projectTombstone('b-tombstone'), 0);
|
|
686
|
+
await store.tombstoneSession('namespace-b', 'tombstoned', projectTombstone('b-tombstone'), 0, bTombstone);
|
|
660
687
|
await store.tombstoneSession(
|
|
661
688
|
'namespace-b',
|
|
662
689
|
'generation-fence',
|
|
663
690
|
projectTombstone('old-generation'),
|
|
664
691
|
0,
|
|
692
|
+
oldGeneration,
|
|
665
693
|
);
|
|
666
694
|
await store.save(
|
|
667
695
|
'namespace-b',
|
|
@@ -675,13 +703,19 @@ export function createFlexStoreContractTests(
|
|
|
675
703
|
'generation-fence',
|
|
676
704
|
projectTombstone('old-generation'),
|
|
677
705
|
0,
|
|
706
|
+
oldGeneration,
|
|
678
707
|
), FlexHarnessStoreConflictError);
|
|
679
708
|
|
|
680
709
|
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);
|
|
710
|
+
plugins.assert.equal(await store.load('namespace-a', 'same-id', aLive), undefined);
|
|
711
|
+
plugins.assert.equal(await store.load('namespace-a', 'tombstoned', aTombstone), undefined);
|
|
712
|
+
plugins.assert.notEqual(await store.load('namespace-b', 'same-id', bLive), undefined);
|
|
713
|
+
plugins.assert.notEqual(await store.load('namespace-b', 'tombstoned', bTombstone), undefined);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
if (backend.storesScopeAliases ?? true) contractTest('scope store keeps the sessions of scope aliases sharing its storage key', async ({ stores }) => {
|
|
717
|
+
await stores.scopes.save('scope', scopeSnapshot(1, 'session-1', 'alias'), 0);
|
|
718
|
+
plugins.assert.equal((await stores.scopes.load('scope'))?.sessions[0].scopeId, 'alias');
|
|
685
719
|
});
|
|
686
720
|
|
|
687
721
|
if (backend.storesResults) contractTest('result stores write each handle once, clone values, list and delete them', async ({ stores }) => {
|
|
@@ -738,8 +772,8 @@ export function createFlexStoreContractTests(
|
|
|
738
772
|
const first = fixture.stores;
|
|
739
773
|
const second = await fixture.openPeer();
|
|
740
774
|
assertOneWinner(await Promise.allSettled([
|
|
741
|
-
first.scopes.save('shared', scopeSnapshot(1, 'first'), 0),
|
|
742
|
-
second.scopes.save('shared', scopeSnapshot(1, 'second'), 0),
|
|
775
|
+
first.scopes.save('shared', scopeSnapshot(1, 'first', 'shared'), 0),
|
|
776
|
+
second.scopes.save('shared', scopeSnapshot(1, 'second', 'shared'), 0),
|
|
743
777
|
]), FlexHarnessStoreConflictError);
|
|
744
778
|
plugins.assert.deepEqual(await first.scopes.load('shared'), await second.scopes.load('shared'));
|
|
745
779
|
|
|
@@ -7,14 +7,14 @@ import type {
|
|
|
7
7
|
IFlexScopeSnapshot,
|
|
8
8
|
} from '../ts/interfaces.js';
|
|
9
9
|
|
|
10
|
-
/** A scope snapshot with one idle session
|
|
11
|
-
export function scopeSnapshot(revision: number, sessionId = 'session-1'): IFlexScopeSnapshot {
|
|
10
|
+
/** A scope snapshot with one idle session of `scopeId`. */
|
|
11
|
+
export function scopeSnapshot(revision: number, sessionId = 'session-1', scopeId = 'scope-1'): IFlexScopeSnapshot {
|
|
12
12
|
return {
|
|
13
13
|
schemaVersion: 1,
|
|
14
14
|
revision,
|
|
15
15
|
sessions: [
|
|
16
16
|
{
|
|
17
|
-
scopeId
|
|
17
|
+
scopeId,
|
|
18
18
|
sessionId,
|
|
19
19
|
createdAt: '2026-08-05T00:00:00.000Z',
|
|
20
20
|
updatedAt: '2026-08-05T00:00:00.000Z',
|
|
@@ -31,6 +31,7 @@ for (const contractTest of createFlexStoreContractTests({
|
|
|
31
31
|
- `sharedBackingStore`: several bundles can open one backing store, as separate processes would. The fixture's `openPeer()` opens another, and a case checks that the bundles share compare-and-swap revisions.
|
|
32
32
|
- `releasesSessions`: the Agent event and tool job providers implement `releaseSession`, which must evict their session wrappers without deleting stored data.
|
|
33
33
|
- `storesResults`: the bundle provides the optional `results` store.
|
|
34
|
+
- `storesScopeAliases` (optional, `true` when left out): the scope store keeps a session whose `scopeId` differs from the storage key it is saved under, as a scope resolver that maps several scope IDs to one storage key saves it. A store behind a resolver whose storage key always equals the scope ID may refuse such a session and declare `false`; every other case saves each session under its own scope ID.
|
|
34
35
|
|
|
35
36
|
The cases cover compare-and-swap revisions and the conflict errors that report them, cloning at the store boundary, session and storage key isolation, the snapshot validation every domain applies, and the change-based saves of projections and Agent events: a projection change keeps the stored entries its list changes name and appends the rest, an Agent event change removes, replaces and appends events, and a change that does not fit the stored value never loads, whether the store rejects it when it saves the change or when it next loads the value. The cases await every store call, as FlexHarness does, so a store method may reject or throw.
|
|
36
37
|
|