@cratis/chronicle 6.35.0 → 6.36.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.
Files changed (22) hide show
  1. package/dist/testing/projections/ProjectionArithmetic.d.ts +11 -0
  2. package/dist/testing/projections/ProjectionArithmetic.d.ts.map +1 -0
  3. package/dist/testing/projections/ProjectionArithmetic.js +70 -0
  4. package/dist/testing/projections/ProjectionArithmetic.js.map +1 -0
  5. package/dist/testing/projections/ProjectionCapabilities.d.ts.map +1 -1
  6. package/dist/testing/projections/ProjectionCapabilities.js +26 -10
  7. package/dist/testing/projections/ProjectionCapabilities.js.map +1 -1
  8. package/dist/testing/projections/ProjectionReadModelProcessor.d.ts.map +1 -1
  9. package/dist/testing/projections/ProjectionReadModelProcessor.js +10 -3
  10. package/dist/testing/projections/ProjectionReadModelProcessor.js.map +1 -1
  11. package/dist/tsconfig.tsbuildinfo +1 -1
  12. package/package.json +2 -2
  13. package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_a_fluent_projection.spec.ts +85 -0
  14. package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_legacy_decorators.legacy.spec.ts +91 -0
  15. package/testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts +92 -0
  16. package/testing/projections/ProjectionArithmetic.ts +71 -0
  17. package/testing/projections/ProjectionCapabilities.ts +27 -9
  18. package/testing/projections/ProjectionReadModelProcessor.fixtures.spec.ts +9 -6
  19. package/testing/projections/ProjectionReadModelProcessor.ts +9 -3
  20. package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +3 -3
  21. package/testing/projections/for_ProjectionCapabilities/when_validating_arithmetic.spec.ts +127 -0
  22. package/testing/projections/for_ProjectionCapabilities/when_validating_numeric_schemas.spec.ts +4 -5
@@ -8,6 +8,7 @@ import { eventContractPath } from '../../projections/eventContractPath.js';
8
8
  import type { ContractEventType, FromRecord, RemovedWithRecord } from '../../projections/declarative/ProjectionBuilderCore.js';
9
9
  import { getEventTypeMapKey, type ChildrenDefinitionLike } from '../../projections/modelBound/childrenAndNestedBuilder.js';
10
10
  import type { JsonSchema } from '../../schemas/JsonSchema.js';
11
+ import { ProjectionArithmetic } from './ProjectionArithmetic.js';
11
12
  import { ProjectionChildrenCapabilities } from './ProjectionChildrenCapabilities.js';
12
13
  import { UnsupportedProjectionOperation } from './UnsupportedProjectionOperation.js';
13
14
 
@@ -95,6 +96,19 @@ export class ProjectionCapabilities {
95
96
  reject(eventContractPath('From', entry.Key), 'events subscribed through both From and RemovedWith require a kernel-backed test (ChronicleKernelScenario / live kernel)');
96
97
  }
97
98
  }
