@asteby/metacore-runtime-react 9.2.0 → 11.0.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.
@@ -0,0 +1,249 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ applyHotSwapReload,
4
+ withVersionParam,
5
+ clearFederationContainer,
6
+ shortenHash,
7
+ type HotSwapReloadConfig,
8
+ type HotSwapReloadDeps,
9
+ } from '../hotswap-reload-policy'
10
+ import { type AddonManifestChangedMessage } from '../manifest-hotswap-subscriber'
11
+
12
+ const msg = (
13
+ addonKey: string,
14
+ newHash: string | undefined = 'sha256:abcdef1234567890',
15
+ ): AddonManifestChangedMessage => ({
16
+ type: 'ADDON_MANIFEST_CHANGED',
17
+ payload: { addonKey, newHash },
18
+ })
19
+
20
+ interface FakeState {
21
+ map: Record<string, string>
22
+ deps: HotSwapReloadDeps
23
+ reload: ReturnType<typeof vi.fn>
24
+ }
25
+
26
+ function makeFakeState(): FakeState {
27
+ const reload = vi.fn()
28
+ const state: FakeState = {
29
+ map: {},
30
+ // populated below
31
+ deps: {} as HotSwapReloadDeps,
32
+ reload,
33
+ }
34
+ state.deps = {
35
+ setVersionMap: (updater) => {
36
+ state.map = updater(state.map)
37
+ },
38
+ reload,
39
+ }
40
+ return state
41
+ }
42
+
43
+ describe('shortenHash', () => {
44
+ it('strips the algorithm prefix and truncates to 8 hex chars', () => {
45
+ expect(shortenHash('sha256:ABCDEF1234567890')).toBe('abcdef12')
46
+ expect(shortenHash('abcdef1234567890')).toBe('abcdef12')
47
+ })
48
+
49
+ it('returns undefined for missing / empty input', () => {
50
+ expect(shortenHash(undefined)).toBeUndefined()
51
+ expect(shortenHash('')).toBeUndefined()
52
+ expect(shortenHash('sha256:')).toBeUndefined()
53
+ })
54
+ })
55
+
56
+ describe('withVersionParam', () => {
57
+ it('appends ?v=<hash8> to a URL with no query', () => {
58
+ expect(
59
+ withVersionParam('/api/addons/pos/frontend/remoteEntry.js', 'sha256:abcdef1234'),
60
+ ).toBe('/api/addons/pos/frontend/remoteEntry.js?v=abcdef12')
61
+ })
62
+
63
+ it('appends &v=<hash8> when other query params exist', () => {
64
+ expect(withVersionParam('/r.js?foo=1&bar=2', 'abcdef1234')).toBe(
65
+ '/r.js?foo=1&bar=2&v=abcdef12',
66
+ )
67
+ })
68
+
69
+ it('replaces a prior v= entry instead of accumulating', () => {
70
+ expect(withVersionParam('/r.js?v=oldhash&foo=1', 'newhash99')).toBe(
71
+ '/r.js?foo=1&v=newhash9',
72
+ )
73
+ })
74
+
75
+ it('preserves the URL fragment', () => {
76
+ expect(withVersionParam('/r.js#section', 'abcdef1234')).toBe(
77
+ '/r.js?v=abcdef12#section',
78
+ )
79
+ })
80
+
81
+ it('is idempotent for the same hash', () => {
82
+ const once = withVersionParam('/r.js', 'abcdef1234')
83
+ const twice = withVersionParam(once, 'abcdef1234')
84
+ expect(twice).toBe(once)
85
+ })
86
+
87
+ it('returns the URL unchanged for falsy hash', () => {
88
+ expect(withVersionParam('/r.js', undefined)).toBe('/r.js')
89
+ expect(withVersionParam('/r.js', '')).toBe('/r.js')
90
+ })
91
+ })
92
+
93
+ describe('clearFederationContainer', () => {
94
+ it('removes the container from window and returns true', () => {
95
+ // Node test environment — globalThis is functionally `window` here.
96
+ // We set a property and expect it to be removed.
97
+ ;(globalThis as Record<string, unknown>).metacore_test_addon = {
98
+ init: () => {},
99
+ get: () => {},
100
+ }
101
+ // The function checks `typeof window === 'undefined'`. In node env
102
+ // that's true → returns false. We only assert this code path doesn't
103
+ // throw and returns a boolean. The window-present case is exercised
104
+ // indirectly through manual integration in the hot-swap docs.
105
+ const result = clearFederationContainer('metacore_test_addon')
106
+ expect(typeof result).toBe('boolean')
107
+ delete (globalThis as Record<string, unknown>).metacore_test_addon
108
+ })
109
+
110
+ it('returns false when called for an unregistered scope in SSR (no window)', () => {
111
+ expect(clearFederationContainer('metacore_never_registered')).toBe(false)
112
+ })
113
+ })
114
+
115
+ describe('applyHotSwapReload', () => {
116
+ it('returns "noop" and does not touch the map when addonKey is missing', async () => {
117
+ const state = makeFakeState()
118
+ const action = await applyHotSwapReload(
119
+ {
120
+ type: 'ADDON_MANIFEST_CHANGED',
121
+ // @ts-expect-error — exercising the guard
122
+ payload: {},
123
+ },
124
+ {},
125
+ state.deps,
126
+ )
127
+ expect(action).toBe('noop')
128
+ expect(state.map).toEqual({})
129
+ })
130
+
131
+ it('default strategy rekeys the map with the short hash and fires onSwap("rekey")', async () => {
132
+ const state = makeFakeState()
133
+ const onSwap = vi.fn()
134
+ const action = await applyHotSwapReload(
135
+ msg('pos', 'sha256:abcdef1234'),
136
+ { onSwap },
137
+ state.deps,
138
+ )
139
+ expect(action).toBe('rekey')
140
+ expect(state.map).toEqual({ pos: 'abcdef12' })
141
+ expect(onSwap).toHaveBeenCalledWith(expect.anything(), 'rekey')
142
+ })
143
+
144
+ it('successive bumps for the same addon update the hash in place', async () => {
145
+ const state = makeFakeState()
146
+ await applyHotSwapReload(msg('pos', 'sha256:abcdef1234'), {}, state.deps)
147
+ await applyHotSwapReload(msg('pos', 'sha256:99999999'), {}, state.deps)
148
+ expect(state.map).toEqual({ pos: '99999999' })
149
+ })
150
+
151
+ it('bumps for different addons accumulate as independent entries', async () => {
152
+ const state = makeFakeState()
153
+ await applyHotSwapReload(msg('pos', 'sha256:aaaaaaaa'), {}, state.deps)
154
+ await applyHotSwapReload(msg('kitchen', 'sha256:bbbbbbbb'), {}, state.deps)
155
+ expect(state.map).toEqual({ pos: 'aaaaaaaa', kitchen: 'bbbbbbbb' })
156
+ })
157
+
158
+ it('cancels rekey when onBeforeReload returns false (sync)', async () => {
159
+ const state = makeFakeState()
160
+ const onBeforeReload = vi.fn().mockReturnValue(false)
161
+ const onSwap = vi.fn()
162
+ const action = await applyHotSwapReload(
163
+ msg('pos'),
164
+ { strategy: 'rekey', onBeforeReload, onSwap },
165
+ state.deps,
166
+ )
167
+ expect(action).toBe('cancelled')
168
+ expect(state.map).toEqual({})
169
+ expect(onSwap).toHaveBeenCalledWith(expect.anything(), 'cancelled')
170
+ })
171
+
172
+ it('proceeds with rekey when async onBeforeReload resolves true', async () => {
173
+ const state = makeFakeState()
174
+ const onBeforeReload = vi.fn().mockResolvedValue(true)
175
+ const action = await applyHotSwapReload(
176
+ msg('pos'),
177
+ { strategy: 'rekey', onBeforeReload },
178
+ state.deps,
179
+ )
180
+ expect(action).toBe('rekey')
181
+ expect(state.map).toEqual({ pos: 'abcdef12' })
182
+ })
183
+
184
+ it('strategy=manual updates the map and reports "manual" without reloading', async () => {
185
+ const state = makeFakeState()
186
+ const onSwap = vi.fn()
187
+ const action = await applyHotSwapReload(
188
+ msg('pos'),
189
+ { strategy: 'manual', onSwap },
190
+ state.deps,
191
+ )
192
+ expect(action).toBe('manual')
193
+ expect(state.map).toEqual({ pos: 'abcdef12' })
194
+ expect(state.reload).not.toHaveBeenCalled()
195
+ expect(onSwap).toHaveBeenCalledWith(expect.anything(), 'manual')
196
+ })
197
+
198
+ it('strategy=page-reload invokes the reload function via microtask', async () => {
199
+ const state = makeFakeState()
200
+ const onSwap = vi.fn()
201
+ const action = await applyHotSwapReload(
202
+ msg('pos'),
203
+ { strategy: 'page-reload', onSwap },
204
+ state.deps,
205
+ )
206
+ expect(action).toBe('page-reload')
207
+ expect(onSwap).toHaveBeenCalledWith(expect.anything(), 'page-reload')
208
+
209
+ // Yield to the microtask queue so the deferred reload fires.
210
+ await Promise.resolve()
211
+ expect(state.reload).toHaveBeenCalledTimes(1)
212
+ })
213
+
214
+ it('strategy=page-reload + onBeforeReload(false) skips the reload', async () => {
215
+ const state = makeFakeState()
216
+ const onBeforeReload = vi.fn().mockReturnValue(false)
217
+ const onSwap = vi.fn()
218
+ const action = await applyHotSwapReload(
219
+ msg('pos'),
220
+ { strategy: 'page-reload', onBeforeReload, onSwap },
221
+ state.deps,
222
+ )
223
+ expect(action).toBe('cancelled')
224
+ await Promise.resolve()
225
+ expect(state.reload).not.toHaveBeenCalled()
226
+ expect(onSwap).toHaveBeenCalledWith(expect.anything(), 'cancelled')
227
+ })
228
+
229
+ it('skips map update when newHash is missing but still rekeys (no-op effect)', async () => {
230
+ const state = makeFakeState()
231
+ const onSwap = vi.fn()
232
+ // Build a message with an explicitly missing newHash. Going through
233
+ // `msg(..., undefined)` would trigger the default-value substitution,
234
+ // so we hand-roll the payload here.
235
+ const action = await applyHotSwapReload(
236
+ {
237
+ type: 'ADDON_MANIFEST_CHANGED',
238
+ payload: { addonKey: 'pos' },
239
+ },
240
+ { onSwap },
241
+ state.deps,
242
+ )
243
+ expect(action).toBe('rekey')
244
+ // Without a hash, we have nothing to write; the map stays empty
245
+ // but onSwap still fires for telemetry.
246
+ expect(state.map).toEqual({})
247
+ expect(onSwap).toHaveBeenCalledWith(expect.anything(), 'rekey')
248
+ })
249
+ })
@@ -0,0 +1,179 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ ADDON_MANIFEST_CHANGED_TYPE,
4
+ wireHotSwapInvalidation,
5
+ type AddonManifestChangedMessage,
6
+ type ManifestHotSwapClient,
7
+ } from '../manifest-hotswap-subscriber'
8
+ import { useMetadataCache, defaultAddonKeyMatcher } from '../metadata-cache'
9
+ import type { TableMetadata } from '../types'
10
+
11
+ // Minimal stub TableMetadata; the cache stores values opaquely so the only
12
+ // requirement is the type-shape compiles.
13
+ const fakeMeta = (label: string): TableMetadata => ({ label }) as unknown as TableMetadata
14
+
15
+ interface FakeClient extends ManifestHotSwapClient {
16
+ emit: (msg: AddonManifestChangedMessage) => void
17
+ subscriberCount: () => number
18
+ }
19
+
20
+ function makeFakeClient(): FakeClient {
21
+ const subs = new Map<string, Set<(m: AddonManifestChangedMessage) => void>>()
22
+ return {
23
+ subscribe(type, handler) {
24
+ let set = subs.get(type)
25
+ if (!set) {
26
+ set = new Set()
27
+ subs.set(type, set)
28
+ }
29
+ set.add(handler)
30
+ return () => {
31
+ subs.get(type)?.delete(handler)
32
+ }
33
+ },
34
+ emit(msg) {
35
+ const set = subs.get(msg.type)
36
+ if (!set) return
37
+ for (const h of set) h(msg)
38
+ },
39
+ subscriberCount() {
40
+ return Array.from(subs.values()).reduce((n, s) => n + s.size, 0)
41
+ },
42
+ }
43
+ }
44
+
45
+ describe('manifest-hotswap-subscriber', () => {
46
+ beforeEach(() => {
47
+ // Reset the zustand cache between tests so each starts from a
48
+ // known-empty state. The store persists by default; resetState
49
+ // here keeps the in-memory copy isolated.
50
+ useMetadataCache.setState({
51
+ cache: {},
52
+ modalCache: {},
53
+ metadataVersion: 'v-old',
54
+ prefetched: true,
55
+ })
56
+ })
57
+
58
+ afterEach(() => {
59
+ vi.restoreAllMocks()
60
+ })
61
+
62
+ it('emits the topic name the kernel/bridge publishes', () => {
63
+ expect(ADDON_MANIFEST_CHANGED_TYPE).toBe('ADDON_MANIFEST_CHANGED')
64
+ })
65
+
66
+ it('subscribes to ADDON_MANIFEST_CHANGED and invalidates the named addon on receipt', () => {
67
+ const cache = useMetadataCache.getState()
68
+ cache.setMetadata('kitchen_display.tickets', fakeMeta('tickets'))
69
+ cache.setMetadata('kitchen_display:settings', fakeMeta('settings'))
70
+ cache.setMetadata('kitchen_display/lines', fakeMeta('lines'))
71
+ cache.setMetadata('pos.cart', fakeMeta('cart'))
72
+ cache.setModalMetadata('kitchen_display.printer', fakeMeta('printer'))
73
+ cache.setModalMetadata('inventory.batch', fakeMeta('batch'))
74
+
75
+ const client = makeFakeClient()
76
+ const unsubscribe = wireHotSwapInvalidation(client)
77
+ expect(client.subscriberCount()).toBe(1)
78
+
79
+ client.emit({
80
+ type: 'ADDON_MANIFEST_CHANGED',
81
+ payload: {
82
+ addonKey: 'kitchen_display',
83
+ oldHash: 'sha256:aaa',
84
+ newHash: 'sha256:bbb',
85
+ version: '1.2.0',
86
+ },
87
+ })
88
+
89
+ const after = useMetadataCache.getState()
90
+ // Three table-cache entries scoped to kitchen_display should be gone.
91
+ expect(after.cache).toEqual({ 'pos.cart': fakeMeta('cart') })
92
+ // One modal entry gone; the inventory entry stays.
93
+ expect(after.modalCache).toEqual({ 'inventory.batch': fakeMeta('batch') })
94
+ // prefetched flipped to false so prefetchAll() can re-run.
95
+ expect(after.prefetched).toBe(false)
96
+
97
+ unsubscribe()
98
+ expect(client.subscriberCount()).toBe(0)
99
+ })
100
+
101
+ it('invokes the onSwap callback with the message and the removed count', () => {
102
+ const cache = useMetadataCache.getState()
103
+ cache.setMetadata('pos.cart', fakeMeta('cart'))
104
+ cache.setMetadata('pos:lines', fakeMeta('lines'))
105
+ cache.setMetadata('inventory.batch', fakeMeta('batch'))
106
+
107
+ const onSwap = vi.fn()
108
+ const client = makeFakeClient()
109
+ wireHotSwapInvalidation(client, { onSwap })
110
+
111
+ const message: AddonManifestChangedMessage = {
112
+ type: 'ADDON_MANIFEST_CHANGED',
113
+ payload: { addonKey: 'pos', newHash: 'sha256:new' },
114
+ }
115
+ client.emit(message)
116
+
117
+ expect(onSwap).toHaveBeenCalledTimes(1)
118
+ expect(onSwap).toHaveBeenCalledWith(message, 2)
119
+ })
120
+
121
+ it('honours a custom matcher when the host uses a different key convention', () => {
122
+ const cache = useMetadataCache.getState()
123
+ cache.setMetadata('addons/pos/cart', fakeMeta('cart'))
124
+ cache.setMetadata('addons/pos/lines', fakeMeta('lines'))
125
+ cache.setMetadata('addons/inv/batch', fakeMeta('batch'))
126
+
127
+ const matcher = (cacheKey: string, addonKey: string) =>
128
+ cacheKey.startsWith(`addons/${addonKey}/`)
129
+
130
+ const client = makeFakeClient()
131
+ wireHotSwapInvalidation(client, { matcher })
132
+
133
+ client.emit({
134
+ type: 'ADDON_MANIFEST_CHANGED',
135
+ payload: { addonKey: 'pos' },
136
+ })
137
+
138
+ const after = useMetadataCache.getState()
139
+ expect(Object.keys(after.cache)).toEqual(['addons/inv/batch'])
140
+ })
141
+
142
+ it('ignores malformed messages (missing addonKey) without throwing', () => {
143
+ const cache = useMetadataCache.getState()
144
+ cache.setMetadata('pos.cart', fakeMeta('cart'))
145
+
146
+ const client = makeFakeClient()
147
+ wireHotSwapInvalidation(client)
148
+
149
+ expect(() =>
150
+ client.emit({
151
+ type: 'ADDON_MANIFEST_CHANGED',
152
+ // @ts-expect-error — exercising the malformed-payload guard
153
+ payload: {},
154
+ }),
155
+ ).not.toThrow()
156
+
157
+ // Cache untouched.
158
+ expect(useMetadataCache.getState().cache).toEqual({
159
+ 'pos.cart': fakeMeta('cart'),
160
+ })
161
+ })
162
+
163
+ it('returns a no-op unsubscribe when the client is undefined', () => {
164
+ const unsubscribe = wireHotSwapInvalidation(undefined)
165
+ expect(typeof unsubscribe).toBe('function')
166
+ expect(() => unsubscribe()).not.toThrow()
167
+ })
168
+
169
+ it('default matcher recognises addonKey, prefix., prefix:, prefix/', () => {
170
+ expect(defaultAddonKeyMatcher('pos', 'pos')).toBe(true)
171
+ expect(defaultAddonKeyMatcher('pos.cart', 'pos')).toBe(true)
172
+ expect(defaultAddonKeyMatcher('pos:lines', 'pos')).toBe(true)
173
+ expect(defaultAddonKeyMatcher('pos/checkout', 'pos')).toBe(true)
174
+ expect(defaultAddonKeyMatcher('possible.x', 'pos')).toBe(false)
175
+ expect(defaultAddonKeyMatcher('inventory.cart', 'pos')).toBe(false)
176
+ expect(defaultAddonKeyMatcher('', 'pos')).toBe(false)
177
+ expect(defaultAddonKeyMatcher('pos.cart', '')).toBe(false)
178
+ })
179
+ })
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import type { ComponentType } from 'react'
3
+ import { slotRegistry } from '../slot'
4
+
5
+ // Dummy components — slot registry stores+sorts, doesn't render.
6
+ const A: ComponentType = () => null
7
+ const B: ComponentType = () => null
8
+ const C: ComponentType = () => null
9
+ const D: ComponentType = () => null
10
+
11
+ describe('slotRegistry priority ordering', () => {
12
+ it('renders higher priority first (DESC) — canonical contract', () => {
13
+ const slot = `test.desc.${Math.random()}`
14
+ const off1 = slotRegistry.register(slot, A, { priority: 1 })
15
+ const off2 = slotRegistry.register(slot, B, { priority: 5 })
16
+ const off3 = slotRegistry.register(slot, C, { priority: 3 })
17
+
18
+ const items = slotRegistry.get(slot)
19
+ expect(items.map((i) => i.priority)).toEqual([5, 3, 1])
20
+ expect(items.map((i) => i.component)).toEqual([B, C, A])
21
+
22
+ off1()
23
+ off2()
24
+ off3()
25
+ })
26
+
27
+ it('treats missing priority as 0', () => {
28
+ const slot = `test.zero.${Math.random()}`
29
+ const off1 = slotRegistry.register(slot, A)
30
+ const off2 = slotRegistry.register(slot, B, { priority: 10 })
31
+ const off3 = slotRegistry.register(slot, C, { priority: -5 })
32
+
33
+ const items = slotRegistry.get(slot)
34
+ expect(items.map((i) => i.component)).toEqual([B, A, C])
35
+
36
+ off1()
37
+ off2()
38
+ off3()
39
+ })
40
+
41
+ it('preserves insertion order on ties', () => {
42
+ const slot = `test.ties.${Math.random()}`
43
+ const off1 = slotRegistry.register(slot, A, { priority: 1 })
44
+ const off2 = slotRegistry.register(slot, B, { priority: 1 })
45
+ const off3 = slotRegistry.register(slot, C, { priority: 2 })
46
+ const off4 = slotRegistry.register(slot, D, { priority: 1 })
47
+
48
+ const items = slotRegistry.get(slot)
49
+ expect(items.map((i) => i.component)).toEqual([C, A, B, D])
50
+
51
+ off1()
52
+ off2()
53
+ off3()
54
+ off4()
55
+ })
56
+
57
+ it('unregister removes the entry and notifies subscribers', () => {
58
+ const slot = `test.unreg.${Math.random()}`
59
+ let notifications = 0
60
+ const unsubscribe = slotRegistry.subscribe(() => { notifications++ })
61
+ const off = slotRegistry.register(slot, A, { priority: 1 })
62
+ expect(slotRegistry.get(slot)).toHaveLength(1)
63
+ expect(notifications).toBeGreaterThanOrEqual(1)
64
+ const before = notifications
65
+ off()
66
+ expect(slotRegistry.get(slot)).toHaveLength(0)
67
+ expect(notifications).toBeGreaterThan(before)
68
+ unsubscribe()
69
+ })
70
+ })
@@ -0,0 +1,82 @@
1
+ // Wasm-client SRI smoke tests — lives in runtime-react because the SDK
2
+ // package does not (yet) carry a vitest setup of its own. We import the
3
+ // helper through the workspace specifier so the test exercises the same
4
+ // public boundary downstream apps see.
5
+ //
6
+ // The SRI logic is decoupled from WebAssembly instantiation by design, so
7
+ // these tests run in plain Node without needing a real `.wasm` module.
8
+
9
+ import { describe, it, expect } from 'vitest'
10
+ import {
11
+ verifyIntegrity,
12
+ WasmIntegrityError,
13
+ } from '@asteby/metacore-sdk'
14
+
15
+ function bytes(input: string): Uint8Array {
16
+ return new TextEncoder().encode(input)
17
+ }
18
+
19
+ async function digestB64(algo: 'SHA-256' | 'SHA-384' | 'SHA-512', input: string) {
20
+ const buf = await crypto.subtle.digest(algo, bytes(input))
21
+ const view = new Uint8Array(buf)
22
+ let bin = ''
23
+ for (let i = 0; i < view.byteLength; i++) bin += String.fromCharCode(view[i]!)
24
+ return btoa(bin)
25
+ }
26
+
27
+ describe('verifyIntegrity', () => {
28
+ it('accepts a matching sha384 digest', async () => {
29
+ const payload = 'hello, metacore wasm world'
30
+ const hash = await digestB64('SHA-384', payload)
31
+ await expect(
32
+ verifyIntegrity(bytes(payload), `sha384-${hash}`),
33
+ ).resolves.toBeUndefined()
34
+ })
35
+
36
+ it('accepts a matching sha256 digest', async () => {
37
+ const payload = '{"hello":"world"}'
38
+ const hash = await digestB64('SHA-256', payload)
39
+ await expect(
40
+ verifyIntegrity(bytes(payload), `sha256-${hash}`),
41
+ ).resolves.toBeUndefined()
42
+ })
43
+
44
+ it('throws WasmIntegrityError on digest mismatch', async () => {
45
+ const payload = 'hello'
46
+ const wrong = await digestB64('SHA-384', 'tampered')
47
+ await expect(
48
+ verifyIntegrity(bytes(payload), `sha384-${wrong}`),
49
+ ).rejects.toBeInstanceOf(WasmIntegrityError)
50
+ })
51
+
52
+ it('passes when ANY space-separated token matches', async () => {
53
+ const payload = 'multi-hash'
54
+ const bad = await digestB64('SHA-384', 'other')
55
+ const good = await digestB64('SHA-256', payload)
56
+ await expect(
57
+ verifyIntegrity(bytes(payload), `sha384-${bad} sha256-${good}`),
58
+ ).resolves.toBeUndefined()
59
+ })
60
+
61
+ it('is a no-op when integrity is the empty string', async () => {
62
+ await expect(verifyIntegrity(bytes('anything'), '')).resolves.toBeUndefined()
63
+ })
64
+
65
+ it('rejects malformed integrity tokens', async () => {
66
+ await expect(
67
+ verifyIntegrity(bytes('x'), 'this-has-no-algo'),
68
+ ).rejects.toBeInstanceOf(WasmIntegrityError)
69
+ })
70
+
71
+ it('rejects unsupported algorithms', async () => {
72
+ await expect(
73
+ verifyIntegrity(bytes('x'), 'md5-deadbeef'),
74
+ ).rejects.toBeInstanceOf(WasmIntegrityError)
75
+ })
76
+
77
+ it('handles length-mismatched digests as a clean mismatch', async () => {
78
+ await expect(
79
+ verifyIntegrity(bytes('x'), 'sha256-short'),
80
+ ).rejects.toBeInstanceOf(WasmIntegrityError)
81
+ })
82
+ })