cursedbelt-core 1.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.
- package/LICENSE +15 -0
- package/dist/client/wsWire.d.ts +49 -0
- package/dist/client/wsWire.js +57 -0
- package/dist/config/appConfig.d.ts +361 -0
- package/dist/config/appConfig.js +308 -0
- package/dist/core/activity/index.d.ts +8 -0
- package/dist/core/activity/index.js +7 -0
- package/dist/core/activity/model.d.ts +115 -0
- package/dist/core/activity/model.js +63 -0
- package/dist/core/analytics/index.d.ts +18 -0
- package/dist/core/analytics/index.js +18 -0
- package/dist/core/analytics/range.d.ts +186 -0
- package/dist/core/analytics/range.js +271 -0
- package/dist/core/analytics/schedule.d.ts +301 -0
- package/dist/core/analytics/schedule.js +372 -0
- package/dist/core/category/index.d.ts +3 -0
- package/dist/core/category/index.js +6 -0
- package/dist/core/category/memoryStore.d.ts +21 -0
- package/dist/core/category/memoryStore.js +97 -0
- package/dist/core/category/tree.d.ts +26 -0
- package/dist/core/category/tree.js +113 -0
- package/dist/core/category/types.d.ts +48 -0
- package/dist/core/category/types.js +8 -0
- package/dist/core/color.d.ts +14 -0
- package/dist/core/color.js +32 -0
- package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
- package/dist/core/colorScheme/colorSchemeStore.js +123 -0
- package/dist/core/colorScheme/index.d.ts +7 -0
- package/dist/core/colorScheme/index.js +7 -0
- package/dist/core/domainOwners.d.ts +19 -0
- package/dist/core/domainOwners.js +15 -0
- package/dist/core/download.d.ts +14 -0
- package/dist/core/download.js +37 -0
- package/dist/core/events/ccEventBus.d.ts +44 -0
- package/dist/core/events/ccEventBus.js +70 -0
- package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
- package/dist/core/file-tree/fileTreeModel.js +312 -0
- package/dist/core/folder-tree/actions.d.ts +84 -0
- package/dist/core/folder-tree/actions.js +119 -0
- package/dist/core/folder-tree/index.d.ts +3 -0
- package/dist/core/folder-tree/index.js +20 -0
- package/dist/core/folder-tree/path.d.ts +76 -0
- package/dist/core/folder-tree/path.js +146 -0
- package/dist/core/folder-tree/tree.d.ts +86 -0
- package/dist/core/folder-tree/tree.js +184 -0
- package/dist/core/layout/index.d.ts +1 -0
- package/dist/core/layout/index.js +7 -0
- package/dist/core/master-lock/index.d.ts +14 -0
- package/dist/core/master-lock/index.js +14 -0
- package/dist/core/master-lock/kdf.d.ts +63 -0
- package/dist/core/master-lock/kdf.js +117 -0
- package/dist/core/master-lock/policy.d.ts +32 -0
- package/dist/core/master-lock/policy.js +52 -0
- package/dist/core/master-lock/presence.d.ts +52 -0
- package/dist/core/master-lock/presence.js +61 -0
- package/dist/core/master-lock/wire.d.ts +139 -0
- package/dist/core/master-lock/wire.js +81 -0
- package/dist/core/media/bgRemoval.d.ts +27 -0
- package/dist/core/media/bgRemoval.js +101 -0
- package/dist/core/media/ffmpeg.d.ts +57 -0
- package/dist/core/media/ffmpeg.js +126 -0
- package/dist/core/media/format.d.ts +20 -0
- package/dist/core/media/format.js +54 -0
- package/dist/core/media/index.d.ts +12 -0
- package/dist/core/media/index.js +18 -0
- package/dist/core/media/mediaItem.d.ts +15 -0
- package/dist/core/media/mediaItem.js +34 -0
- package/dist/core/media/renderPlan.d.ts +63 -0
- package/dist/core/media/renderPlan.js +232 -0
- package/dist/core/media/segment.d.ts +79 -0
- package/dist/core/media/segment.js +247 -0
- package/dist/core/media/size.d.ts +1 -0
- package/dist/core/media/size.js +3 -0
- package/dist/core/media/trim.d.ts +21 -0
- package/dist/core/media/trim.js +40 -0
- package/dist/core/media/types.d.ts +148 -0
- package/dist/core/media/types.js +11 -0
- package/dist/core/media/uploadLimits.d.ts +7 -0
- package/dist/core/media/uploadLimits.js +16 -0
- package/dist/core/media/validate.d.ts +9 -0
- package/dist/core/media/validate.js +121 -0
- package/dist/core/media/videoCuts.d.ts +152 -0
- package/dist/core/media/videoCuts.js +284 -0
- package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
- package/dist/core/navigation/breadcrumbCompat.js +21 -0
- package/dist/core/navigation/coverageTest.d.ts +24 -0
- package/dist/core/navigation/coverageTest.js +48 -0
- package/dist/core/navigation/index.d.ts +6 -0
- package/dist/core/navigation/index.js +10 -0
- package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
- package/dist/core/navigation/migrateNavPreferences.js +63 -0
- package/dist/core/navigation/navigationModel.d.ts +165 -0
- package/dist/core/navigation/navigationModel.js +14 -0
- package/dist/core/navigation/navigationSelectors.d.ts +63 -0
- package/dist/core/navigation/navigationSelectors.js +167 -0
- package/dist/core/navigation/searchCatalogue.d.ts +23 -0
- package/dist/core/navigation/searchCatalogue.js +76 -0
- package/dist/core/net/reconnectSchedule.d.ts +46 -0
- package/dist/core/net/reconnectSchedule.js +38 -0
- package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
- package/dist/core/palette/ShortcutRegistry.js +52 -0
- package/dist/core/palette/fuzzyRank.d.ts +18 -0
- package/dist/core/palette/fuzzyRank.js +73 -0
- package/dist/core/palette/hotkeys.d.ts +28 -0
- package/dist/core/palette/hotkeys.js +118 -0
- package/dist/core/palette/index.d.ts +3 -0
- package/dist/core/palette/index.js +8 -0
- package/dist/core/schema/projection.d.ts +84 -0
- package/dist/core/schema/projection.js +184 -0
- package/dist/core/session/claims.d.ts +145 -0
- package/dist/core/session/claims.js +157 -0
- package/dist/core/sharing/index.d.ts +5 -0
- package/dist/core/sharing/index.js +4 -0
- package/dist/core/sharing/model.d.ts +195 -0
- package/dist/core/sharing/model.js +150 -0
- package/dist/core/site/siteCopy.d.ts +92 -0
- package/dist/core/site/siteCopy.js +91 -0
- package/dist/core/slots.d.ts +22 -0
- package/dist/core/slots.js +31 -0
- package/dist/core/styles/constructableSheet.d.ts +51 -0
- package/dist/core/styles/constructableSheet.js +80 -0
- package/dist/core/tokens/registry.d.ts +119 -0
- package/dist/core/tokens/registry.js +447 -0
- package/dist/core/uiScale/index.d.ts +8 -0
- package/dist/core/uiScale/index.js +8 -0
- package/dist/core/uiScale/scale.d.ts +36 -0
- package/dist/core/uiScale/scale.js +47 -0
- package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
- package/dist/core/uiScale/uiScaleStore.js +63 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
- package/dist/core/wire/index.d.ts +17 -0
- package/dist/core/wire/index.js +17 -0
- package/dist/core/wire/ingress.d.ts +44 -0
- package/dist/core/wire/ingress.js +134 -0
- package/dist/core/wire/marshalWorker.d.ts +70 -0
- package/dist/core/wire/marshalWorker.js +273 -0
- package/dist/core/wire/marshalling.d.ts +122 -0
- package/dist/core/wire/marshalling.js +126 -0
- package/dist/core/wire/wireHub.d.ts +80 -0
- package/dist/core/wire/wireHub.js +116 -0
- package/dist/ctgr/base64url.d.ts +8 -0
- package/dist/ctgr/base64url.js +39 -0
- package/dist/ctgr/chunkAccumulator.d.ts +214 -0
- package/dist/ctgr/chunkAccumulator.js +307 -0
- package/dist/ctgr/chunkStore.d.ts +24 -0
- package/dist/ctgr/chunkStore.js +125 -0
- package/dist/ctgr/codec.d.ts +44 -0
- package/dist/ctgr/codec.js +153 -0
- package/dist/ctgr/compress.d.ts +2 -0
- package/dist/ctgr/compress.js +20 -0
- package/dist/ctgr/crc32.d.ts +2 -0
- package/dist/ctgr/crc32.js +23 -0
- package/dist/ctgr/digest.d.ts +1 -0
- package/dist/ctgr/digest.js +15 -0
- package/dist/ctgr/index.d.ts +9 -0
- package/dist/ctgr/index.js +17 -0
- package/dist/ctgr/types.d.ts +64 -0
- package/dist/ctgr/types.js +21 -0
- package/dist/ctgr/v0compat.d.ts +27 -0
- package/dist/ctgr/v0compat.js +128 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/shared/errors.d.ts +101 -0
- package/dist/shared/errors.js +99 -0
- package/dist/testing/bytes.d.ts +32 -0
- package/dist/testing/bytes.js +33 -0
- package/dist/testing/domInspect.d.ts +59 -0
- package/dist/testing/domInspect.js +115 -0
- package/dist/testing/fakeFetch.d.ts +23 -0
- package/dist/testing/fakeFetch.js +21 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +11 -0
- package/package.json +373 -0
- package/src/client/wsWire.ts +83 -0
- package/src/config/appConfig.spec.ts +158 -0
- package/src/config/appConfig.ts +372 -0
- package/src/core/activity/index.ts +21 -0
- package/src/core/activity/model.ts +160 -0
- package/src/core/analytics/index.ts +18 -0
- package/src/core/analytics/range.spec.ts +280 -0
- package/src/core/analytics/range.ts +383 -0
- package/src/core/analytics/schedule.spec.ts +311 -0
- package/src/core/analytics/schedule.ts +624 -0
- package/src/core/category/category.spec.ts +144 -0
- package/src/core/category/index.ts +19 -0
- package/src/core/category/memoryStore.ts +104 -0
- package/src/core/category/tree.ts +118 -0
- package/src/core/category/types.ts +59 -0
- package/src/core/color.spec.ts +35 -0
- package/src/core/color.ts +35 -0
- package/src/core/colorScheme/colorScheme.spec.ts +127 -0
- package/src/core/colorScheme/colorSchemeStore.ts +184 -0
- package/src/core/colorScheme/index.ts +23 -0
- package/src/core/domainOwners.ts +32 -0
- package/src/core/download.spec.ts +38 -0
- package/src/core/download.ts +35 -0
- package/src/core/events/ccEventBus.spec.ts +90 -0
- package/src/core/events/ccEventBus.ts +120 -0
- package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
- package/src/core/file-tree/fileTreeModel.ts +511 -0
- package/src/core/folder-tree/actions.spec.ts +103 -0
- package/src/core/folder-tree/actions.ts +153 -0
- package/src/core/folder-tree/index.ts +46 -0
- package/src/core/folder-tree/path.spec.ts +113 -0
- package/src/core/folder-tree/path.ts +148 -0
- package/src/core/folder-tree/tree.spec.ts +157 -0
- package/src/core/folder-tree/tree.ts +244 -0
- package/src/core/layout/index.ts +7 -0
- package/src/core/master-lock/index.ts +50 -0
- package/src/core/master-lock/kdf.spec.ts +95 -0
- package/src/core/master-lock/kdf.ts +145 -0
- package/src/core/master-lock/policy.ts +55 -0
- package/src/core/master-lock/presence.spec.ts +73 -0
- package/src/core/master-lock/presence.ts +71 -0
- package/src/core/master-lock/wire.ts +155 -0
- package/src/core/media/bgRemoval.spec.ts +120 -0
- package/src/core/media/bgRemoval.ts +123 -0
- package/src/core/media/ffmpeg.spec.ts +135 -0
- package/src/core/media/ffmpeg.ts +162 -0
- package/src/core/media/format.spec.ts +41 -0
- package/src/core/media/format.ts +58 -0
- package/src/core/media/index.ts +105 -0
- package/src/core/media/mediaItem.spec.ts +46 -0
- package/src/core/media/mediaItem.ts +40 -0
- package/src/core/media/renderPlan.spec.ts +140 -0
- package/src/core/media/renderPlan.ts +318 -0
- package/src/core/media/segment.spec.ts +159 -0
- package/src/core/media/segment.ts +352 -0
- package/src/core/media/size.spec.ts +19 -0
- package/src/core/media/size.ts +3 -0
- package/src/core/media/trim.spec.ts +75 -0
- package/src/core/media/trim.ts +60 -0
- package/src/core/media/types.ts +147 -0
- package/src/core/media/uploadLimits.ts +18 -0
- package/src/core/media/validate.spec.ts +106 -0
- package/src/core/media/validate.ts +131 -0
- package/src/core/media/videoCuts.spec.ts +327 -0
- package/src/core/media/videoCuts.ts +355 -0
- package/src/core/navigation/breadcrumbCompat.ts +44 -0
- package/src/core/navigation/coverageTest.ts +54 -0
- package/src/core/navigation/index.ts +50 -0
- package/src/core/navigation/migrateNavPreferences.ts +119 -0
- package/src/core/navigation/navigation.spec.ts +388 -0
- package/src/core/navigation/navigationModel.ts +185 -0
- package/src/core/navigation/navigationSelectors.ts +206 -0
- package/src/core/navigation/searchCatalogue.ts +89 -0
- package/src/core/net/reconnectSchedule.spec.ts +49 -0
- package/src/core/net/reconnectSchedule.ts +78 -0
- package/src/core/palette/ShortcutRegistry.ts +69 -0
- package/src/core/palette/fuzzyRank.ts +78 -0
- package/src/core/palette/hotkeys.ts +132 -0
- package/src/core/palette/index.ts +22 -0
- package/src/core/palette/palette.spec.ts +160 -0
- package/src/core/schema/projection.spec.ts +122 -0
- package/src/core/schema/projection.ts +225 -0
- package/src/core/session/claims.ts +181 -0
- package/src/core/sharing/index.ts +33 -0
- package/src/core/sharing/model.spec.ts +104 -0
- package/src/core/sharing/model.ts +296 -0
- package/src/core/site/siteCopy.ts +115 -0
- package/src/core/slots.ts +38 -0
- package/src/core/styles/constructableSheet.ts +86 -0
- package/src/core/tokens/registry.spec.ts +160 -0
- package/src/core/tokens/registry.ts +536 -0
- package/src/core/uiScale/index.ts +32 -0
- package/src/core/uiScale/scale.ts +63 -0
- package/src/core/uiScale/uiScale.spec.ts +77 -0
- package/src/core/uiScale/uiScaleStore.ts +100 -0
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
- package/src/core/wire/index.ts +56 -0
- package/src/core/wire/ingress.spec.ts +276 -0
- package/src/core/wire/ingress.ts +182 -0
- package/src/core/wire/marshalWorker.spec.ts +196 -0
- package/src/core/wire/marshalWorker.ts +361 -0
- package/src/core/wire/marshalling.spec.ts +139 -0
- package/src/core/wire/marshalling.ts +238 -0
- package/src/core/wire/wireHub.spec.ts +137 -0
- package/src/core/wire/wireHub.ts +213 -0
- package/src/ctgr/base64url.ts +44 -0
- package/src/ctgr/chunkAccumulator.ts +403 -0
- package/src/ctgr/chunkStore.spec.ts +101 -0
- package/src/ctgr/chunkStore.ts +168 -0
- package/src/ctgr/codec.spec.ts +208 -0
- package/src/ctgr/codec.ts +202 -0
- package/src/ctgr/compress.ts +21 -0
- package/src/ctgr/crc32.ts +23 -0
- package/src/ctgr/digest.ts +14 -0
- package/src/ctgr/index.ts +39 -0
- package/src/ctgr/types.ts +80 -0
- package/src/ctgr/v0compat.spec.ts +73 -0
- package/src/ctgr/v0compat.ts +156 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/index.ts +25 -0
- package/src/leafSubpathsImportNothing.spec.ts +287 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/shared/errors.ts +153 -0
- package/src/skippedTestsAreAnswered.spec.ts +221 -0
- package/src/testTmpRoot.spec.ts +102 -0
- package/src/testing/bytes.ts +38 -0
- package/src/testing/domInspect.spec.ts +79 -0
- package/src/testing/domInspect.ts +127 -0
- package/src/testing/fakeFetch.ts +26 -0
- package/src/testing/index.ts +11 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
clampUiScale,
|
|
4
|
+
createUiScaleStore,
|
|
5
|
+
DEFAULT_UI_SCALE,
|
|
6
|
+
nearestUiScaleLevel,
|
|
7
|
+
stepUiScale,
|
|
8
|
+
type UiScaleStorage,
|
|
9
|
+
uiScaleToFontSize,
|
|
10
|
+
} from './index';
|
|
11
|
+
|
|
12
|
+
describe('ui-scale pure model', () => {
|
|
13
|
+
test('clamps into the supported range; non-finite → default', () => {
|
|
14
|
+
expect(clampUiScale(5)).toBe(2);
|
|
15
|
+
expect(clampUiScale(0.1)).toBe(0.8);
|
|
16
|
+
expect(clampUiScale(Number.NaN)).toBe(DEFAULT_UI_SCALE);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('steps up/down by one notch, clamped', () => {
|
|
20
|
+
expect(stepUiScale(1, 1)).toBeCloseTo(1.1);
|
|
21
|
+
expect(stepUiScale(1, -1)).toBeCloseTo(0.9);
|
|
22
|
+
expect(stepUiScale(2, 1)).toBe(2); // already at max
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('nearestUiScaleLevel snaps a continuous value to a preset', () => {
|
|
26
|
+
expect(nearestUiScaleLevel(1.16).id).toBe('lg'); // 1.15
|
|
27
|
+
expect(nearestUiScaleLevel(1).id).toBe('md');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('uiScaleToFontSize emits a percent string', () => {
|
|
31
|
+
expect(uiScaleToFontSize(1.15)).toBe('115%');
|
|
32
|
+
expect(uiScaleToFontSize(0.9)).toBe('90%');
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('ui-scale store (injected side-effects)', () => {
|
|
37
|
+
const makeStorage = (): UiScaleStorage & { map: Map<string, string> } => {
|
|
38
|
+
const map = new Map<string, string>();
|
|
39
|
+
return {
|
|
40
|
+
map,
|
|
41
|
+
getItem: (k) => map.get(k) ?? null,
|
|
42
|
+
setItem: (k, v) => void map.set(k, v),
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
test('hydrates from storage, persists + applies on set', () => {
|
|
47
|
+
const storage = makeStorage();
|
|
48
|
+
const applied: number[] = [];
|
|
49
|
+
const store = createUiScaleStore({ storage, apply: (s) => applied.push(s) });
|
|
50
|
+
|
|
51
|
+
expect(store.get()).toBe(DEFAULT_UI_SCALE);
|
|
52
|
+
store.set(1.3);
|
|
53
|
+
expect(store.get()).toBe(1.3);
|
|
54
|
+
expect(storage.getItem('cwip.uiScale')).toBe('1.3');
|
|
55
|
+
expect(applied).toEqual([1.3]);
|
|
56
|
+
|
|
57
|
+
store.reset();
|
|
58
|
+
expect(store.get()).toBe(DEFAULT_UI_SCALE);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('init re-applies the persisted scale; subscribers fire on change', () => {
|
|
62
|
+
const storage = makeStorage();
|
|
63
|
+
storage.map.set('cwip.uiScale', '1.5');
|
|
64
|
+
const applied: number[] = [];
|
|
65
|
+
const store = createUiScaleStore({ storage, apply: (s) => applied.push(s) });
|
|
66
|
+
|
|
67
|
+
let notifications = 0;
|
|
68
|
+
const unsub = store.subscribe(() => notifications++);
|
|
69
|
+
store.init();
|
|
70
|
+
expect(applied).toEqual([1.5]);
|
|
71
|
+
|
|
72
|
+
store.step(-1);
|
|
73
|
+
expect(store.get()).toBeCloseTo(1.4);
|
|
74
|
+
expect(notifications).toBe(1);
|
|
75
|
+
unsub();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The UI-scale store: holds the current scale, persists it, applies it to the
|
|
3
|
+
* document root, and notifies subscribers. Split from the React hook so it works
|
|
4
|
+
* before React mounts (call {@link initUiScale} early — like a theme init — to
|
|
5
|
+
* apply the saved size with no flash) and so its logic is testable without a DOM.
|
|
6
|
+
*
|
|
7
|
+
* Side-effects (storage + applying to the page) are injected, so a test drives a
|
|
8
|
+
* real store with fakes; the exported default singleton binds them to
|
|
9
|
+
* `localStorage` + the `<html>` element.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { clampUiScale, DEFAULT_UI_SCALE, stepUiScale, uiScaleToFontSize } from './scale';
|
|
13
|
+
|
|
14
|
+
/** The slice of the Web Storage API the store needs (so a test can fake it). */
|
|
15
|
+
export interface UiScaleStorage {
|
|
16
|
+
getItem(key: string): string | null;
|
|
17
|
+
setItem(key: string, value: string): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UiScaleStore {
|
|
21
|
+
/** Current scale (lazily hydrated from storage on first read). */
|
|
22
|
+
get(): number;
|
|
23
|
+
/** Set, clamp, persist, apply, and notify. */
|
|
24
|
+
set(scale: number): void;
|
|
25
|
+
/** Nudge up (+1) / down (-1) one step. */
|
|
26
|
+
step(dir: 1 | -1): void;
|
|
27
|
+
/** Back to the default size. */
|
|
28
|
+
reset(): void;
|
|
29
|
+
/** Re-apply the persisted scale to the page (call once on app boot). */
|
|
30
|
+
init(): void;
|
|
31
|
+
subscribe(listener: () => void): () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UiScaleStoreOptions {
|
|
35
|
+
storage?: UiScaleStorage | null;
|
|
36
|
+
/** Apply a scale to the page. Default writes the root font-size + a CSS var. */
|
|
37
|
+
apply?: (scale: number) => void;
|
|
38
|
+
/** localStorage key. */
|
|
39
|
+
key?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const UI_SCALE_STORAGE_KEY = 'cwip.uiScale';
|
|
43
|
+
|
|
44
|
+
export function createUiScaleStore(options: UiScaleStoreOptions = {}): UiScaleStore {
|
|
45
|
+
const { storage = null, apply, key = UI_SCALE_STORAGE_KEY } = options;
|
|
46
|
+
const listeners = new Set<() => void>();
|
|
47
|
+
let current: number | undefined; // undefined until first hydrate
|
|
48
|
+
|
|
49
|
+
const hydrate = (): number => {
|
|
50
|
+
if (current !== undefined) return current;
|
|
51
|
+
const raw = storage?.getItem(key);
|
|
52
|
+
current = raw != null ? clampUiScale(Number.parseFloat(raw)) : DEFAULT_UI_SCALE;
|
|
53
|
+
return current;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const commit = (next: number): void => {
|
|
57
|
+
const clamped = clampUiScale(next);
|
|
58
|
+
current = clamped;
|
|
59
|
+
storage?.setItem(key, String(clamped));
|
|
60
|
+
apply?.(clamped);
|
|
61
|
+
for (const l of listeners) l();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
get: hydrate,
|
|
66
|
+
set: commit,
|
|
67
|
+
step: (dir) => commit(stepUiScale(hydrate(), dir)),
|
|
68
|
+
reset: () => commit(DEFAULT_UI_SCALE),
|
|
69
|
+
init: () => apply?.(hydrate()),
|
|
70
|
+
subscribe(listener) {
|
|
71
|
+
listeners.add(listener);
|
|
72
|
+
return () => listeners.delete(listener);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Apply a scale to the document root: scale the rem base + expose a `--cwip-ui-scale`
|
|
78
|
+
* var (for the rare px that wants `calc(... * var(--cwip-ui-scale))`). SSR-safe. */
|
|
79
|
+
export function applyUiScaleToDocument(scale: number): void {
|
|
80
|
+
if (typeof document === 'undefined') return;
|
|
81
|
+
const root = document.documentElement;
|
|
82
|
+
root.style.fontSize = uiScaleToFontSize(scale);
|
|
83
|
+
root.style.setProperty('--cwip-ui-scale', String(clampUiScale(scale)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const defaultStorage: UiScaleStorage | null =
|
|
87
|
+
typeof localStorage !== 'undefined' ? localStorage : null;
|
|
88
|
+
|
|
89
|
+
/** The app-wide singleton bound to localStorage + the page. */
|
|
90
|
+
export const uiScaleStore = createUiScaleStore({
|
|
91
|
+
storage: defaultStorage,
|
|
92
|
+
apply: applyUiScaleToDocument,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const getUiScale = (): number => uiScaleStore.get();
|
|
96
|
+
export const setUiScale = (scale: number): void => uiScaleStore.set(scale);
|
|
97
|
+
export const subscribeUiScale = (listener: () => void): (() => void) =>
|
|
98
|
+
uiScaleStore.subscribe(listener);
|
|
99
|
+
/** Apply the saved UI size on app boot, before React mounts (no flash). */
|
|
100
|
+
export const initUiScale = (): void => uiScaleStore.init();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A REAL marshalling worker entry, used by `marshalWorker.spec.ts` to prove the
|
|
3
|
+
* round-trip over an actual OS thread rather than over an injected fake.
|
|
4
|
+
*
|
|
5
|
+
* This is also the shape every production entry has, so it doubles as the
|
|
6
|
+
* reference an app author copies: construct the marshaller, hand it to
|
|
7
|
+
* `serveMarshalWorker`, done. The host never imports this file — it names it,
|
|
8
|
+
* and the worker imports it.
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { serveMarshalWorker } from '../marshalWorker';
|
|
12
|
+
|
|
13
|
+
const schema = z.object({ rows: z.array(z.object({ id: z.string() })) });
|
|
14
|
+
|
|
15
|
+
await serveMarshalWorker<{ rows: { id: string }[] }, z.infer<typeof schema>>({
|
|
16
|
+
id: 'rows',
|
|
17
|
+
validate: (inbound) => Array.isArray(inbound?.rows),
|
|
18
|
+
transform: (inbound) => {
|
|
19
|
+
// `boom` is the spec's hook for "the transform throws" — a worker must
|
|
20
|
+
// report that failure and stay alive for the next payload.
|
|
21
|
+
if (inbound.rows.some((row) => row.id === 'boom')) throw new Error('transform exploded');
|
|
22
|
+
return { rows: inbound.rows.map((row) => ({ id: `worker:${row.id}` })) };
|
|
23
|
+
},
|
|
24
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cursedbelt-core/wire` — the wire tier's public surface.
|
|
3
|
+
*
|
|
4
|
+
* This entry exists because of Law #1, for the same reason `client/index.ts`
|
|
5
|
+
* gives that reason for `createWireHub`: an ingress route has to be able to
|
|
6
|
+
* IMPORT the marshalling seam, and a worker entry has to be able to import
|
|
7
|
+
* `serveMarshalWorker`. Until the Worker commit (05 §B1) neither symbol had any
|
|
8
|
+
* export path at all — `marshalInbound` was reachable only from inside this
|
|
9
|
+
* package, which is most of why it went three arcs with zero callers.
|
|
10
|
+
*
|
|
11
|
+
* `wireHub` is re-exported from `cursedbelt-cc/client` as well; that entry is where
|
|
12
|
+
* a shell host looks, this one is where the wire tier lives.
|
|
13
|
+
*/
|
|
14
|
+
export {
|
|
15
|
+
type CCWireMarshaller,
|
|
16
|
+
createMarshallerRegistry,
|
|
17
|
+
MARSHALLER_PAYLOAD_TOO_LARGE,
|
|
18
|
+
MARSHALLER_SCHEMA_VIOLATION,
|
|
19
|
+
MARSHALLER_VALIDATION_FAILED,
|
|
20
|
+
MARSHALLER_WORKER_OFFLOAD_BYTES,
|
|
21
|
+
type MarshallerRegistry,
|
|
22
|
+
type MarshalOptions,
|
|
23
|
+
type MarshalWorker,
|
|
24
|
+
marshalInbound,
|
|
25
|
+
payloadBytes,
|
|
26
|
+
requiresWorkerOffload,
|
|
27
|
+
} from './marshalling';
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
createMarshalWorkerClient,
|
|
31
|
+
MARSHAL_WORKER_DEFAULT_TIMEOUT_MS,
|
|
32
|
+
MARSHALLER_WORKER_FAILED,
|
|
33
|
+
MARSHALLER_WORKER_TIMEOUT,
|
|
34
|
+
type MarshalWorkerClient,
|
|
35
|
+
type MarshalWorkerClientOptions,
|
|
36
|
+
type MarshalWorkerRequest,
|
|
37
|
+
type MarshalWorkerResponse,
|
|
38
|
+
serveMarshalWorker,
|
|
39
|
+
} from './marshalWorker';
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
createMarshalledIngress,
|
|
43
|
+
INGRESS_TOKEN_UNKNOWN,
|
|
44
|
+
type MarshalledIngress,
|
|
45
|
+
type MarshalledIngressOptions,
|
|
46
|
+
} from './ingress';
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
createInstanceRuntime,
|
|
50
|
+
createWireHub,
|
|
51
|
+
type InstanceRuntime,
|
|
52
|
+
type InstanceRuntimeOptions,
|
|
53
|
+
type WireEnvelope,
|
|
54
|
+
type WireHub,
|
|
55
|
+
type WireHubOptions,
|
|
56
|
+
} from './wireHub';
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The composable ingress seam (05 §B1, E16/D8).
|
|
3
|
+
*
|
|
4
|
+
* The decision this file records is "no product ingress yet, but the extension
|
|
5
|
+
* point is real". So the last block MOUNTS A WEBHOOK over it, on a real Hono app,
|
|
6
|
+
* over real HTTP — not because a webhook is shipping, but because the owner's
|
|
7
|
+
* condition was *"the architecture is ready to add it later… we can just build
|
|
8
|
+
* on top of it"*, and the only honest way to assert that is to actually build one
|
|
9
|
+
* and watch it work.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it } from 'bun:test';
|
|
12
|
+
import { Hono } from 'hono';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { createMarshalledIngress, INGRESS_TOKEN_UNKNOWN } from './ingress';
|
|
15
|
+
import { type CCWireMarshaller, createMarshallerRegistry } from './marshalling';
|
|
16
|
+
|
|
17
|
+
const schema = z.object({ rows: z.array(z.object({ id: z.string() })) });
|
|
18
|
+
const WORKER_ENTRY = new URL('./__fixtures__/rowsMarshalWorker.ts', import.meta.url);
|
|
19
|
+
|
|
20
|
+
const rowsMarshaller = (over: Partial<CCWireMarshaller> = {}): CCWireMarshaller =>
|
|
21
|
+
({
|
|
22
|
+
id: 'rows',
|
|
23
|
+
tokenId: 'knowledge:records',
|
|
24
|
+
maxPayloadBytes: 1024 * 1024,
|
|
25
|
+
validate: (inbound: unknown) => typeof inbound === 'object' && inbound !== null && 'rows' in inbound,
|
|
26
|
+
transform: (inbound: unknown) => ({ rows: (inbound as { rows: { id: string }[] }).rows }),
|
|
27
|
+
targetZodSchema: schema,
|
|
28
|
+
...over,
|
|
29
|
+
}) as CCWireMarshaller;
|
|
30
|
+
|
|
31
|
+
const registryWith = (marshaller: CCWireMarshaller) => {
|
|
32
|
+
const registry = createMarshallerRegistry();
|
|
33
|
+
registry.register(marshaller.tokenId, marshaller);
|
|
34
|
+
return registry;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe('createMarshalledIngress', () => {
|
|
38
|
+
it('marshals a payload through the token’s registered marshaller', async () => {
|
|
39
|
+
const ingress = createMarshalledIngress({ registry: registryWith(rowsMarshaller()) });
|
|
40
|
+
await expect(ingress.ingest('knowledge:records', { rows: [{ id: 'a' }] })).resolves.toEqual({
|
|
41
|
+
rows: [{ id: 'a' }],
|
|
42
|
+
});
|
|
43
|
+
await ingress.dispose();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('is a 404 for an unregistered token, not a 400', async () => {
|
|
47
|
+
const ingress = createMarshalledIngress({ registry: createMarshallerRegistry() });
|
|
48
|
+
// From outside, "no marshaller" and "no such token" are the same fact.
|
|
49
|
+
// Distinguishing them would make an ingress an enumeration oracle for the
|
|
50
|
+
// app's whole wire surface.
|
|
51
|
+
await expect(ingress.ingest('nope', {})).rejects.toMatchObject({
|
|
52
|
+
code: INGRESS_TOKEN_UNKNOWN,
|
|
53
|
+
status: 404,
|
|
54
|
+
});
|
|
55
|
+
await ingress.dispose();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('carries the marshaller’s own typed refusals through unchanged', async () => {
|
|
59
|
+
const ingress = createMarshalledIngress({
|
|
60
|
+
registry: registryWith(rowsMarshaller({ maxPayloadBytes: 8 })),
|
|
61
|
+
});
|
|
62
|
+
// 413, from the marshaller's declared ceiling — the ingress adds no policy
|
|
63
|
+
// of its own about size, because the marshaller is where that is declared.
|
|
64
|
+
await expect(ingress.ingest('knowledge:records', { rows: [{ id: 'a-very-long-id' }] })).rejects.toMatchObject({
|
|
65
|
+
status: 413,
|
|
66
|
+
});
|
|
67
|
+
await ingress.dispose();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('preserves the marshalling tier’s status codes rather than flattening them', async () => {
|
|
71
|
+
// These codes are the marshalling tier's, unchanged, and the distinction is
|
|
72
|
+
// load-bearing: a payload `validate` refuses is the CALLER's wrong shape
|
|
73
|
+
// (400), while a transform whose OUTPUT violates the token schema means an
|
|
74
|
+
// upstream provider changed shape under us (502 — not our error budget, and
|
|
75
|
+
// it should page a different person). An ingress that mapped both to one
|
|
76
|
+
// code would erase exactly the information an on-call reader needs.
|
|
77
|
+
const rejects = createMarshalledIngress({
|
|
78
|
+
registry: registryWith(rowsMarshaller({ validate: () => false })),
|
|
79
|
+
});
|
|
80
|
+
await expect(rejects.ingest('knowledge:records', { rows: [] })).rejects.toMatchObject({ status: 400 });
|
|
81
|
+
await rejects.dispose();
|
|
82
|
+
|
|
83
|
+
const drifted = createMarshalledIngress({
|
|
84
|
+
registry: registryWith(rowsMarshaller({ transform: () => ({ wrong: true }) })),
|
|
85
|
+
});
|
|
86
|
+
await expect(drifted.ingest('knowledge:records', { rows: [] })).rejects.toMatchObject({ status: 502 });
|
|
87
|
+
await drifted.dispose();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/* ------------------------------------------------------ worker ownership */
|
|
91
|
+
|
|
92
|
+
it('spawns ONE worker per entry and reuses it across payloads', async () => {
|
|
93
|
+
let spawns = 0;
|
|
94
|
+
const ingress = createMarshalledIngress({
|
|
95
|
+
registry: registryWith(rowsMarshaller({ workerEntry: WORKER_ENTRY.pathname })),
|
|
96
|
+
spawn: () => {
|
|
97
|
+
spawns += 1;
|
|
98
|
+
return {
|
|
99
|
+
postMessage() {},
|
|
100
|
+
addEventListener() {},
|
|
101
|
+
removeEventListener() {},
|
|
102
|
+
terminate() {},
|
|
103
|
+
} as never;
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Under the offload threshold, so no worker is needed at all yet.
|
|
108
|
+
await ingress.ingest('knowledge:records', { rows: [{ id: 'a' }] });
|
|
109
|
+
expect(spawns).toBe(0);
|
|
110
|
+
await ingress.dispose();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The reason this module exists at all. `createMarshalWorkerClient` spawns a
|
|
115
|
+
* thread PER CALL, so a route handler that called it per request would leak one
|
|
116
|
+
* OS thread per webhook — a server that gets slower for a week and then dies,
|
|
117
|
+
* with nothing in the marshalling API to warn you. Caching per entry is not an
|
|
118
|
+
* optimization here; it is the whole point of having one owner.
|
|
119
|
+
*/
|
|
120
|
+
it('reuses ONE thread for a marshaller that offloads, and disposes it once', async () => {
|
|
121
|
+
const disposed: string[] = [];
|
|
122
|
+
const ingress = createMarshalledIngress({
|
|
123
|
+
registry: registryWith(
|
|
124
|
+
rowsMarshaller({ workerEntry: WORKER_ENTRY.pathname, maxPayloadBytes: 10 * 1024 * 1024 }),
|
|
125
|
+
),
|
|
126
|
+
workerTimeoutMs: 20_000,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// A payload over the 512KB offload threshold — the real thread runs it.
|
|
130
|
+
const big = { rows: Array.from({ length: 20_000 }, (_unused, index) => ({ id: `row-${index}-padding-padding` })) };
|
|
131
|
+
const first = await ingress.ingest<z.infer<typeof schema>>('knowledge:records', big);
|
|
132
|
+
const second = await ingress.ingest<z.infer<typeof schema>>('knowledge:records', big);
|
|
133
|
+
|
|
134
|
+
// The `worker:` prefix can only have been added by the fixture module, which
|
|
135
|
+
// this process never imported — so both really crossed a thread boundary.
|
|
136
|
+
expect(first.rows[0]?.id.startsWith('worker:')).toBe(true);
|
|
137
|
+
expect(second.rows[0]?.id.startsWith('worker:')).toBe(true);
|
|
138
|
+
|
|
139
|
+
await ingress.dispose();
|
|
140
|
+
// Idempotent: a host that disposes twice (shutdown + a signal handler) must
|
|
141
|
+
// not throw on the second.
|
|
142
|
+
await ingress.dispose();
|
|
143
|
+
expect(disposed).toEqual([]);
|
|
144
|
+
}, 60_000);
|
|
145
|
+
|
|
146
|
+
it('refuses to spawn after dispose rather than leaking a thread past shutdown', async () => {
|
|
147
|
+
const ingress = createMarshalledIngress({
|
|
148
|
+
registry: registryWith(
|
|
149
|
+
rowsMarshaller({ workerEntry: WORKER_ENTRY.pathname, maxPayloadBytes: 10 * 1024 * 1024 }),
|
|
150
|
+
),
|
|
151
|
+
});
|
|
152
|
+
await ingress.dispose();
|
|
153
|
+
const big = { rows: Array.from({ length: 20_000 }, (_unused, index) => ({ id: `row-${index}-padding-padding` })) };
|
|
154
|
+
await expect(ingress.ingest('knowledge:records', big)).rejects.toMatchObject({ code: 'INGRESS_DISPOSED' });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
/* --------------------------------------------------------- request bodies */
|
|
158
|
+
|
|
159
|
+
it('parses a JSON body and hands the marshaller a VALUE', async () => {
|
|
160
|
+
const ingress = createMarshalledIngress({ registry: registryWith(rowsMarshaller()) });
|
|
161
|
+
const request = new Request('http://x/hook', {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: { 'content-type': 'application/json' },
|
|
164
|
+
body: JSON.stringify({ rows: [{ id: 'json' }] }),
|
|
165
|
+
});
|
|
166
|
+
await expect(ingress.ingestRequest('knowledge:records', request)).resolves.toEqual({ rows: [{ id: 'json' }] });
|
|
167
|
+
await ingress.dispose();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('a malformed JSON body is a 400, never a validation failure', async () => {
|
|
171
|
+
const ingress = createMarshalledIngress({ registry: registryWith(rowsMarshaller()) });
|
|
172
|
+
const request = new Request('http://x/hook', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
headers: { 'content-type': 'application/json' },
|
|
175
|
+
body: '{ not json',
|
|
176
|
+
});
|
|
177
|
+
// A body we never managed to DECODE must not read as a payload whose shape
|
|
178
|
+
// we checked and rejected — those are different bugs with different owners.
|
|
179
|
+
await expect(ingress.ingestRequest('knowledge:records', request)).rejects.toMatchObject({
|
|
180
|
+
code: 'INGRESS_BODY_MALFORMED',
|
|
181
|
+
status: 400,
|
|
182
|
+
});
|
|
183
|
+
await ingress.dispose();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('hands a non-JSON body over as BYTES', async () => {
|
|
187
|
+
// A CSV or binary feed wants its bytes; re-encoding them as a string would
|
|
188
|
+
// be lossy for anything that is not UTF-8, which is most binary formats.
|
|
189
|
+
let seen: unknown;
|
|
190
|
+
const ingress = createMarshalledIngress({
|
|
191
|
+
registry: registryWith(
|
|
192
|
+
rowsMarshaller({
|
|
193
|
+
validate: (inbound: unknown) => {
|
|
194
|
+
seen = inbound;
|
|
195
|
+
return true;
|
|
196
|
+
},
|
|
197
|
+
transform: () => ({ rows: [] }),
|
|
198
|
+
}),
|
|
199
|
+
),
|
|
200
|
+
});
|
|
201
|
+
const request = new Request('http://x/hook', {
|
|
202
|
+
method: 'POST',
|
|
203
|
+
headers: { 'content-type': 'text/csv' },
|
|
204
|
+
body: 'id\na\n',
|
|
205
|
+
});
|
|
206
|
+
await ingress.ingestRequest('knowledge:records', request);
|
|
207
|
+
expect(seen).toBeInstanceOf(Uint8Array);
|
|
208
|
+
await ingress.dispose();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* THE assertion behind decision E16. Not "a webhook exists" — one does not, on
|
|
214
|
+
* purpose — but "adding one is a route, not a refactor".
|
|
215
|
+
*/
|
|
216
|
+
describe('adding an ingress later is a ROUTE, not a refactor (E16/D8)', () => {
|
|
217
|
+
it('a webhook endpoint over the seam works end to end, in five lines', async () => {
|
|
218
|
+
const accepted: unknown[] = [];
|
|
219
|
+
const ingress = createMarshalledIngress({ registry: registryWith(rowsMarshaller()) });
|
|
220
|
+
|
|
221
|
+
// ── this is the whole of what a real webhook route would add ────────────
|
|
222
|
+
const app = new Hono();
|
|
223
|
+
app.post('/api/hooks/:token', async (c) => {
|
|
224
|
+
const value = await ingress.ingestRequest(c.req.param('token'), c.req.raw);
|
|
225
|
+
accepted.push(value);
|
|
226
|
+
return c.json({ success: true }, 202);
|
|
227
|
+
});
|
|
228
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
const ok = await app.request('/api/hooks/knowledge:records', {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: { 'content-type': 'application/json' },
|
|
233
|
+
body: JSON.stringify({ rows: [{ id: 'from-the-wire' }] }),
|
|
234
|
+
});
|
|
235
|
+
expect(ok.status).toBe(202);
|
|
236
|
+
expect(accepted).toEqual([{ rows: [{ id: 'from-the-wire' }] }]);
|
|
237
|
+
|
|
238
|
+
// And a CSV import is the SAME route with a different marshaller and a
|
|
239
|
+
// different service call — which is the claim the owner's condition asked
|
|
240
|
+
// to be able to rely on.
|
|
241
|
+
await ingress.dispose();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('the seam’s typed refusals reach the client as their own status codes', async () => {
|
|
245
|
+
const ingress = createMarshalledIngress({
|
|
246
|
+
registry: registryWith(rowsMarshaller({ maxPayloadBytes: 8 })),
|
|
247
|
+
});
|
|
248
|
+
const app = new Hono();
|
|
249
|
+
app.post('/api/hooks/:token', async (c) => {
|
|
250
|
+
try {
|
|
251
|
+
await ingress.ingestRequest(c.req.param('token'), c.req.raw);
|
|
252
|
+
return c.json({ success: true }, 202);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
// A real mount uses the app's error middleware; the point asserted here
|
|
255
|
+
// is that the ingress hands up an error that already KNOWS its status,
|
|
256
|
+
// so a route never has to map marshalling failures itself.
|
|
257
|
+
return c.json({ success: false }, (error as { status?: number }).status as 413);
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const tooBig = await app.request('/api/hooks/knowledge:records', {
|
|
262
|
+
method: 'POST',
|
|
263
|
+
headers: { 'content-type': 'application/json' },
|
|
264
|
+
body: JSON.stringify({ rows: [{ id: 'far too long for the declared ceiling' }] }),
|
|
265
|
+
});
|
|
266
|
+
expect(tooBig.status).toBe(413);
|
|
267
|
+
|
|
268
|
+
const unknown = await app.request('/api/hooks/no-such-token', {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: { 'content-type': 'application/json' },
|
|
271
|
+
body: '{}',
|
|
272
|
+
});
|
|
273
|
+
expect(unknown.status).toBe(404);
|
|
274
|
+
await ingress.dispose();
|
|
275
|
+
});
|
|
276
|
+
});
|