99
+ const accumulators = new Set(from.flatMap(entry => Object.entries(entry.Value.Properties ?? {})
100
+ .filter(([, expression]) => ProjectionArithmetic.isArithmetic(expression)).map(([property]) => property)));
101
+ if (accumulators.size && Object.keys(wire.Children as object ?? {}).length) {
102
+ reject('Children', 'root arithmetic combined with children requires a kernel-backed test');
103
+ }
104
+ for (const entry of from) {
105
+ for (const [property, expression] of Object.entries(entry.Value.Properties ?? {})) {
106
+ if (expression === '$null' && accumulators.has(property)) {
107
+ reject(`${eventContractPath('From', entry.Key)}.Properties.${property}`,
108
+ 'clearing an arithmetic accumulator requires a kernel-backed test; the kernel rejects arithmetic on a present null');
109
+ }
110
+ }
111
+ }
98
112
  for (const entry of [...from, ...removedWith]) {
99
113
  const section = from.includes(entry as FromRecord) ? 'From' : 'RemovedWith';
100
114
  const path = eventContractPath(section, entry.Key);
@@ -105,10 +119,10 @@ export class ProjectionCapabilities {
105
119
  const properties = (entry as FromRecord).Value.Properties ?? {};
106
120
  for (const [property, expression] of Object.entries(properties)) {
107
121
  const mappingPath = `${path}.Properties.${property}`;
108
- this.checkMapping(schema!, eventSchema, property, expression, mappingPath, reject);
122
+ this.checkMapping(schema!, eventSchema, property, expression, mappingPath, reject, accumulators);
109
123
  }
110
- if (wire.AutoMap !== AutoMap.Disabled) {
111
- this.checkAutoMap(schema!, eventSchema, properties, wire.NoAutoMapProperties as string[] ?? [], path, reject);
124
+ if (wire.AutoMap !== AutoMap.Disabled && !ProjectionArithmetic.suppressesAutoMap(properties)) {
125
+ this.checkAutoMap(schema!, eventSchema, properties, wire.NoAutoMapProperties as string[] ?? [], path, reject, accumulators);
112
126
  }
113
127
  }
114
128
  ProjectionChildrenCapabilities.validate(wire.Children as Record<string, ChildrenDefinitionLike> ?? {}, {
@@ -121,20 +135,20 @@ export class ProjectionCapabilities {
121
135
 
122
136
  private static checkAutoMap(
123
137
  modelSchema: JsonSchema, eventSchema: JsonSchema, explicit: Record<string, string>, exclusions: string[],
124
- path: string, reject: (path: string, reason: string) => never
138
+ path: string, reject: (path: string, reason: string) => never, accumulators: ReadonlySet<string>
125
139
  ): void {
126
140
  for (const [destination] of Object.entries(modelSchema.properties ?? {})) {
127
141
  if (Object.keys(explicit).some(name => name.toLowerCase() === destination.toLowerCase()) ||
128
142
  exclusions.some(name => name.toLowerCase() === destination.toLowerCase())) continue;
129
143
  const candidates = Object.keys(eventSchema.properties ?? {}).filter(source => source.toLowerCase() === destination.toLowerCase());
130
144
  if (candidates.length > 1) reject(`${path}.AutoMap.${destination}`, `inferred AutoMap source is ambiguous: ${candidates.join(', ')}`);
131
- if (candidates.length) this.checkMapping(modelSchema, eventSchema, destination, candidates[0], `${path}.AutoMap.${destination}`, reject);
145
+ if (candidates.length) this.checkMapping(modelSchema, eventSchema, destination, candidates[0], `${path}.AutoMap.${destination}`, reject, accumulators);
132
146
  }
133
147
  }
134
148
 
135
149
  private static checkMapping(
136
150
  modelSchema: JsonSchema, eventSchema: JsonSchema, destination: string, expression: string,
137
- path: string, reject: (path: string, reason: string) => never
151
+ path: string, reject: (path: string, reason: string) => never, accumulators?: ReadonlySet<string>
138
152
  ): void {
139
153
  const fail = (reason: string): never => reject(path, `expression '${expression}': ${reason}`);
140
154
  const target = modelSchema.properties?.[destination]
@@ -184,9 +198,10 @@ export class ProjectionCapabilities {
184
198
  }
185
199
  return;
186
200
  }
187
- const operation = /^(\$add|\$subtract)\(([^()]+)\)$/.exec(expression);
188
- const arithmetic = operation || ['$count', '$increment', '$decrement'].includes(expression);
189
- if (arithmetic) fail('arithmetic requires a kernel-backed test (ChronicleKernelScenario / live kernel)');
201
+ if (ProjectionArithmetic.isArithmetic(expression)) {
202
+ ProjectionArithmetic.validate(expression, target, eventSchema, fail);
203
+ return;
204
+ }
190
205
  if (/^[A-Za-z_][\w]*(?:\.[A-Za-z_][\w]*)*$/.test(expression)) {
191
206
  if (['true', 'True', 'false', 'False'].includes(expression)) {
192
207
  fail('kernel resolves this expression as a boolean literal before event content');
@@ -195,6 +210,9 @@ export class ProjectionCapabilities {
195
210
  const source = this.propertyAt(eventSchema, expression);
196
211
  if (!source) return fail(`event property '${expression}' is absent from the participating event schema`);
197
212
  this.checkSchema(source, path, (_path, reason) => fail(reason));
213
+ if (accumulators?.has(destination) && !this.numeric(source)) {
214
+ fail('assigning non-numeric event properties to an arithmetic accumulator requires a kernel-backed test; missing values can clear it');
215
+ }
198
216
  if (!this.compatible(source, target)) {
199
217
  fail(`source '${expression}' (${source.type}${source.format ? `/${source.format}` : ''}) to target '${destination}' (${target.type}${target.format ? `/${target.format}` : ''}) requires a kernel-backed test`);
200
218
  }
@@ -38,11 +38,11 @@ const allFixtures = readdirSync(directory).filter(name => name.endsWith('.json')
38
38
  .map(name => ({ name, fixture: JSON.parse(readFileSync(new URL(name, directory), 'utf8')) as Fixture }));
39
39
  const fixtures = allFixtures.filter(({ fixture }) => fixture.kind === 'kernelSemantics');
40
40
  const guardErrors: Record<string, { type: string; message: string }> = {
41
- 'aggregate-only-automap.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic requires a kernel-backed test' },
42
- 'arithmetic.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic requires a kernel-backed test' },
43
- 'rounded-integer-operand.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic requires a kernel-backed test' },
44
- 'int32-overflow.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic requires a kernel-backed test' },
45
- 'null-accumulator.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic requires a kernel-backed test' },
41
+ 'arithmetic-string-operand.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic operands must have number/double' },
42
+ 'arithmetic-nonnumeric-operand.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic operands must have number/double' },
43
+ 'arithmetic-uint32-overflow.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic targets must be number/double or integer/int32' },
44
+ 'arithmetic-null-initial.json': { type: 'UnsupportedProjectionOperation', message: 'initial numeric value must be a JSON number' },
45
+ 'null-accumulator.json': { type: 'UnsupportedProjectionOperation', message: 'clearing an arithmetic accumulator requires a kernel-backed test' },
46
46
  'concept-shaped-objects.json': { type: 'UnsupportedProjectionOperation', message: 'nested event property paths require a kernel-backed test' },
47
47
  'mapped-and-removed.json': { type: 'UnsupportedProjectionOperation', message: 'both From and RemovedWith require a kernel-backed test' },
48
48
  'mapped-id-key-wins.json': { type: 'UnsupportedProjectionOperation', message: 'identifier or case-insensitively colliding target mappings require a kernel-backed test' },
@@ -97,9 +97,12 @@ describe('when replaying committed kernel semantics fixtures', () => {
97
97
  () => { throw new Error('Evaluator accepted a kernel-rejected mapping'); },
98
98
  error => {
99
99
  (error instanceof RangeError).should.be.true;
100
- (error as Error).message.should.include('not supported by integer/int32');
100
+ (error as Error).message.should.include(input.expectedError!.type === 'System.OverflowException'
101
+ ? 'outside the supported integer/int32 range' : 'not supported by integer/int32');
101
102
  }
102
103
  );
104
+ processor.engineState.should.deep.equal(expected.engineState);
105
+ processor.publicRead.should.deep.equal(expected.publicRead);
103
106
  } else {
104
107
  await processor.process(events.slice(0, step + 1));
105
108
  processor.engineState.should.deep.equal(expected.engineState);
@@ -12,6 +12,7 @@ import type { JsonSchema } from '../../schemas/JsonSchema.js';
12
12
  import type { IReadModelProcessor } from '../IReadModelProcessor.js';
13
13
  import type { ReadModelState } from '../ReadModelState.js';
14
14
  import type { ScenarioEvent } from '../ScenarioEvent.js';
15
+ import { ProjectionArithmetic } from './ProjectionArithmetic.js';
15
16
  import { ProjectionCapabilities } from './ProjectionCapabilities.js';
16
17
  import { ProjectionExpressionEvaluator } from './ProjectionExpressionEvaluator.js';
17
18
  import { ProjectionValueConverter } from './ProjectionValueConverter.js';
@@ -107,7 +108,8 @@ export class ProjectionReadModelProcessor<TReadModel extends object> implements
107
108
  if (from) {
108
109
  const explicit = { ...from.Value.Properties };
109
110
  const excluded = (this._definition.NoAutoMapProperties ?? []);
110
- const properties = this._definition.AutoMap !== AutoMap.Disabled ? this.withAutoMap(explicit, schema, this._schema, excluded) : explicit;
111
+ const properties = this._definition.AutoMap !== AutoMap.Disabled && !ProjectionArithmetic.suppressesAutoMap(explicit)
112
+ ? this.withAutoMap(explicit, schema, this._schema, excluded) : explicit;
111
113
  this.apply(properties, content, event, this._schema, state);
112
114
  }
113
115
  if (child) this.applyChild(child, content, event, schema, state, binding);
@@ -168,8 +170,12 @@ export class ProjectionReadModelProcessor<TReadModel extends object> implements
168
170
 
169
171
  private apply(properties: Record<string, string>, content: unknown, event: ScenarioEvent, target: JsonSchema, state: Record<string, unknown>): void {
170
172
  for (const [destination, expression] of Object.entries(properties)) {
171
- const value = ProjectionExpressionEvaluator.value(expression, content, event.context, target.properties![destination]);
172
- // The kernel compares old and new null values; null on an absent member is no change.
173
+ const arithmetic = ProjectionArithmetic.isArithmetic(expression);
174
+ const value = arithmetic
175
+ ? ProjectionArithmetic.value(expression, content, target.properties![destination], state[destination])
176
+ : ProjectionExpressionEvaluator.value(expression, content, event.context, target.properties![destination]);
177
+ // Zero arithmetic on an absent accumulator is no change, just like null on an absent scalar.
178
+ if (arithmetic && value === 0 && state[destination] === undefined) continue;
173
179
  if (value !== null || state[destination] !== undefined) state[destination] = value;
174
180
  }
175
181
  }
@@ -148,11 +148,11 @@ describe('when rejecting unsupported operations before any event is seeded', ()
148
148
  });
149
149
 
150
150
  for (const expression of ['$add(quantity)', '$subtract(quantity)', '$count', '$increment', '$decrement']) {
151
- it(`should reject all arithmetic ${expression} before replay`, () => {
151
+ it(`should reject arithmetic ${expression} on non-numeric targets before replay`, () => {
152
152
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.total).toValue(0)));
153
- definition.From[0].Value.Properties.total = expression;
153
+ definition.From[0].Value.Properties = { name: expression };
154
154
  (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
155
- .with.property('message').that.includes('arithmetic requires a kernel-backed test (ChronicleKernelScenario / live kernel)');
155
+ .with.property('message').that.includes('arithmetic targets must be number/double or integer/int32');
156
156
  });
157
157
  }
158
158
 
@@ -0,0 +1,127 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { chai, describe, it } from 'vitest';
5
+ import { count } from '../../../projections/modelBound/count.js';
6
+ import { increment } from '../../../projections/modelBound/increment.js';
7
+ import { decrement } from '../../../projections/modelBound/decrement.js';
8
+ import type { JsonSchema } from '../../../schemas/JsonSchema.js';
9
+ import { ProjectionCapabilities } from '../ProjectionCapabilities.js';
10
+ import { UnsupportedProjectionOperation } from '../UnsupportedProjectionOperation.js';
11
+ import { Changed } from './given/Changed.js';
12
+ import { Removed } from './given/Removed.js';
13
+ import { compileDeclarative, compileModelBound } from './given/compile.js';
14
+
15
+ chai.should();
16
+
17
+ describe('when validating arithmetic before replay', () => {
18
+ for (const expression of ['$add(quantity)', '$subtract(quantity)', '$count', '$increment', '$decrement']) {
19
+ it(`should admit ${expression} on number fields without seeding events`, () => {
20
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
21
+ definition.From[0].Value.Properties.total = expression;
22
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
23
+ });
24
+ }
25
+
26
+ for (const expression of ['$add(name)', '$subtract(name)', '$add(missing)', '$add(inner.quantity)', '$subtract($eventSourceId)', '$add($value(1))', '$add(true)', '$add()']) {
27
+ it(`should reject unproven arithmetic operands in ${expression} with the contract path`, () => {
28
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
29
+ definition.From[0].Value.Properties.total = expression;
30
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
31
+ .with.property('message').that.includes('From[capability-changed:1].Properties.total (.from().addFrom)').and.includes('arithmetic');
32
+ });
33
+ }
34
+
35
+ for (const operation of ['add', 'subtract']) {
36
+ for (const property of ['unit_price', '_unit_price']) {
37
+ it(`should reject $${operation}(${property}) with the kernel operand limitation before replay`, () => {
38
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
39
+ compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties![property] = { type: 'number' };
40
+ definition.From[0].Value.Properties.total = `$${operation}(${property})`;
41
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
42
+ .with.property('message').that.includes('From[capability-changed:1].Properties.total')
43
+ .and.includes('kernel only permits an underscore as the first character')
44
+ .and.includes('https://github.com/Cratis/Chronicle/issues/4491');
45
+ });
46
+ }
47
+ it(`should admit $${operation}(_quantity) with a leading underscore`, () => {
48
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
49
+ compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!._quantity = { type: 'number' };
50
+ definition.From[0].Value.Properties.total = `$${operation}(_quantity)`;
51
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
52
+ });
53
+ }
54
+
55
+ for (const target of [{ type: 'integer', format: 'uint32' }, { type: ['number', 'null'] }, { type: 'string' }, { type: 'boolean' }]) {
56
+ it(`should reject an unproven ${JSON.stringify(target)} accumulator schema`, () => {
57
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.increment(model => model.total)));
58
+ const schema = JSON.parse(compiled.readModels[0].Schema) as JsonSchema;
59
+ schema.properties!.total = target as JsonSchema;
60
+ compiled.readModels[0].Schema = JSON.stringify(schema);
61
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
62
+ .with.property('message').that.includes('From[capability-changed:1].Properties.total');
63
+ });
64
+ }
65
+
66
+ for (const source of [{ type: 'boolean' }, { type: 'object' }, { type: ['number', 'null'] }, { type: 'number', format: 'decimal' }, { type: 'integer', format: 'int64' }]) {
67
+ it(`should reject an unproven ${JSON.stringify(source)} operand schema`, () => {
68
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
69
+ compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.quantity = source as JsonSchema;
70
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
71
+ .with.property('message').that.includes('arithmetic operands must have number/double');
72
+ });
73
+ }
74
+
75
+ it('should reject arithmetic on a protected operand', () => {
76
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
77
+ compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.quantity.security = [{ metadataType: 'EncryptedSubject', details: '' }];
78
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
79
+ .with.property('message').that.includes('protected arithmetic operands');
80
+ });
81
+
82
+ it('should reject any declaration that can clear an accumulator before arithmetic', () => {
83
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.increment(model => model.total))
84
+ .from(Removed, from => from.set(model => model.total).toValue(0)));
85
+ definition.From[1].Value.Properties.total = '$null';
86
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
87
+ .with.property('message').that.includes('From[capability-removed:1].Properties.total').and.includes('clearing an arithmetic accumulator');
88
+ });
89
+
90
+ it('should reject a text assignment that can clear an arithmetic accumulator', () => {
91
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.total).to(event => event.name))
92
+ .from(Removed, from => from.increment(model => model.total)));
93
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
94
+ .with.property('message').that.includes('assigning non-numeric event properties to an arithmetic accumulator');
95
+ });
96
+
97
+ it('should reject inferred text AutoMap that can clear an arithmetic accumulator', () => {
98
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed)
99
+ .from(Removed, from => from.increment(model => model.total)));
100
+ compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.total = { type: 'string' };
101
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
102
+ .with.property('message').that.includes('AutoMap.total').and.includes('assigning non-numeric event properties');
103
+ });
104
+
105
+ it('should reject root arithmetic combined with children without widening the child boundary', () => {
106
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.increment(model => model.total))
107
+ .children(model => model.labels, child => child.from(Removed)));
108
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
109
+ .with.property('message').that.includes('root arithmetic combined with children');
110
+ });
111
+
112
+ for (const [name, decorate] of [['count', count], ['increment', increment], ['decrement', decrement]] as const) {
113
+ it(`should keep @${name} constant keys outside the supported source-id shape`, () => {
114
+ const { compiled, definition } = compileModelBound(model => decorate(Removed, 'all')(model.prototype, 'total'));
115
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
116
+ .with.property('message').that.includes(`(@${name})`).and.includes('only $eventSourceId keys');
117
+ });
118
+ }
119
+
120
+ it('should not validate suppressed AutoMap as an executed mapping', () => {
121
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.increment(model => model.total)));
122
+ const schema = JSON.parse(compiled.readModels[0].Schema) as JsonSchema;
123
+ schema.properties!.quantity = { type: 'number', format: 'decimal' };
124
+ compiled.readModels[0].Schema = JSON.stringify(schema);
125
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
126
+ });
127
+ });
@@ -12,7 +12,7 @@ chai.should();
12
12
 
