|
27 | 27 | * the only shape that can hold duplicates, and a table that already holds them |
28 | 28 | * can never build the index again. |
29 | 29 | * |
30 | | - * The engine is OBSERVED, never replaced: every verb forwards to it, and the |
31 | | - * dispatch-shaped verbs open with the producer's own predicates |
32 | | - * (`check:engine-double-contract`). The protocol is a counting stub — what is |
33 | | - * pinned about it is whether it is CALLED, which a stub answers exactly. |
| 30 | + * The engine is SPIED, never replaced: each spied verb counts and then runs |
| 31 | + * the real method, so no double stands between the heal and the engine's own |
| 32 | + * dispatch contract. The protocol is a counting stub — what is pinned about it |
| 33 | + * is whether it is CALLED, which a stub answers exactly. |
34 | 34 | */ |
35 | 35 |
|
36 | | -import { describe, it, expect, afterEach } from 'vitest'; |
| 36 | +import { describe, it, expect, afterEach, vi } from 'vitest'; |
37 | 37 | import { ObjectQL } from '@objectstack/objectql'; |
38 | 38 | import { SqlDriver } from '@objectstack/driver-sql'; |
39 | | -import { assertEngineFindOnePredicate, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; |
40 | 39 |
|
41 | 40 | import { SysPermissionSet } from './objects/sys-permission-set.object.js'; |
42 | 41 | import { |
@@ -81,6 +80,7 @@ const driftedRow = (id: string, name: string) => ({ |
81 | 80 |
|
82 | 81 | const engines: ObjectQL[] = []; |
83 | 82 | afterEach(async () => { |
| 83 | + vi.restoreAllMocks(); |
84 | 84 | while (engines.length) { |
85 | 85 | try { await engines.pop()?.destroy(); } catch { /* noop */ } |
86 | 86 | } |
@@ -111,33 +111,43 @@ async function boot(declaredNames: string[], rows: Array<Record<string, any>>): |
111 | 111 | return engine; |
112 | 112 | } |
113 | 113 |
|
114 | | -/** The real engine with its verbs counted; `refuseNameReads` makes every by-name read of the table throw. */ |
| 114 | +/** |
| 115 | + * The real engine with its verbs SPIED — every call still runs the real method, |
| 116 | + * so the engine's own dispatch contract answers it; the spies only count and, |
| 117 | + * when `refuseNameReads` is set, make a by-name read of the table throw. |
| 118 | + * |
| 119 | + * `withRegistry: false` hands the heal the same engine with `registry` hidden, |
| 120 | + * the shape of a kernel whose SchemaRegistry is not readable — the one shape |
| 121 | + * whose trust rule still reads the layered item. |
| 122 | + */ |
115 | 123 | function observe(engine: any, opts: { withRegistry?: boolean } = {}) { |
116 | | - const log = { updatedIds: [] as string[], inserts: 0, nameReads: 0 }; |
| 124 | + const log = { updatedIds: [] as string[], inserts: 0 }; |
117 | 125 | const state = { refuseNameReads: false }; |
118 | | - const ql: any = { |
119 | | - ...(opts.withRegistry === false ? {} : { registry: engine.registry }), |
120 | | - find: async (o: string, q?: any, opt?: any) => { |
121 | | - if (o === 'sys_permission_set' && q?.where && typeof q.where === 'object' && 'name' in q.where) { |
122 | | - log.nameReads += 1; |
123 | | - if (state.refuseNameReads) throw new Error('fake outage: the by-name read was refused'); |
124 | | - } |
125 | | - return engine.find(o, q, opt); |
126 | | - }, |
127 | | - findOne: (o: string, q?: any, opt?: any) => { |
128 | | - assertEngineFindOnePredicate(o, q); |
129 | | - return engine.findOne(o, q, opt); |
130 | | - }, |
131 | | - insert: async (o: string, d: any, opt?: any) => { |
132 | | - if (o === 'sys_permission_set') log.inserts += 1; |
133 | | - return engine.insert(o, d, opt); |
134 | | - }, |
135 | | - update: async (o: string, d: any, opt?: any) => { |
136 | | - assertEngineUpdateDispatch(d, opt); |
137 | | - if (o === 'sys_permission_set') log.updatedIds.push(String(d?.id)); |
138 | | - return engine.update(o, d, opt); |
139 | | - }, |
140 | | - }; |
| 126 | + const realFind = engine.find.bind(engine); |
| 127 | + const realInsert = engine.insert.bind(engine); |
| 128 | + const realUpdate = engine.update.bind(engine); |
| 129 | + vi.spyOn(engine, 'find').mockImplementation(async (o: any, q?: any, opt?: any) => { |
| 130 | + const byName = o === 'sys_permission_set' && q?.where && typeof q.where === 'object' && 'name' in q.where; |
| 131 | + if (byName && state.refuseNameReads) throw new Error('fake outage: the by-name read was refused'); |
| 132 | + return realFind(o, q, opt); |
| 133 | + }); |
| 134 | + vi.spyOn(engine, 'insert').mockImplementation(async (o: any, d: any, opt?: any) => { |
| 135 | + if (o === 'sys_permission_set') log.inserts += 1; |
| 136 | + return realInsert(o, d, opt); |
| 137 | + }); |
| 138 | + vi.spyOn(engine, 'update').mockImplementation(async (o: any, d: any, opt?: any) => { |
| 139 | + if (o === 'sys_permission_set') log.updatedIds.push(String(d?.id)); |
| 140 | + return realUpdate(o, d, opt); |
| 141 | + }); |
| 142 | + const ql = opts.withRegistry === false |
| 143 | + ? new Proxy(engine, { |
| 144 | + get(target, prop) { |
| 145 | + if (prop === 'registry') return undefined; |
| 146 | + const value = Reflect.get(target, prop, target); |
| 147 | + return typeof value === 'function' ? value.bind(target) : value; |
| 148 | + }, |
| 149 | + }) |
| 150 | + : engine; |
141 | 151 | return { ql, log, state }; |
142 | 152 | } |
143 | 153 |
|
|
0 commit comments