@zhin.js/testing 0.0.0-stage → 1.0.2

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -2
  3. package/lib/delivery-capture.d.ts +3 -0
  4. package/lib/delivery-capture.d.ts.map +1 -0
  5. package/lib/delivery-capture.js +54 -0
  6. package/lib/delivery-capture.js.map +1 -0
  7. package/lib/fake-clock.d.ts +36 -0
  8. package/lib/fake-clock.d.ts.map +1 -0
  9. package/lib/fake-clock.js +100 -0
  10. package/lib/fake-clock.js.map +1 -0
  11. package/lib/fake-hosts.d.ts +51 -0
  12. package/lib/fake-hosts.d.ts.map +1 -0
  13. package/lib/fake-hosts.js +113 -0
  14. package/lib/fake-hosts.js.map +1 -0
  15. package/lib/fake-plugin.d.ts +83 -0
  16. package/lib/fake-plugin.d.ts.map +1 -0
  17. package/lib/fake-plugin.js +130 -0
  18. package/lib/fake-plugin.js.map +1 -0
  19. package/lib/fake-runners.d.ts +47 -0
  20. package/lib/fake-runners.d.ts.map +1 -0
  21. package/lib/fake-runners.js +60 -0
  22. package/lib/fake-runners.js.map +1 -0
  23. package/lib/index.d.ts +74 -0
  24. package/lib/index.d.ts.map +1 -0
  25. package/lib/index.js +168 -0
  26. package/lib/index.js.map +1 -0
  27. package/lib/partial.d.ts +7 -0
  28. package/lib/partial.d.ts.map +1 -0
  29. package/lib/partial.js +9 -0
  30. package/lib/partial.js.map +1 -0
  31. package/lib/test-module-runtime.d.ts +5 -0
  32. package/lib/test-module-runtime.d.ts.map +1 -0
  33. package/lib/test-module-runtime.js +34 -0
  34. package/lib/test-module-runtime.js.map +1 -0
  35. package/package.json +50 -3
  36. package/src/delivery-capture.ts +48 -0
  37. package/src/fake-clock.ts +116 -0
  38. package/src/fake-hosts.ts +131 -0
  39. package/src/fake-plugin.ts +171 -0
  40. package/src/fake-runners.ts +117 -0
  41. package/src/index.ts +202 -0
  42. package/src/partial.ts +8 -0
  43. package/src/test-module-runtime.ts +38 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 凉菜
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,116 @@
1
- # Temporary Holding Version
1
+ # @zhin.js/testing
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The kit has two layers: typed **fakes** for fast unit tests (no `as never`, below), and
4
+ `PluginTestKit` for integration tests through the real Plugin Runtime (the rest of this file).
5
+
6
+ ## Fakes for unit tests
7
+
8
+ ```ts
9
+ import { createFakeMessage, fakeOf, fakeResource, runCommand, runPluginSetup } from '@zhin.js/testing';
10
+
11
+ const fake = await runPluginSetup(plugin, {
12
+ config: { target: 'g1' },
13
+ resources: [fakeResource(myToken, myValue)], // typed by the token
14
+ });
15
+ await fake.clock.advance(60_000); // runs due cron jobs, in order
16
+ expect(fake.sent).toHaveLength(1); // recorded outbound calls
17
+ expect(fake.rows('notes')).toEqual([]); // tables the plugin defined
18
+ await fake.dispose();
19
+
20
+ const message = createFakeMessage({ sender: { id: 'u1', roles: ['admin'] } });
21
+ await runCommand(fake, commandDefinition, { input: message, params: {} });
22
+ expect(message.replies).toEqual(['ok']);
23
+ ```
24
+
25
+ - `createFakePlugin(options)` / `runPluginSetup(definition, options)` build a `FakePlugin`: a real
26
+ `Scope`, `DisposeStack` and `GenerationHandoffStack`, plus the real owner-bound
27
+ `databaseHostToken`, `scheduleHostToken` and `outboundHostToken` facades over in-memory hosts
28
+ (`FakeDatabaseRootHost`, `FakeScheduleRootHost` driven by a `FakeClock`, `FakeOutboundRootHost`).
29
+ - `runCommand`, `runMiddleware`, `runHandler` execute convention-directory definitions against the
30
+ fake. `runCommand` calls `execute()` directly: no routing, parameter parsing, `permit` or admission.
31
+ - `fakeOf<T>(partial)` replaces `{ ... } as never` for narrow collaborators and event payloads.
32
+ - The fakes do not run the renderer or outbound middleware; use `PluginTestKit` to verify the send chain.
33
+
34
+ Full guide: `docs/authoring/testing.md`.
35
+
36
+ ## Integration tests with `PluginTestKit`
37
+
38
+ Test a disposable Plugin Runtime application through real module discovery, configuration validation,
39
+ command parsing, permissions, generation admission and the unified outbound chain. Synthetic endpoints
40
+ capture the final negotiated platform payload without a platform SDK.
41
+
42
+ Install the kit as a development dependency. Prepare a separate test application with a real
43
+ `package.json#zhin` manifest, `plugin.ts` and the dependencies/features used by its mounted plugins.
44
+ The kit neither installs packages nor replaces module resolution. Feature providers must be the same
45
+ physical package instance used by the host, as in a normal application.
46
+
47
+ ```ts
48
+ import { PluginTestKit } from '@zhin.js/testing';
49
+
50
+ const kit = await PluginTestKit.create({
51
+ projectRoot: '/absolute/path/to/disposable-test-app',
52
+ endpoints: ['test', 'second'],
53
+ config: { plugins: { suite: { commandNamespace: 'suite' } } },
54
+ });
55
+ try {
56
+ await kit.start();
57
+ const result = await kit.receive({
58
+ endpoint: 'test',
59
+ conversation: { kind: 'group', id: 'test-group' },
60
+ sender: { id: 'test-member', name: 'Alice', roles: ['admin'] },
61
+ content: 'suite checkin',
62
+ });
63
+ console.log(result?.status, kit.deliveries);
64
+ } finally {
65
+ await kit.close();
66
+ }
67
+ ```
68
+
69
+ The example requires the mounted plugin to provide `suite checkin`. Roles are supplied explicitly by
70
+ the fixture; this verifies command admission, not a platform's real role projection. `permissions` is
71
+ the real PermissionHost used by the IM runtime.
72
+
73
+ The test application must contain only trusted test plugins and synthetic endpoints. **This kit is not
74
+ a sandbox:** discovered adapters and `setup()` functions execute normally and can open connections,
75
+ write files or send real messages. Never point it at a production app or include live credentials.
76
+ `emit()` similarly injects an explicit synthetic event; it does not verify an adapter's event contract.
77
+
78
+ ## Operations and lifecycle
79
+
80
+ - `receive()` enters the synthetic Endpoint event sink, then real ingress/middleware/command dispatch.
81
+ - `send()` runs the real renderer, outbound middleware and segment negotiation; terminal captures are
82
+ detached, readonly metadata snapshots in `deliveries`. Node/Web streams remain opaque references: captures do not consume or freeze them, so tests may inspect identity and subsequently read the stream. Byte buffers are copied. `clearDeliveries()` clears captures without reusing message IDs.
83
+ - `reloadSource(absolutePath)` submits a changed file to the real HMR coordinator and reports `reloaded`,
84
+ `unchanged`, `failed` or `restart-required`, with the committed generation. Edit the test app first.
85
+ A process restart requirement latches the native coordinator; close this kit and create a new one.
86
+ - `patchConfig()` uses normal composition/validation. Invalid patches reject and keep the prior generation.
87
+ - `installResources()` installs host-owned public resources, such as a Database Host, Schedule Host or
88
+ a test HTML renderer. The library does not invent storage or a scheduler implementation.
89
+ - `close()` is idempotent and must also run after a failed `start()`. Each kit owns its endpoints,
90
+ runtime, caches and captures; there is no global active fixture.
91
+
92
+ Native Node loading is the default. TypeScript/TSX fixtures require the same process loader as the
93
+ application; for example, run the test entry in Node with `--import tsx`. A runner that owns module
94
+ loading may provide its public `ModuleRuntime` via `modules`. Do not mix Vite-loaded and Node-loaded
95
+ copies of a Feature provider: run native integration fixtures in a child Node process.
96
+
97
+ ## Evidence boundary
98
+
99
+ The repository fixture mounts two real Group Suite instances, uses real Native module loading and
100
+ HMR, and runs the real PluginStateStore over a SQLite compare-exchange test port. It verifies permission
101
+ rejection, invalid configuration rollback, exact endpoint/group isolation, persistent keywords after
102
+ command HMR, plugin HMR and a fresh Node process, welcome messages and HTML rendering/text fallback.
103
+ Its manual clock drives real PluginScheduleHost callbacks and checks retired-generation rejection.
104
+
105
+ SQLite test-state persistence is real; the fixture's statistics/check-in/teaching model port is volatile.
106
+ The HTML renderer is a stub. These tests do **not** prove browser screenshot fidelity, platform
107
+ login, delivery, role mapping or production database portability. Real-platform acceptance remains a
108
+ separate, explicitly authorized test with an adapter account and harmless target conversation.
109
+
110
+ ## SQLite fixture compatibility
111
+
112
+ The kit itself requires Node 22.19+ (its `engines`) and does not require SQLite. Its base native integration test
113
+ runs without importing `node:sqlite`. SQLite-specific repository acceptance requires Node 22.5+;
114
+ Node 22.5–22.12 child processes include `--experimental-sqlite`, and Node 20 explicitly skips those
115
+ optional persistence cases. They never substitute memory storage while claiming durable persistence.
116
+ The selected flags also propagate into the fresh-process restart probe.
@@ -0,0 +1,3 @@
1
+ /** Streams are transport-owned handles: record their identity without draining or freezing them. */
2
+ export declare function captureDeliveryValue<T>(value: T): T;
3
+ //# sourceMappingURL=delivery-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery-capture.d.ts","sourceRoot":"","sources":["../src/delivery-capture.ts"],"names":[],"mappings":"AAEA,oGAAoG;AACpG,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAEnD"}
@@ -0,0 +1,54 @@
1
+ import { Stream } from 'node:stream';
2
+ /** Streams are transport-owned handles: record their identity without draining or freezing them. */
3
+ export function captureDeliveryValue(value) {
4
+ return capture(value, new WeakMap());
5
+ }
6
+ function capture(value, seen) {
7
+ if (value instanceof Stream || (typeof ReadableStream !== 'undefined' && value instanceof ReadableStream))
8
+ return value;
9
+ if (!value || typeof value !== 'object')
10
+ return structuredClone(value);
11
+ if (seen.has(value))
12
+ return seen.get(value);
13
+ if (Buffer.isBuffer(value) || ArrayBuffer.isView(value)) {
14
+ const copy = Buffer.isBuffer(value) ? Buffer.from(value) : structuredClone(value);
15
+ seen.set(value, copy);
16
+ return copy;
17
+ }
18
+ if (Array.isArray(value)) {
19
+ const copy = [];
20
+ seen.set(value, copy);
21
+ for (const item of value)
22
+ copy.push(capture(item, seen));
23
+ return Object.freeze(copy);
24
+ }
25
+ if (value instanceof Map) {
26
+ const copy = new Map();
27
+ seen.set(value, copy);
28
+ for (const [key, item] of value)
29
+ copy.set(capture(key, seen), capture(item, seen));
30
+ return Object.freeze(copy);
31
+ }
32
+ if (value instanceof Set) {
33
+ const copy = new Set();
34
+ seen.set(value, copy);
35
+ for (const item of value)
36
+ copy.add(capture(item, seen));
37
+ return Object.freeze(copy);
38
+ }
39
+ // Native built-ins retain structuredClone semantics. SDK DTO classes use
40
+ // their enumerable data, just as structuredClone does, but may contain streams.
41
+ if (Object.prototype.toString.call(value) !== '[object Object]') {
42
+ const copy = Object.freeze(structuredClone(value));
43
+ seen.set(value, copy);
44
+ return copy;
45
+ }
46
+ const copy = {};
47
+ seen.set(value, copy);
48
+ for (const [key, item] of Object.entries(value))
49
+ Object.defineProperty(copy, key, {
50
+ value: capture(item, seen), enumerable: true, writable: true, configurable: true,
51
+ });
52
+ return Object.freeze(copy);
53
+ }
54
+ //# sourceMappingURL=delivery-capture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery-capture.js","sourceRoot":"","sources":["../src/delivery-capture.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,oGAAoG;AACpG,MAAM,UAAU,oBAAoB,CAAI,KAAQ;IAC9C,OAAO,OAAO,CAAC,KAAK,EAAE,IAAI,OAAO,EAAE,CAAM,CAAC;AAC5C,CAAC;AAED,SAAS,OAAO,CAAC,KAAc,EAAE,IAA8B;IAC7D,IAAI,KAAK,YAAY,MAAM,IAAI,CAAC,OAAO,cAAc,KAAK,WAAW,IAAI,KAAK,YAAY,cAAc,CAAC;QAAE,OAAO,KAAK,CAAC;IACxH,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;IACvE,IAAI,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC5C,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,GAAG,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QAClF,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,GAAc,EAAE,CAAC;QAC3B,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACtB,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QACzD,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IACD,IAAI,KAAK,YAAY,GAAG,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACtB,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,KAAK;YAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QACnF,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IACD,IAAI,KAAK,YAAY,GAAG,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,IAAI,GAAG,EAAE,CAAC;QACvB,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACtB,KAAK,MAAM,IAAI,IAAI,KAAK;YAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;QACxD,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;IACD,yEAAyE;IACzE,gFAAgF;IAChF,IAAI,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,iBAAiB,EAAE,CAAC;QAChE,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC;QACnD,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAA4B,EAAE,CAAC;IACzC,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACtB,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,MAAM,CAAC,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE;YAChF,KAAK,EAAE,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI;SACjF,CAAC,CAAC;IACH,OAAO,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,36 @@
1
+ import type { PluginId, ScheduleHost, ScheduleJobRegistration } from '@zhin.js/plugin-runtime';
2
+ /** Controllable wall clock. It never touches the real `Date` or timers. */
3
+ export declare class FakeClock {
4
+ #private;
5
+ constructor(start?: number | Date);
6
+ now(): number;
7
+ date(): Date;
8
+ /** Jump without firing anything (like setting the system time). */
9
+ set(time: number | Date): void;
10
+ /** Move forward and run every cron job that became due, in time order. */
11
+ advance(ms: number): Promise<void>;
12
+ /** @internal */
13
+ onAdvance(listener: (from: number, to: number) => Promise<void>): () => void;
14
+ }
15
+ /** Six-field cron (`sec min hour day month weekday`, Sunday = 0) evaluated in local time. */
16
+ export declare function parseCron(cron: string): (date: Date) => boolean;
17
+ export interface FakeScheduledJob {
18
+ readonly id: string;
19
+ readonly cron: string;
20
+ readonly description?: string;
21
+ readonly owner?: PluginId;
22
+ }
23
+ /**
24
+ * In-memory process Schedule Host driven by a {@link FakeClock}. The harness wraps it with the real
25
+ * `PluginScheduleHost` to get the exact facade plugins see.
26
+ */
27
+ export declare class FakeScheduleRootHost implements ScheduleHost {
28
+ #private;
29
+ readonly clock: FakeClock;
30
+ constructor(clock: FakeClock);
31
+ register(job: ScheduleJobRegistration): () => void;
32
+ list(): readonly FakeScheduledJob[];
33
+ /** Run a registered job now, by its qualified id; rejects if it is not registered. */
34
+ trigger(id: string): Promise<void>;
35
+ }
36
+ //# sourceMappingURL=fake-clock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-clock.d.ts","sourceRoot":"","sources":["../src/fake-clock.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAE/F,2EAA2E;AAC3E,qBAAa,SAAS;;gBAIR,KAAK,GAAE,MAAM,GAAG,IAAoC;IAIhE,GAAG,IAAI,MAAM;IACb,IAAI,IAAI,IAAI;IAEZ,mEAAmE;IACnE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI;IAE9B,0EAA0E;IACpE,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQxC,gBAAgB;IAChB,SAAS,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI;CAI7E;AAyBD,6FAA6F;AAC7F,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAY/D;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAID;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,YAAY;;IAG3C,QAAQ,CAAC,KAAK,EAAE,SAAS;gBAAhB,KAAK,EAAE,SAAS;IAWrC,QAAQ,CAAC,GAAG,EAAE,uBAAuB,GAAG,MAAM,IAAI;IAMlD,IAAI,IAAI,SAAS,gBAAgB,EAAE;IAKnC,sFAAsF;IAChF,OAAO,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAKzC"}
@@ -0,0 +1,100 @@
1
+ /** Controllable wall clock. It never touches the real `Date` or timers. */
2
+ export class FakeClock {
3
+ #now;
4
+ #listeners = new Set();
5
+ constructor(start = Date.UTC(2026, 0, 1, 0, 0, 0)) {
6
+ this.#now = typeof start === 'number' ? start : start.getTime();
7
+ }
8
+ now() { return this.#now; }
9
+ date() { return new Date(this.#now); }
10
+ /** Jump without firing anything (like setting the system time). */
11
+ set(time) { this.#now = typeof time === 'number' ? time : time.getTime(); }
12
+ /** Move forward and run every cron job that became due, in time order. */
13
+ async advance(ms) {
14
+ if (!(ms >= 0))
15
+ throw new RangeError('FakeClock.advance requires a non-negative duration');
16
+ const from = this.#now;
17
+ const to = from + ms;
18
+ for (const listener of [...this.#listeners])
19
+ await listener(from, to);
20
+ this.#now = to;
21
+ }
22
+ /** @internal */
23
+ onAdvance(listener) {
24
+ this.#listeners.add(listener);
25
+ return () => { this.#listeners.delete(listener); };
26
+ }
27
+ }
28
+ function parseField(text, min, max) {
29
+ const values = new Set();
30
+ for (const part of text.split(',')) {
31
+ const [range = '', stepText] = part.split('/');
32
+ const step = stepText === undefined ? 1 : Number(stepText);
33
+ if (!Number.isInteger(step) || step < 1)
34
+ throw new TypeError(`Invalid cron step: ${part}`);
35
+ let lo = min;
36
+ let hi = max;
37
+ if (range !== '*') {
38
+ const [a, b] = range.split('-');
39
+ lo = Number(a);
40
+ hi = b === undefined ? (stepText === undefined ? lo : max) : Number(b);
41
+ }
42
+ if (!Number.isInteger(lo) || !Number.isInteger(hi) || lo < min || hi > max || lo > hi) {
43
+ throw new TypeError(`Invalid cron field: ${text}`);
44
+ }
45
+ for (let v = lo; v <= hi; v += step)
46
+ values.add(v);
47
+ }
48
+ return values;
49
+ }
50
+ /** Six-field cron (`sec min hour day month weekday`, Sunday = 0) evaluated in local time. */
51
+ export function parseCron(cron) {
52
+ const parts = cron.trim().split(/\s+/);
53
+ if (parts.length !== 6)
54
+ throw new TypeError(`Cron must have six fields: ${cron}`);
55
+ const sec = parseField(parts[0], 0, 59);
56
+ const min = parseField(parts[1], 0, 59);
57
+ const hour = parseField(parts[2], 0, 23);
58
+ const day = parseField(parts[3], 1, 31);
59
+ const month = parseField(parts[4], 1, 12);
60
+ const week = parseField(parts[5], 0, 7);
61
+ return (d) => sec.has(d.getSeconds()) && min.has(d.getMinutes()) && hour.has(d.getHours())
62
+ && day.has(d.getDate()) && month.has(d.getMonth() + 1)
63
+ && (week.has(d.getDay()) || (d.getDay() === 0 && week.has(7)));
64
+ }
65
+ /**
66
+ * In-memory process Schedule Host driven by a {@link FakeClock}. The harness wraps it with the real
67
+ * `PluginScheduleHost` to get the exact facade plugins see.
68
+ */
69
+ export class FakeScheduleRootHost {
70
+ clock;
71
+ #jobs = new Set();
72
+ constructor(clock) {
73
+ this.clock = clock;
74
+ clock.onAdvance(async (from, to) => {
75
+ for (let t = Math.floor(from / 1000) * 1000 + 1000; t <= to; t += 1000) {
76
+ const date = new Date(t);
77
+ for (const job of [...this.#jobs]) {
78
+ if (this.#jobs.has(job) && job.isActive?.() !== false && job.matches(date))
79
+ await job.execute();
80
+ }
81
+ }
82
+ });
83
+ }
84
+ register(job) {
85
+ const entry = Object.freeze({ ...job, matches: parseCron(job.cron) });
86
+ this.#jobs.add(entry);
87
+ return () => { this.#jobs.delete(entry); };
88
+ }
89
+ list() {
90
+ return [...this.#jobs].map(({ id, cron, description, owner }) => ({ id, cron, ...(description === undefined ? {} : { description }), ...(owner === undefined ? {} : { owner }) }));
91
+ }
92
+ /** Run a registered job now, by its qualified id; rejects if it is not registered. */
93
+ async trigger(id) {
94
+ const job = [...this.#jobs].find(item => item.id === id);
95
+ if (!job)
96
+ throw new Error(`No scheduled job: ${id}`);
97
+ await job.execute();
98
+ }
99
+ }
100
+ //# sourceMappingURL=fake-clock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-clock.js","sourceRoot":"","sources":["../src/fake-clock.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ import type { DatabaseHost, DatabaseHostModel, DeliveryTarget, OutboundEditInput, OutboundReactionInput, OutboundRecallInput, OutboundRootHost, OutboundSendInput, OutboundTypingInput, PersistentStatePort, PluginId } from '@zhin.js/plugin-runtime';
2
+ type Row = Record<string, unknown>;
3
+ /**
4
+ * Process-wide in-memory Database Host. Tables appear on `define()` (plugins define in `setup`);
5
+ * `where` is equality-only, there are no SQL semantics. `state` is a real compare-exchange port, so
6
+ * `PluginStateStore` behaves as in production (TTL uses the real `Date.now()`).
7
+ */
8
+ export declare class FakeDatabaseRootHost implements DatabaseHost {
9
+ #private;
10
+ readonly dialect = "memory";
11
+ started: boolean;
12
+ readonly state: PersistentStatePort;
13
+ readonly models: {
14
+ get: (name: string) => DatabaseHostModel | undefined;
15
+ };
16
+ define(name: string, _definition: Record<string, unknown>): void;
17
+ tables(): readonly string[];
18
+ getRawDatabase(): undefined;
19
+ start(): Promise<void>;
20
+ stop(): Promise<void>;
21
+ /** Rows by physical (owner-qualified) table name. */
22
+ rowsOf(physical: string): readonly Row[];
23
+ }
24
+ export interface FakeOutboundCall {
25
+ readonly kind: 'send' | 'sendTarget' | 'addReaction' | 'removeReaction' | 'recall' | 'edit' | 'typing';
26
+ readonly input: unknown;
27
+ /** Present for `sendTarget`: the sending plugin, as bound by the real facade. */
28
+ readonly requester?: PluginId;
29
+ readonly id: string | null;
30
+ }
31
+ /**
32
+ * Records proactive sends. It stands in for the Root outbound port, so the real `PluginOutboundHost`
33
+ * facade (owner binding) still wraps it. It does NOT run the renderer/middleware chain; for that use
34
+ * `PluginTestKit` with a synthetic endpoint.
35
+ */
36
+ export declare class FakeOutboundRootHost implements OutboundRootHost {
37
+ #private;
38
+ readonly calls: FakeOutboundCall[];
39
+ /** Make the next outbound operation reject once. */
40
+ failNext(error?: unknown): void;
41
+ send(input: OutboundSendInput): Promise<string | null>;
42
+ sendTarget(target: DeliveryTarget, content: unknown, requester: PluginId): Promise<string | null>;
43
+ addReaction(input: OutboundReactionInput): Promise<string | null>;
44
+ removeReaction(input: unknown): Promise<void>;
45
+ recall(input: OutboundRecallInput): Promise<void>;
46
+ edit(input: OutboundEditInput): Promise<string | null>;
47
+ typing(input: OutboundTypingInput): Promise<void>;
48
+ runWithView<T>(operation: () => Promise<T>): Promise<T>;
49
+ }
50
+ export {};
51
+ //# sourceMappingURL=fake-hosts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-hosts.d.ts","sourceRoot":"","sources":["../src/fake-hosts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EAAE,iBAAiB,EAAyB,cAAc,EACtE,iBAAiB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,iBAAiB,EAClG,mBAAmB,EAAE,mBAAmB,EAAE,QAAQ,EACnD,MAAM,yBAAyB,CAAC;AAEjC,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AA0DnC;;;;GAIG;AACH,qBAAa,oBAAqB,YAAW,YAAY;;IACvD,QAAQ,CAAC,OAAO,YAAY;IAC5B,OAAO,UAAQ;IACf,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAqB;IAExD,QAAQ,CAAC,MAAM;oBAAiB,MAAM,KAAG,iBAAiB,GAAG,SAAS;MAA6B;IAEnG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAGhE,MAAM,IAAI,SAAS,MAAM,EAAE;IAC3B,cAAc,IAAI,SAAS;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAC3B,qDAAqD;IACrD,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,EAAE;CACzC;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,aAAa,GAAG,gBAAgB,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;IACvG,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,iFAAiF;IACjF,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,YAAW,gBAAgB;;IAC3D,QAAQ,CAAC,KAAK,EAAE,gBAAgB,EAAE,CAAM;IAIxC,oDAAoD;IACpD,QAAQ,CAAC,KAAK,GAAE,OAA4C,GAAG,IAAI;IAanE,IAAI,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IACtD,UAAU,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAGjG,WAAW,CAAC,KAAK,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAC3D,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAC7C,MAAM,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IACvD,IAAI,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;IAChD,MAAM,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IACvD,WAAW,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;CACxD"}
@@ -0,0 +1,113 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ const matches = (row, query) => Object.entries(query).every(([k, v]) => row[k] === v);
3
+ class MemoryModel {
4
+ rows = [];
5
+ select(...fields) {
6
+ let query = {};
7
+ let order;
8
+ let max = Infinity;
9
+ const run = () => {
10
+ let out = this.rows.filter(row => matches(row, query));
11
+ if (order) {
12
+ const { field, direction } = order;
13
+ out = [...out].sort((a, b) => (a[field] < b[field] ? -1
14
+ : a[field] > b[field] ? 1 : 0) * (direction === 'DESC' ? -1 : 1));
15
+ }
16
+ return out.slice(0, max).map(row => fields.length === 0 ? { ...row }
17
+ : Object.fromEntries(fields.map(f => [f, row[f]])));
18
+ };
19
+ const selection = {
20
+ where: (q) => { query = { ...query, ...q }; return selection; },
21
+ orderBy: (field, direction = 'ASC') => { order = { field, direction }; return selection; },
22
+ limit: (count) => { max = count; return selection; },
23
+ then: (ok, bad) => Promise.resolve().then(run).then(ok, bad),
24
+ };
25
+ return selection;
26
+ }
27
+ async insert(row) { this.rows.push({ ...row }); return undefined; }
28
+ delete() {
29
+ return { where: async (query) => {
30
+ for (let i = this.rows.length - 1; i >= 0; i--)
31
+ if (matches(this.rows[i], query))
32
+ this.rows.splice(i, 1);
33
+ } };
34
+ }
35
+ update(patch) {
36
+ return { where: async (query) => { for (const row of this.rows)
37
+ if (matches(row, query))
38
+ Object.assign(row, patch); } };
39
+ }
40
+ async count(where = {}) { return this.rows.filter(row => matches(row, where)).length; }
41
+ }
42
+ class MemoryState {
43
+ #entries = new Map();
44
+ static key(owner, scope, key) { return JSON.stringify([owner, scope, key]); }
45
+ async read(owner, scope, key) {
46
+ return this.#entries.get(MemoryState.key(owner, scope, key));
47
+ }
48
+ async compareExchange(owner, scope, key, revision, value, expiresAt) {
49
+ const id = MemoryState.key(owner, scope, key);
50
+ if (this.#entries.get(id)?.revision !== revision)
51
+ return false;
52
+ if (value === undefined)
53
+ this.#entries.delete(id);
54
+ else
55
+ this.#entries.set(id, { revision: randomUUID(), value: JSON.parse(JSON.stringify(value)), expiresAt });
56
+ return true;
57
+ }
58
+ }
59
+ /**
60
+ * Process-wide in-memory Database Host. Tables appear on `define()` (plugins define in `setup`);
61
+ * `where` is equality-only, there are no SQL semantics. `state` is a real compare-exchange port, so
62
+ * `PluginStateStore` behaves as in production (TTL uses the real `Date.now()`).
63
+ */
64
+ export class FakeDatabaseRootHost {
65
+ dialect = 'memory';
66
+ started = true;
67
+ state = new MemoryState();
68
+ #models = new Map();
69
+ models = { get: (name) => this.#models.get(name) };
70
+ define(name, _definition) {
71
+ if (!this.#models.has(name))
72
+ this.#models.set(name, new MemoryModel());
73
+ }
74
+ tables() { return [...this.#models.keys()]; }
75
+ getRawDatabase() { return undefined; }
76
+ async start() { this.started = true; }
77
+ async stop() { this.started = false; }
78
+ /** Rows by physical (owner-qualified) table name. */
79
+ rowsOf(physical) { return this.#models.get(physical)?.rows ?? []; }
80
+ }
81
+ /**
82
+ * Records proactive sends. It stands in for the Root outbound port, so the real `PluginOutboundHost`
83
+ * facade (owner binding) still wraps it. It does NOT run the renderer/middleware chain; for that use
84
+ * `PluginTestKit` with a synthetic endpoint.
85
+ */
86
+ export class FakeOutboundRootHost {
87
+ calls = [];
88
+ #failure;
89
+ #nextId = 1;
90
+ /** Make the next outbound operation reject once. */
91
+ failNext(error = new Error('fake outbound failure')) { this.#failure = error; }
92
+ #record(kind, input, requester, hasId = true) {
93
+ if (this.#failure !== undefined) {
94
+ const error = this.#failure;
95
+ this.#failure = undefined;
96
+ return Promise.reject(error);
97
+ }
98
+ const id = hasId ? `fake-${this.#nextId++}` : null;
99
+ this.calls.push(Object.freeze({ kind, input, ...(requester === undefined ? {} : { requester }), id }));
100
+ return Promise.resolve(id);
101
+ }
102
+ send(input) { return this.#record('send', input); }
103
+ sendTarget(target, content, requester) {
104
+ return this.#record('sendTarget', { target, content }, requester);
105
+ }
106
+ addReaction(input) { return this.#record('addReaction', input); }
107
+ async removeReaction(input) { await this.#record('removeReaction', input, undefined, false); }
108
+ async recall(input) { await this.#record('recall', input, undefined, false); }
109
+ edit(input) { return this.#record('edit', input); }
110
+ async typing(input) { await this.#record('typing', input, undefined, false); }
111
+ runWithView(operation) { return operation(); }
112
+ }
113
+ //# sourceMappingURL=fake-hosts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-hosts.js","sourceRoot":"","sources":["../src/fake-hosts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ import { DisposeStack, GenerationHandoffStack, PluginDatabaseHost, PluginOutboundHost, PluginScheduleHost, Scope, type FeatureId, type PluginDefinition, type PluginId, type PluginSetupContext, type Token } from '@zhin.js/plugin-runtime';
2
+ import type { CapabilityContext } from '@zhin.js/feature-kit';
3
+ import { FakeClock, FakeScheduleRootHost } from './fake-clock.js';
4
+ import { FakeDatabaseRootHost, FakeOutboundRootHost, type FakeOutboundCall } from './fake-hosts.js';
5
+ /** A typed `token -> value` binding; create with {@link fakeResource}. */
6
+ export interface FakeResource {
7
+ readonly token: Token<unknown>;
8
+ readonly value: unknown;
9
+ }
10
+ /** Bind a value to a token with the token's own type, so tests need no cast. */
11
+ export declare function fakeResource<T>(token: Token<T>, value: T): FakeResource;
12
+ export interface FakePluginOptions<TConfig = unknown> {
13
+ /** Plugin name (also the instance key). Default `test-plugin`. */
14
+ readonly name?: string;
15
+ /** Value returned by `context.config.get()`. Default `{}`. */
16
+ readonly config?: TConfig;
17
+ /** Extra resources to `provide()` on the Scope, e.g. a hand-written fake for a custom token. */
18
+ readonly resources?: readonly FakeResource[];
19
+ /** Install the fake Database / Schedule / Outbound hosts (default: all `true`). */
20
+ readonly hosts?: {
21
+ readonly database?: boolean;
22
+ readonly schedule?: boolean;
23
+ readonly outbound?: boolean;
24
+ };
25
+ /** Start time or an existing clock shared by several fakes. */
26
+ readonly clock?: FakeClock | number | Date;
27
+ /** Feature projections returned by `capabilityContext().project()`. */
28
+ readonly projections?: ReadonlyMap<FeatureId, unknown>;
29
+ readonly generation?: number;
30
+ }
31
+ /** A capability registered through `context.addFeature` / `context.addCommand` / `context.addXxx`. */
32
+ export interface FakeRegisteredFeature {
33
+ readonly kind: string;
34
+ readonly localName: string;
35
+ readonly definition: unknown;
36
+ }
37
+ /**
38
+ * Typed fake of one plugin's runtime surface. Built from the real `Scope`, `DisposeStack`,
39
+ * `GenerationHandoffStack` and the real owner-bound `PluginDatabaseHost` / `PluginScheduleHost` /
40
+ * `PluginOutboundHost` facades, over in-memory process hosts. No `as never` needed in tests.
41
+ */
42
+ export declare class FakePlugin<TConfig = unknown> {
43
+ #private;
44
+ readonly id: PluginId;
45
+ readonly name: string;
46
+ readonly config: TConfig;
47
+ readonly resources: Scope;
48
+ readonly lifecycle: DisposeStack;
49
+ readonly handoff: GenerationHandoffStack;
50
+ readonly clock: FakeClock;
51
+ readonly generation: number;
52
+ readonly projections: ReadonlyMap<FeatureId, unknown>;
53
+ /** Features registered by `setup()` through `addFeature` / `addCommand` / `addMiddleware` ... */
54
+ readonly features: FakeRegisteredFeature[];
55
+ /** Process-level fakes behind the facades. */
56
+ readonly databaseHost: FakeDatabaseRootHost;
57
+ readonly outboundHost: FakeOutboundRootHost;
58
+ readonly scheduleHost: FakeScheduleRootHost;
59
+ /** Facades exactly as `resources.use(token)` returns them. */
60
+ readonly database: PluginDatabaseHost;
61
+ readonly schedule: PluginScheduleHost;
62
+ readonly outbound: PluginOutboundHost;
63
+ constructor(options?: FakePluginOptions<TConfig>);
64
+ /** The `context` argument `setup()` receives. */
65
+ get context(): PluginSetupContext<TConfig>;
66
+ get signal(): AbortSignal;
67
+ /** Messages recorded by the fake outbound host (every kind of call). */
68
+ get sent(): readonly FakeOutboundCall[];
69
+ /** Rows of a table the plugin created with `database.define(name, ...)`. */
70
+ rows(name: string): readonly Record<string, unknown>[];
71
+ /** Run `definition.setup()` against this fake; a returned disposer joins `lifecycle`. */
72
+ setup(definition: PluginDefinition<TConfig>): Promise<void>;
73
+ /** Tokens declared in `requires` that this fake did not provide. */
74
+ missingRequirements(definition: PluginDefinition<TConfig>): readonly Token<unknown>[];
75
+ /** A `CapabilityContext` for running commands / middleware / handlers against this fake. */
76
+ capabilityContext(): CapabilityContext<TConfig>;
77
+ /** Abort `signal` and run every disposer (reverse order). */
78
+ dispose(): Promise<void>;
79
+ }
80
+ export declare function createFakePlugin<TConfig = unknown>(options?: FakePluginOptions<TConfig>): FakePlugin<TConfig>;
81
+ /** Create a fake and run `definition.setup()` in one step. Call `dispose()` when done. */
82
+ export declare function runPluginSetup<TConfig = unknown>(definition: PluginDefinition<TConfig>, options?: Omit<FakePluginOptions<TConfig>, 'name'>): Promise<FakePlugin<TConfig>>;
83
+ //# sourceMappingURL=fake-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fake-plugin.d.ts","sourceRoot":"","sources":["../src/fake-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EAAE,sBAAsB,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,kBAAkB,EAChG,KAAK,EACS,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,KAAK,QAAQ,EAClE,KAAK,kBAAkB,EAAE,KAAK,KAAK,EACpC,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,SAAS,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEpG,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAAG,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CAAE;AAEzF,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,YAAY,CAEvE;AAED,MAAM,WAAW,iBAAiB,CAAC,OAAO,GAAG,OAAO;IAClD,kEAAkE;IAClE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,gGAAgG;IAChG,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAC7C,mFAAmF;IACnF,QAAQ,CAAC,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC3G,+DAA+D;IAC/D,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAAC;IAC3C,uEAAuE;IACvE,QAAQ,CAAC,WAAW,CAAC,EAAE,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACvD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,sGAAsG;AACtG,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;CAC9B;AAED;;;;GAIG;AACH,qBAAa,UAAU,CAAC,OAAO,GAAG,OAAO;;IACvC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAkB;IACvC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;IAC1B,QAAQ,CAAC,SAAS,eAAsB;IACxC,QAAQ,CAAC,OAAO,yBAAgC;IAChD,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACtD,iGAAiG;IACjG,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,EAAE,CAAM;IAChD,8CAA8C;IAC9C,QAAQ,CAAC,YAAY,uBAA8B;IACnD,QAAQ,CAAC,YAAY,uBAA8B;IACnD,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,CAAC;gBAI1B,OAAO,GAAE,iBAAiB,CAAC,OAAO,CAAM;IAmBpD,iDAAiD;IACjD,IAAI,OAAO,IAAI,kBAAkB,CAAC,OAAO,CAAC,CAA0B;IACpE,IAAI,MAAM,IAAI,WAAW,CAA+B;IAExD,wEAAwE;IACxE,IAAI,IAAI,IAAI,SAAS,gBAAgB,EAAE,CAAoC;IAE3E,4EAA4E;IAC5E,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE;IAItD,yFAAyF;IACnF,KAAK,CAAC,UAAU,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjE,oEAAoE;IACpE,mBAAmB,CAAC,UAAU,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,SAAS,KAAK,CAAC,OAAO,CAAC,EAAE;IAIrF,4FAA4F;IAC5F,iBAAiB,IAAI,iBAAiB,CAAC,OAAO,CAAC;IAe/C,6DAA6D;IACvD,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CA4B/B;AAED,wBAAgB,gBAAgB,CAAC,OAAO,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,UAAU,CAAC,OAAO,CAAC,CAE7G;AAED,0FAA0F;AAC1F,wBAAsB,cAAc,CAAC,OAAO,GAAG,OAAO,EACpD,UAAU,EAAE,gBAAgB,CAAC,OAAO,CAAC,EACrC,OAAO,CAAC,EAAE,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC,GACjD,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAI9B"}