13
13
  describe('when validating the numeric capability matrix', () => {
14
14
  for (const format of ['int32', 'uint32']) {
15
- it(`should accept a finite ${format} literal without admitting arithmetic`, () => {
15
+ it(`should accept a finite ${format} literal`, () => {
16
16
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from
17
17
  .set(model => model.quantity).toValue(12)));
18
18
  const modelSchema = JSON.parse(compiled.readModels[0].Schema) as { properties: { quantity: { type: string; format?: string } } };
@@ -63,13 +63,12 @@ describe('when validating the numeric capability matrix', () => {
63
63
  });
64
64
  }
65
65
 
66
- it('should reject a double event operand into an integer accumulator before replay', () => {
66
+ it('should admit fixture-backed rounding of a double event operand into an int32 accumulator', () => {
67
67
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
68
68
  const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { total: { type: string; format?: string } } };
69
69
  schema.properties.total = { type: 'integer', format: 'int32' };
70
70
  compiled.readModels[0].Schema = JSON.stringify(schema);
71
- (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
72
- .with.property('message').that.includes('arithmetic requires a kernel-backed test');
71
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
73
72
  });
74
73
 
75
74
  it('should reject an int32 $value literal with a fractional spelling even if its value is integral', () => {
@@ -96,6 +95,6 @@ describe('when validating the numeric capability matrix', () => {
96
95
  const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
97
96
  compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.quantity.format = 'int64';
98
97
  (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
99
- .with.property('message').that.includes('arithmetic requires a kernel-backed test');
98
+ .with.property('message').that.includes('arithmetic operands must have number/double');
100
99
  });
101
100
  });