@ultimat3/testing 30.1.0 → 30.2.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/package.json +17 -17
- package/src/exercise.ts +1 -0
- package/src/scale-sweep-graph.ts +8 -2
- package/src/scale-sweep-run.ts +4 -1
- package/src/seed-graph-plan.ts +7 -0
- package/src/seed-graph-row.ts +3 -1
- package/src/seed-graph-shape.ts +20 -3
- package/src/seed-graph.ts +13 -4
- package/src/subject-proof.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/testing",
|
|
3
|
-
"version": "30.
|
|
3
|
+
"version": "30.2.0",
|
|
4
4
|
"description": "Test harness: cloned template DBs per worker, frozen clock, sealed network, 6 test types",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ultimate",
|
|
@@ -46,21 +46,21 @@
|
|
|
46
46
|
"test": "bun test"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@ultimat3/action": "30.
|
|
50
|
-
"@ultimat3/cache": "30.
|
|
51
|
-
"@ultimat3/core": "30.
|
|
52
|
-
"@ultimat3/db": "30.
|
|
53
|
-
"@ultimat3/entity": "30.
|
|
54
|
-
"@ultimat3/http": "30.
|
|
55
|
-
"@ultimat3/i18n": "30.
|
|
56
|
-
"@ultimat3/jobs": "30.
|
|
57
|
-
"@ultimat3/mail": "30.
|
|
58
|
-
"@ultimat3/policy": "30.
|
|
59
|
-
"@ultimat3/pwa": "30.
|
|
60
|
-
"@ultimat3/query": "30.
|
|
61
|
-
"@ultimat3/realtime": "30.
|
|
62
|
-
"@ultimat3/render": "30.
|
|
63
|
-
"@ultimat3/schema": "30.
|
|
64
|
-
"@ultimat3/time": "30.
|
|
49
|
+
"@ultimat3/action": "30.2.0",
|
|
50
|
+
"@ultimat3/cache": "30.2.0",
|
|
51
|
+
"@ultimat3/core": "30.2.0",
|
|
52
|
+
"@ultimat3/db": "30.2.0",
|
|
53
|
+
"@ultimat3/entity": "30.2.0",
|
|
54
|
+
"@ultimat3/http": "30.2.0",
|
|
55
|
+
"@ultimat3/i18n": "30.2.0",
|
|
56
|
+
"@ultimat3/jobs": "30.2.0",
|
|
57
|
+
"@ultimat3/mail": "30.2.0",
|
|
58
|
+
"@ultimat3/policy": "30.2.0",
|
|
59
|
+
"@ultimat3/pwa": "30.2.0",
|
|
60
|
+
"@ultimat3/query": "30.2.0",
|
|
61
|
+
"@ultimat3/realtime": "30.2.0",
|
|
62
|
+
"@ultimat3/render": "30.2.0",
|
|
63
|
+
"@ultimat3/schema": "30.2.0",
|
|
64
|
+
"@ultimat3/time": "30.2.0"
|
|
65
65
|
}
|
|
66
66
|
}
|
package/src/exercise.ts
CHANGED
|
@@ -441,6 +441,7 @@ export async function runExercisePass(
|
|
|
441
441
|
main: graph,
|
|
442
442
|
seed: passSeed,
|
|
443
443
|
room,
|
|
444
|
+
factories: options.overrides?.factories,
|
|
444
445
|
setup: async (subject, sized, tenant, self) => {
|
|
445
446
|
const full = chosen.find((one) => one.name === subject.name);
|
|
446
447
|
return full === undefined
|
package/src/scale-sweep-graph.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// every reference to one is pointed at the pass's own row in the same position. Pure.
|
|
4
4
|
|
|
5
5
|
import { seedGraph } from './seed-graph';
|
|
6
|
-
import type { SeedGraph } from './seed-graph-plan';
|
|
6
|
+
import type { SeedFactory, SeedGraph } from './seed-graph-plan';
|
|
7
7
|
import type { SeedEntity } from './seed-graph-shape';
|
|
8
8
|
|
|
9
9
|
export interface ScaleGraph {
|
|
@@ -23,12 +23,18 @@ export interface ScaleGraph {
|
|
|
23
23
|
export function scaleGraph(
|
|
24
24
|
main: SeedGraph,
|
|
25
25
|
entities: readonly SeedEntity[],
|
|
26
|
-
options: {
|
|
26
|
+
options: {
|
|
27
|
+
readonly perTable: number;
|
|
28
|
+
readonly seed: number;
|
|
29
|
+
/** The app's `defineExercise` factories: a table only they satisfy is built by them here too. */
|
|
30
|
+
readonly factories?: Readonly<Record<string, SeedFactory>> | undefined;
|
|
31
|
+
},
|
|
27
32
|
): ScaleGraph {
|
|
28
33
|
const sized = seedGraph(Object.fromEntries(entities.map((entity) => [entity.$name, entity])), {
|
|
29
34
|
tenants: 1,
|
|
30
35
|
perTable: options.perTable,
|
|
31
36
|
seed: options.seed,
|
|
37
|
+
...(options.factories === undefined ? {} : { factories: options.factories }),
|
|
32
38
|
});
|
|
33
39
|
const shared = (graph: SeedGraph) => graph.inserts.filter((insert) => insert.tenant === null);
|
|
34
40
|
const theirs = shared(main);
|
package/src/scale-sweep-run.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { testJobs } from './fixture-jobs';
|
|
|
10
10
|
import type { ScaleProof, StatementTally } from './scale-sweep';
|
|
11
11
|
import { judgeScale, SCALE_SIZES } from './scale-sweep';
|
|
12
12
|
import { scaleGraph } from './scale-sweep-graph';
|
|
13
|
-
import type { SeedGraph, SeedWriter } from './seed-graph-plan';
|
|
13
|
+
import type { SeedFactory, SeedGraph, SeedWriter } from './seed-graph-plan';
|
|
14
14
|
import { insertSeedGraph } from './seed-graph-plan';
|
|
15
15
|
import type { SeedEntity } from './seed-graph-shape';
|
|
16
16
|
|
|
@@ -45,6 +45,8 @@ export interface ScaleSweepOptions {
|
|
|
45
45
|
readonly seed: number;
|
|
46
46
|
readonly room: ExerciseRoom;
|
|
47
47
|
readonly setup: ScaleSetup;
|
|
48
|
+
/** `defineExercise`'s factories, handed to every sized graph as to the pass's own. */
|
|
49
|
+
readonly factories?: Readonly<Record<string, SeedFactory>> | undefined;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
type Call = { readonly shapes: readonly StatementTally[] } | { readonly failed: string };
|
|
@@ -88,6 +90,7 @@ export async function scaleSweep(
|
|
|
88
90
|
scaleGraph(options.main, options.entities, {
|
|
89
91
|
perTable,
|
|
90
92
|
seed: fnv1a(`scale:${options.seed}:${i}`) || 1,
|
|
93
|
+
factories: options.factories,
|
|
91
94
|
}),
|
|
92
95
|
);
|
|
93
96
|
try {
|
package/src/seed-graph-plan.ts
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
export interface SeedFactory {
|
|
7
7
|
build(): object;
|
|
8
8
|
reset(): void;
|
|
9
|
+
/**
|
|
10
|
+
* Columns that follow from what the GRAPH chose — its keys and references, which no factory
|
|
11
|
+
* builds: `memorial_templates.scopeKey` from `ownerOrgId`. Called on each candidate row before
|
|
12
|
+
* its rules are judged; what it answers wins. A `defineFactory` with it:
|
|
13
|
+
* `{ build: () => f.build(), reset: () => f.reset(), derive: (row) => ({ … }) }`.
|
|
14
|
+
*/
|
|
15
|
+
derive?(row: Readonly<Record<string, unknown>>): object;
|
|
9
16
|
}
|
|
10
17
|
|
|
11
18
|
export interface SeedGraphOptions {
|
package/src/seed-graph-row.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface RowProblem {
|
|
|
17
17
|
readonly uniques: readonly (readonly string[])[];
|
|
18
18
|
/** Per entry of `uniques`, the keys earlier rows already hold. */
|
|
19
19
|
readonly taken: readonly ReadonlySet<string>[];
|
|
20
|
+
/** A factory's `derive`: columns that follow from the picked ones, applied before any rule. */
|
|
21
|
+
readonly derive?: ((row: Readonly<Record<string, unknown>>) => object) | undefined;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export type RowSolution =
|
|
@@ -77,7 +79,7 @@ const build = (problem: RowProblem, picks: readonly number[]): Record<string, un
|
|
|
77
79
|
problem.columns.forEach(([property, candidates], index) => {
|
|
78
80
|
row[property] = candidates[picks[index] ?? 0];
|
|
79
81
|
});
|
|
80
|
-
return row;
|
|
82
|
+
return problem.derive === undefined ? row : { ...row, ...problem.derive(row) };
|
|
81
83
|
};
|
|
82
84
|
|
|
83
85
|
/** Odometer over the varied columns, last one fastest: deterministic and exhaustive up to the cap. */
|
package/src/seed-graph-shape.ts
CHANGED
|
@@ -26,6 +26,12 @@ export interface EntityShape {
|
|
|
26
26
|
readonly references: ReadonlyMap<string, ReferenceDescription>;
|
|
27
27
|
readonly checks: readonly RowCheck[];
|
|
28
28
|
readonly uniques: readonly (readonly string[])[];
|
|
29
|
+
/**
|
|
30
|
+
* The uniques with a `where`: SQL the generator cannot evaluate, so held as total — stricter than
|
|
31
|
+
* the table — unless a factory builds every one of their columns, when the database judges them.
|
|
32
|
+
* `orgs: unique(kind) where kind = 'staff'` otherwise ran an enum out of values (`X_SEED_GRAPH_UNSATISFIED`).
|
|
33
|
+
*/
|
|
34
|
+
readonly partialUniques: readonly (readonly string[])[];
|
|
29
35
|
/**
|
|
30
36
|
* The soft-delete property, or `null`. Seeded null unless a factory builds it: a row born deleted
|
|
31
37
|
* is invisible to every read.
|
|
@@ -83,15 +89,25 @@ export function shapeOf(entity: SeedEntity): EntityShape {
|
|
|
83
89
|
holds: (row) => invariant.holds(row),
|
|
84
90
|
}),
|
|
85
91
|
);
|
|
86
|
-
// A partial unique (`where`) is treated as total: stricter than the table, never looser.
|
|
87
92
|
const uniques = dedupe([
|
|
88
93
|
entity.$primaryKey,
|
|
89
94
|
...description.columns.filter((column) => column.unique).map((c) => toProperties([c.column])),
|
|
90
95
|
...entity.$invariants
|
|
91
|
-
.filter((invariant) => invariant.kind === 'unique')
|
|
96
|
+
.filter((invariant) => invariant.kind === 'unique' && invariant.where === undefined)
|
|
92
97
|
.map((invariant) => toProperties(invariant.columns)),
|
|
93
|
-
...description.indexes
|
|
98
|
+
...description.indexes
|
|
99
|
+
.filter((index) => index.unique && index.where === null)
|
|
100
|
+
.map((i) => toProperties(i.columns)),
|
|
94
101
|
]);
|
|
102
|
+
const total = new Set(uniques.map((set) => set.join('\u0000')));
|
|
103
|
+
const partialUniques = dedupe([
|
|
104
|
+
...entity.$invariants
|
|
105
|
+
.filter((invariant) => invariant.kind === 'unique' && invariant.where !== undefined)
|
|
106
|
+
.map((invariant) => toProperties(invariant.columns)),
|
|
107
|
+
...description.indexes
|
|
108
|
+
.filter((index) => index.unique && index.where !== null)
|
|
109
|
+
.map((i) => toProperties(i.columns)),
|
|
110
|
+
]).filter((set) => !total.has(set.join('\u0000')));
|
|
95
111
|
return {
|
|
96
112
|
entity,
|
|
97
113
|
name: entity.$name,
|
|
@@ -99,6 +115,7 @@ export function shapeOf(entity: SeedEntity): EntityShape {
|
|
|
99
115
|
references: new Map(entity.$references().map((reference) => [reference.property, reference])),
|
|
100
116
|
checks,
|
|
101
117
|
uniques,
|
|
118
|
+
partialUniques,
|
|
102
119
|
softDelete: description.softDelete ? SEED_SOFT_DELETE_PROPERTY : null,
|
|
103
120
|
};
|
|
104
121
|
}
|
package/src/seed-graph.ts
CHANGED
|
@@ -135,7 +135,8 @@ export function seedGraph(
|
|
|
135
135
|
const factory = options.factories?.[name];
|
|
136
136
|
const rows: Placed[] = [];
|
|
137
137
|
placed.set(name, rows);
|
|
138
|
-
const
|
|
138
|
+
const allUniques = [...shape.uniques, ...shape.partialUniques];
|
|
139
|
+
const taken = allUniques.map(() => new Set<string>());
|
|
139
140
|
const rootProperty = roots.get(name);
|
|
140
141
|
const buckets: readonly (string | null)[] = scoped(name) ? tenants : [null];
|
|
141
142
|
let ordinal = 0;
|
|
@@ -176,11 +177,19 @@ export function seedGraph(
|
|
|
176
177
|
candidates.length === 0 ? refuseEmpty(shape, property, tenant) : candidates,
|
|
177
178
|
] as const;
|
|
178
179
|
});
|
|
180
|
+
// A partial unique whose every column the factory built is the database's to judge.
|
|
181
|
+
const held = allUniques
|
|
182
|
+
.map((set, i) => [set, taken[i] as Set<string>] as const)
|
|
183
|
+
.filter(
|
|
184
|
+
([set], i) =>
|
|
185
|
+
i < shape.uniques.length || !set.every((property) => Object.hasOwn(built, property)),
|
|
186
|
+
);
|
|
179
187
|
const problem: RowProblem = {
|
|
180
188
|
columns,
|
|
181
189
|
checks: shape.checks,
|
|
182
|
-
uniques:
|
|
183
|
-
taken,
|
|
190
|
+
uniques: held.map(([set]) => set),
|
|
191
|
+
taken: held.map(([, set]) => set),
|
|
192
|
+
derive: factory?.derive?.bind(factory),
|
|
184
193
|
};
|
|
185
194
|
const solved = solveRow(problem);
|
|
186
195
|
if (!solved.ok) {
|
|
@@ -197,7 +206,7 @@ export function seedGraph(
|
|
|
197
206
|
});
|
|
198
207
|
}
|
|
199
208
|
const row = { ...solved.row };
|
|
200
|
-
|
|
209
|
+
allUniques.forEach((set, i) => {
|
|
201
210
|
const key = uniqueKey(row, set);
|
|
202
211
|
if (key !== undefined) taken[i]?.add(key);
|
|
203
212
|
});
|
package/src/subject-proof.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { ctxOf, finiteOption, runWithContext, serviceActor } from '@ultimat3/cor
|
|
|
8
8
|
import type { Subject, SubjectPlan } from '@ultimat3/jobs';
|
|
9
9
|
import { memoryStepStore, planSubject, stepRunner, subjectEraseTables } from '@ultimat3/jobs';
|
|
10
10
|
import { seedGraph } from './seed-graph';
|
|
11
|
-
import type { SeedGraph, SeedWriter } from './seed-graph-plan';
|
|
11
|
+
import type { SeedFactory, SeedGraph, SeedWriter } from './seed-graph-plan';
|
|
12
12
|
import { insertSeedGraph } from './seed-graph-plan';
|
|
13
13
|
import type { SeedEntity } from './seed-graph-shape';
|
|
14
14
|
import { SubjectEraseIncompleteError } from './subject-proof-error';
|
|
@@ -22,6 +22,8 @@ export interface SubjectProofOptions {
|
|
|
22
22
|
/** Every stored row of one table, as the database holds it, unscoped. */
|
|
23
23
|
readonly read: (entity: SeedEntity) => Promise<readonly Stored[]>;
|
|
24
24
|
readonly seed?: number;
|
|
25
|
+
/** `defineExercise`'s factories: a table only they satisfy is built by them for the proof too. */
|
|
26
|
+
readonly factories?: Readonly<Record<string, SeedFactory>> | undefined;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export interface SubjectProofResult {
|
|
@@ -107,6 +109,7 @@ export async function subjectProof<I>(
|
|
|
107
109
|
const graph = seedGraph(Object.fromEntries(entities), {
|
|
108
110
|
seed: finiteOption('subjectProof', 'seed', options.seed ?? 1),
|
|
109
111
|
tenants: 2,
|
|
112
|
+
...(options.factories === undefined ? {} : { factories: options.factories }),
|
|
110
113
|
});
|
|
111
114
|
await insertSeedGraph(graph, options.writer);
|
|
112
115
|
const tenant = graph.tenants[0] ?? null;
|