@things-factory/headless-twin 10.1.141 → 10.1.143

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 (28) hide show
  1. package/dist-server/engine/model-refusal.d.ts +3 -0
  2. package/dist-server/engine/model-refusal.js +37 -0
  3. package/dist-server/engine/model-refusal.js.map +1 -0
  4. package/dist-server/engine/twin-engine.js +8 -7
  5. package/dist-server/engine/twin-engine.js.map +1 -1
  6. package/dist-server/service/reference/reference-resolver.js +4 -1
  7. package/dist-server/service/reference/reference-resolver.js.map +1 -1
  8. package/dist-server/service/twin-forecast/twin-forecast-query.js +2 -1
  9. package/dist-server/service/twin-forecast/twin-forecast-query.js.map +1 -1
  10. package/dist-server/service/twin-journal/twin-journal-query.js +2 -1
  11. package/dist-server/service/twin-journal/twin-journal-query.js.map +1 -1
  12. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js +2 -1
  13. package/dist-server/service/twin-lifecycle/twin-lifecycle-mutation.js.map +1 -1
  14. package/dist-server/service/twin-model/twin-model-item-query.js +2 -1
  15. package/dist-server/service/twin-model/twin-model-item-query.js.map +1 -1
  16. package/dist-server/service/twin-model/twin-model-query.js +2 -1
  17. package/dist-server/service/twin-model/twin-model-query.js.map +1 -1
  18. package/package.json +6 -6
  19. package/server/engine/model-refusal.ts +31 -0
  20. package/server/engine/twin-engine.ts +12 -7
  21. package/server/service/reference/reference-resolver.ts +5 -2
  22. package/server/service/twin-forecast/twin-forecast-query.ts +3 -2
  23. package/server/service/twin-journal/twin-journal-query.ts +3 -2
  24. package/server/service/twin-lifecycle/twin-lifecycle-mutation.ts +3 -2
  25. package/server/service/twin-model/twin-model-item-query.ts +3 -2
  26. package/server/service/twin-model/twin-model-query.ts +3 -2
  27. package/test/model-refusal.test.ts +75 -0
  28. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,75 @@
1
+ /*
2
+ * ★ A twin model the kernel refuses reaches the reader as a named refusal, not INTERNAL_SERVER_ERROR.
3
+ *
4
+ * 2026-10-10: `twinReplay(miratek)` failed with the MES kernel's English sentence "MES twin requires a
5
+ * production declaration" in the shell's global notice. The kernel now names it
6
+ * (`twin-model-no-production-spec`, with `params`); the engine turns a named kernel error into a shell
7
+ * `Refusal` wherever it loads or replays a model, and the resolvers that reach those places speak it.
8
+ */
9
+ import { test } from 'node:test'
10
+ import assert from 'node:assert/strict'
11
+ import { readFileSync } from 'node:fs'
12
+ import { Refusal, speakRefusal } from '@things-factory/shell'
13
+ import { asModelRefusal, refusingModel } from '../server/engine/model-refusal.ts'
14
+
15
+ /* The shape the kernel throws (`MesKernel.loadTwinModel`, twin-kernel 02f117f). */
16
+ function kernelRefusal(): Error {
17
+ return Object.assign(new Error('MES twin requires a production declaration (ProductionSpec): …'), {
18
+ code: 'twin-model-no-production-spec',
19
+ params: {}
20
+ })
21
+ }
22
+
23
+ test('a named kernel error becomes a Refusal that says which twin', () => {
24
+ const r = asModelRefusal(kernelRefusal(), 'miratek')
25
+ assert.ok(r instanceof Refusal)
26
+ assert.equal((r as Refusal).code, 'twin-model-no-production-spec')
27
+ assert.deepEqual((r as Refusal).params, { instanceId: 'miratek' })
28
+ assert.match((r as Refusal).message, /requires a production declaration/, 'the English sentence stays as the fallback')
29
+ })
30
+
31
+ test('an error without a name passes through untouched', () => {
32
+ const plain = new Error('boom')
33
+ assert.equal(asModelRefusal(plain, 'x'), plain)
34
+ const codeOnly = Object.assign(new Error('x'), { code: 'ENOENT' })
35
+ assert.equal(asModelRefusal(codeOnly, 'x'), codeOnly, 'a code without params is a system error, not a refusal')
36
+ assert.throws(() => refusingModel('x', () => { throw plain }), e => e === plain)
37
+ })
38
+
39
+ test('spoken, it is a refusal in the reader language under error.<code>', () => {
40
+ let askedKey = ''
41
+ const t = (key: string, opts?: any) => ((askedKey = key), `${opts.instanceId}: no production declaration`)
42
+ const spoken = speakRefusal(asModelRefusal(kernelRefusal(), 'miratek') as Refusal, { t })
43
+ assert.equal(askedKey, 'error.twin-model-no-production-spec')
44
+ assert.equal(spoken.message, 'miratek: no production declaration')
45
+ assert.equal(spoken.extensions.code, 'BAD_USER_INPUT', 'not INTERNAL_SERVER_ERROR')
46
+ assert.deepEqual(spoken.extensions.refusal, { code: 'twin-model-no-production-spec', params: { instanceId: 'miratek' } })
47
+ })
48
+
49
+ const ENGINE = readFileSync(new URL('../server/engine/twin-engine.ts', import.meta.url), 'utf8')
50
+
51
+ test('every place the engine loads or replays a model goes through refusingModel', () => {
52
+ const code = ENGINE.split('\n').filter(l => !/^\s*(\*|\/\/|\/\*)/.test(l))
53
+ const roads = code.filter(l => /\.loadTwinModel\(|\breplay(From|WithCheckpoint|Segments)?\(/.test(l))
54
+ assert.ok(roads.length >= 3, 'the engine still loads models somewhere')
55
+ for (const l of roads) {
56
+ /* a replay call that wraps onto the next line is inside a refusingModel opened on the line above */
57
+ const i = code.indexOf(l)
58
+ assert.ok(/refusingModel\(/.test(l) || /refusingModel\([^)]*\(\)\s*=>\s*$/.test(code[i - 1]), `not wrapped: ${l.trim()}`)
59
+ }
60
+ })
61
+
62
+ test('the resolvers that reach those places speak refusals', () => {
63
+ for (const f of [
64
+ 'twin-journal/twin-journal-query.ts',
65
+ 'twin-model/twin-model-query.ts',
66
+ 'twin-model/twin-model-item-query.ts',
67
+ 'twin-lifecycle/twin-lifecycle-mutation.ts',
68
+ 'twin-forecast/twin-forecast-query.ts'
69
+ ]) {
70
+ const src = readFileSync(new URL(`../server/service/${f}`, import.meta.url), 'utf8')
71
+ assert.match(src, /@Resolver\(\)\n@UseMiddleware\(SpeakRefusals\)\nexport class/, f)
72
+ }
73
+ const ref = readFileSync(new URL('../server/service/reference/reference-resolver.ts', import.meta.url), 'utf8')
74
+ assert.match(ref, /e instanceof Refusal \? refusalKey\(e\.code\)/, 'the import result speaks the same key')
75
+ })