@reventlessdev/reventless-local 3.0.0-alpha.152 → 3.0.0-alpha.154

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,161 @@
1
+ // Tests for DomainGraphQL_Server's plugin=subgraph scoped registry (Phase 6
2
+ // of the merged-API composition plan). Mirrors the AWS model locally: each
3
+ // plugin scope's registrations form a standalone subgraph document that is
4
+ // validated in isolation and composed with graphql-tools merge semantics at
5
+ // start()/composeSchema().
6
+
7
+ open JestGlobals
8
+
9
+ module Server = DomainGraphQL_Server
10
+
11
+ // Execute a query directly against a composed schema (no HTTP server needed).
12
+ type executionResult = {data: Nullable.t<JSON.t>, errors: Nullable.t<array<JSON.t>>}
13
+ @module("graphql")
14
+ external graphqlExecute: {"schema": GraphqlYoga.schema, "source": string} => promise<executionResult> =
15
+ "graphql"
16
+
17
+ let intResolver = (value: int): Server.resolverFn =>
18
+ async (_root, _args, _ctx) =>
19
+ JSON.Encode.object(Dict.fromArray([("x", JSON.Encode.int(value))]))
20
+
21
+ let resolversOf = (entries: array<(string, Server.resolverFn)>) => Dict.fromArray(entries)
22
+
23
+ let sharedTypeSdl = `type SharedThing {\n x: Int\n}`
24
+
25
+ let dataField = (result: executionResult, ~field: string): option<int> =>
26
+ result.data
27
+ ->Nullable.toOption
28
+ ->Option.flatMap(JSON.Decode.object)
29
+ ->Option.flatMap(d => d->Dict.get(field))
30
+ ->Option.flatMap(JSON.Decode.object)
31
+ ->Option.flatMap(d => d->Dict.get("x"))
32
+ ->Option.flatMap(JSON.Decode.float)
33
+ ->Option.map(Float.toInt)
34
+
35
+ let exnMessage = (err: JsExn.t): string => JsExn.message(err)->Option.getOr("")
36
+
37
+ beforeEach(() => Server.reset())
38
+ afterAll(() => Server.reset())
39
+
40
+ // ── (a) identical shared-type copies compose; both plugins' fields resolve ──
41
+
42
+ test("two scopes with identical shared-type copies compose and both fields resolve", async () => {
43
+ Server.setScope("PluginA")
44
+ Server.registerTypes(~sdlTypes=[sharedTypeSdl])
45
+ Server.registerQueries(
46
+ ~sdlFields=[" aItem: SharedThing"],
47
+ ~resolvers=resolversOf([("aItem", intResolver(1))]),
48
+ )
49
+ Server.setScope("PluginB")
50
+ Server.registerTypes(~sdlTypes=[sharedTypeSdl])
51
+ Server.registerQueries(
52
+ ~sdlFields=[" bItem: SharedThing"],
53
+ ~resolvers=resolversOf([("bItem", intResolver(2))]),
54
+ )
55
+ Server.resetScope()
56
+
57
+ // Cross-bucket resolver lookup (MCP_Server path) sees both scopes.
58
+ expect(Server.getQueryResolver("aItem")->Option.isSome)->toBe(true)
59
+ expect(Server.getQueryResolver("bItem")->Option.isSome)->toBe(true)
60
+
61
+ let schema = Server.composeSchema()
62
+ let result = await graphqlExecute({"schema": schema, "source": "{ aItem { x } bItem { x } }"})
63
+ expect(result.errors->Nullable.toOption->Option.isNone)->toBe(true)
64
+ expect(dataField(result, ~field="aItem"))->toEqual(Some(1))
65
+ expect(dataField(result, ~field="bItem"))->toEqual(Some(2))
66
+ })
67
+
68
+ // ── (b) standalone-invalid scope fails with plugin-name attribution ─────────
69
+
70
+ testSync("scope referencing an undefined type fails standalone validation naming the plugin", () => {
71
+ // Register under a construction token, then relabel to the plugin name —
72
+ // exercising the Platform.res flow (name only known after construction).
73
+ Server.setScope("plugin-token-1")
74
+ Server.registerQueries(
75
+ ~sdlFields=[" broken: MissingType"],
76
+ ~resolvers=resolversOf([("broken", intResolver(0))]),
77
+ )
78
+ Server.relabelScope(~from="plugin-token-1", ~to_="BrokenPlugin")
79
+ Server.resetScope()
80
+
81
+ switch Server.composeSchema() {
82
+ | _ => Runner.fail("expected composeSchema to throw")
83
+ | exception JsExn(err) => {
84
+ expect(exnMessage(err))->toContain(
85
+ `Plugin "BrokenPlugin" subgraph document is not valid standalone`,
86
+ )
87
+ expect(exnMessage(err))->toContain("MissingType")
88
+ }
89
+ }
90
+ })
91
+
92
+ // ── (c) conflicting same-named type across scopes fails the merge ────────────
93
+
94
+ testSync("two scopes defining the same type name with conflicting fields fail composition", () => {
95
+ Server.setScope("PluginA")
96
+ Server.registerTypes(~sdlTypes=[`type SharedThing {\n x: Int\n}`])
97
+ Server.registerQueries(
98
+ ~sdlFields=[" aItem: SharedThing"],
99
+ ~resolvers=resolversOf([("aItem", intResolver(1))]),
100
+ )
101
+ Server.setScope("PluginB")
102
+ Server.registerTypes(~sdlTypes=[`type SharedThing {\n x: String\n}`])
103
+ Server.registerQueries(
104
+ ~sdlFields=[" bItem: SharedThing"],
105
+ ~resolvers=resolversOf([("bItem", intResolver(2))]),
106
+ )
107
+ Server.resetScope()
108
+
109
+ switch Server.composeSchema() {
110
+ | _ => Runner.fail("expected composeSchema to throw")
111
+ | exception JsExn(err) =>
112
+ expect(exnMessage(err))->toContain("Cross-plugin schema merge failed (mirrors AWS MERGE_FAILED)")
113
+ }
114
+ })
115
+
116
+ // ── (d) reset clears buckets and restores the platform scope ────────────────
117
+
118
+ testSync("reset clears all scope buckets and restores the platform scope", () => {
119
+ Server.setScope("PluginA")
120
+ Server.registerTypes(~sdlTypes=[sharedTypeSdl])
121
+ Server.registerQueries(
122
+ ~sdlFields=[" aItem: SharedThing"],
123
+ ~resolvers=resolversOf([("aItem", intResolver(1))]),
124
+ )
125
+
126
+ Server.reset()
127
+
128
+ expect(Server.currentScope.contents)->toBe("platform")
129
+ expect(Server.getQueryResolver("aItem")->Option.isNone)->toBe(true)
130
+ let d = Server.diagnostics()
131
+ expect(d.typeCount)->toBe(0)
132
+ expect(d.sdlQueryCount)->toBe(0)
133
+ expect(d.resolverQueryCount)->toBe(0)
134
+ // Post-reset registrations land in the platform bucket again.
135
+ Server.registerQueries(
136
+ ~sdlFields=[" pItem: String"],
137
+ ~resolvers=resolversOf([("pItem", intResolver(0))]),
138
+ )
139
+ expect(Server.buildSdl()->String.includes("pItem"))->toBe(true)
140
+ })
141
+
142
+ // ── per-scope shared types keep plugin subgraphs standalone-valid ────────────
143
+
144
+ test("scope missing its own shared-type copy fails standalone even when platform defines it", async () => {
145
+ // The platform bucket defines SharedThing; the plugin bucket references it
146
+ // without carrying its own copy — standalone-invalid, mirroring an AWS
147
+ // source API that leans on another API's types.
148
+ Server.registerTypes(~sdlTypes=[sharedTypeSdl]) // platform scope
149
+ Server.setScope("Leaner")
150
+ Server.registerQueries(
151
+ ~sdlFields=[" leanItem: SharedThing"],
152
+ ~resolvers=resolversOf([("leanItem", intResolver(3))]),
153
+ )
154
+ Server.resetScope()
155
+
156
+ switch Server.composeSchema() {
157
+ | _ => Runner.fail("expected composeSchema to throw")
158
+ | exception JsExn(err) =>
159
+ expect(exnMessage(err))->toContain(`Plugin "Leaner" subgraph document is not valid standalone`)
160
+ }
161
+ })
@@ -0,0 +1,192 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Graphql from "graphql";
4
+ import * as JestGlobals from "@reventlessdev/rescript-jest/src/JestGlobals.res.mjs";
5
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
6
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
9
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
10
+ import * as DomainGraphQL_Server$ReventlessLocal from "../../src/adapter/DomainGraphQL_Server.res.mjs";
11
+
12
+ function intResolver(value) {
13
+ return async (_root, _args, _ctx) => Object.fromEntries([[
14
+ "x",
15
+ value
16
+ ]]);
17
+ }
18
+
19
+ function resolversOf(entries) {
20
+ return Object.fromEntries(entries);
21
+ }
22
+
23
+ let sharedTypeSdl = `type SharedThing {\n x: Int\n}`;
24
+
25
+ function dataField(result, field) {
26
+ return Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Primitive_option.fromNullable(result.data), Stdlib_JSON.Decode.object), d => d[field]), Stdlib_JSON.Decode.object), d => d["x"]), Stdlib_JSON.Decode.float), prim => prim | 0);
27
+ }
28
+
29
+ function exnMessage(err) {
30
+ return Stdlib_Option.getOr(Stdlib_JsExn.message(err), "");
31
+ }
32
+
33
+ globalThis.beforeEach(() => DomainGraphQL_Server$ReventlessLocal.reset());
34
+
35
+ globalThis.afterAll(() => DomainGraphQL_Server$ReventlessLocal.reset());
36
+
37
+ globalThis.test("two scopes with identical shared-type copies compose and both fields resolve", async () => {
38
+ DomainGraphQL_Server$ReventlessLocal.setScope("PluginA");
39
+ DomainGraphQL_Server$ReventlessLocal.registerTypes([sharedTypeSdl]);
40
+ DomainGraphQL_Server$ReventlessLocal.registerQueries([" aItem: SharedThing"], Object.fromEntries([[
41
+ "aItem",
42
+ async (_root, _args, _ctx) => Object.fromEntries([[
43
+ "x",
44
+ 1
45
+ ]])
46
+ ]]));
47
+ DomainGraphQL_Server$ReventlessLocal.setScope("PluginB");
48
+ DomainGraphQL_Server$ReventlessLocal.registerTypes([sharedTypeSdl]);
49
+ DomainGraphQL_Server$ReventlessLocal.registerQueries([" bItem: SharedThing"], Object.fromEntries([[
50
+ "bItem",
51
+ async (_root, _args, _ctx) => Object.fromEntries([[
52
+ "x",
53
+ 2
54
+ ]])
55
+ ]]));
56
+ DomainGraphQL_Server$ReventlessLocal.resetScope();
57
+ globalThis.expect(Stdlib_Option.isSome(DomainGraphQL_Server$ReventlessLocal.getQueryResolver("aItem"))).toBe(true);
58
+ globalThis.expect(Stdlib_Option.isSome(DomainGraphQL_Server$ReventlessLocal.getQueryResolver("bItem"))).toBe(true);
59
+ let schema = DomainGraphQL_Server$ReventlessLocal.composeSchema();
60
+ let result = await Graphql.graphql({
61
+ schema: schema,
62
+ source: "{ aItem { x } bItem { x } }"
63
+ });
64
+ globalThis.expect(Stdlib_Option.isNone(Primitive_option.fromNullable(result.errors))).toBe(true);
65
+ globalThis.expect(dataField(result, "aItem")).toEqual(1);
66
+ globalThis.expect(dataField(result, "bItem")).toEqual(2);
67
+ });
68
+
69
+ globalThis.test("scope referencing an undefined type fails standalone validation naming the plugin", () => {
70
+ DomainGraphQL_Server$ReventlessLocal.setScope("plugin-token-1");
71
+ DomainGraphQL_Server$ReventlessLocal.registerQueries([" broken: MissingType"], Object.fromEntries([[
72
+ "broken",
73
+ async (_root, _args, _ctx) => Object.fromEntries([[
74
+ "x",
75
+ 0
76
+ ]])
77
+ ]]));
78
+ DomainGraphQL_Server$ReventlessLocal.relabelScope("plugin-token-1", "BrokenPlugin");
79
+ DomainGraphQL_Server$ReventlessLocal.resetScope();
80
+ let val;
81
+ try {
82
+ val = DomainGraphQL_Server$ReventlessLocal.composeSchema();
83
+ } catch (raw_err) {
84
+ let err = Primitive_exceptions.internalToException(raw_err);
85
+ if (err.RE_EXN_ID === "JsExn") {
86
+ let err$1 = err._1;
87
+ globalThis.expect(Stdlib_Option.getOr(Stdlib_JsExn.message(err$1), "")).toContain(`Plugin "BrokenPlugin" subgraph document is not valid standalone`);
88
+ globalThis.expect(Stdlib_Option.getOr(Stdlib_JsExn.message(err$1), "")).toContain("MissingType");
89
+ return;
90
+ }
91
+ throw err;
92
+ }
93
+ JestGlobals.Runner.fail("expected composeSchema to throw");
94
+ });
95
+
96
+ globalThis.test("two scopes defining the same type name with conflicting fields fail composition", () => {
97
+ DomainGraphQL_Server$ReventlessLocal.setScope("PluginA");
98
+ DomainGraphQL_Server$ReventlessLocal.registerTypes([`type SharedThing {\n x: Int\n}`]);
99
+ DomainGraphQL_Server$ReventlessLocal.registerQueries([" aItem: SharedThing"], Object.fromEntries([[
100
+ "aItem",
101
+ async (_root, _args, _ctx) => Object.fromEntries([[
102
+ "x",
103
+ 1
104
+ ]])
105
+ ]]));
106
+ DomainGraphQL_Server$ReventlessLocal.setScope("PluginB");
107
+ DomainGraphQL_Server$ReventlessLocal.registerTypes([`type SharedThing {\n x: String\n}`]);
108
+ DomainGraphQL_Server$ReventlessLocal.registerQueries([" bItem: SharedThing"], Object.fromEntries([[
109
+ "bItem",
110
+ async (_root, _args, _ctx) => Object.fromEntries([[
111
+ "x",
112
+ 2
113
+ ]])
114
+ ]]));
115
+ DomainGraphQL_Server$ReventlessLocal.resetScope();
116
+ let val;
117
+ try {
118
+ val = DomainGraphQL_Server$ReventlessLocal.composeSchema();
119
+ } catch (raw_err) {
120
+ let err = Primitive_exceptions.internalToException(raw_err);
121
+ if (err.RE_EXN_ID === "JsExn") {
122
+ globalThis.expect(Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "")).toContain("Cross-plugin schema merge failed (mirrors AWS MERGE_FAILED)");
123
+ return;
124
+ }
125
+ throw err;
126
+ }
127
+ JestGlobals.Runner.fail("expected composeSchema to throw");
128
+ });
129
+
130
+ globalThis.test("reset clears all scope buckets and restores the platform scope", () => {
131
+ DomainGraphQL_Server$ReventlessLocal.setScope("PluginA");
132
+ DomainGraphQL_Server$ReventlessLocal.registerTypes([sharedTypeSdl]);
133
+ DomainGraphQL_Server$ReventlessLocal.registerQueries([" aItem: SharedThing"], Object.fromEntries([[
134
+ "aItem",
135
+ async (_root, _args, _ctx) => Object.fromEntries([[
136
+ "x",
137
+ 1
138
+ ]])
139
+ ]]));
140
+ DomainGraphQL_Server$ReventlessLocal.reset();
141
+ globalThis.expect(DomainGraphQL_Server$ReventlessLocal.currentScope.contents).toBe("platform");
142
+ globalThis.expect(Stdlib_Option.isNone(DomainGraphQL_Server$ReventlessLocal.getQueryResolver("aItem"))).toBe(true);
143
+ let d = DomainGraphQL_Server$ReventlessLocal.diagnostics();
144
+ globalThis.expect(d.typeCount).toBe(0);
145
+ globalThis.expect(d.sdlQueryCount).toBe(0);
146
+ globalThis.expect(d.resolverQueryCount).toBe(0);
147
+ DomainGraphQL_Server$ReventlessLocal.registerQueries([" pItem: String"], Object.fromEntries([[
148
+ "pItem",
149
+ async (_root, _args, _ctx) => Object.fromEntries([[
150
+ "x",
151
+ 0
152
+ ]])
153
+ ]]));
154
+ globalThis.expect(DomainGraphQL_Server$ReventlessLocal.buildSdl().includes("pItem")).toBe(true);
155
+ });
156
+
157
+ globalThis.test("scope missing its own shared-type copy fails standalone even when platform defines it", async () => {
158
+ DomainGraphQL_Server$ReventlessLocal.registerTypes([sharedTypeSdl]);
159
+ DomainGraphQL_Server$ReventlessLocal.setScope("Leaner");
160
+ DomainGraphQL_Server$ReventlessLocal.registerQueries([" leanItem: SharedThing"], Object.fromEntries([[
161
+ "leanItem",
162
+ async (_root, _args, _ctx) => Object.fromEntries([[
163
+ "x",
164
+ 3
165
+ ]])
166
+ ]]));
167
+ DomainGraphQL_Server$ReventlessLocal.resetScope();
168
+ let val;
169
+ try {
170
+ val = DomainGraphQL_Server$ReventlessLocal.composeSchema();
171
+ } catch (raw_err) {
172
+ let err = Primitive_exceptions.internalToException(raw_err);
173
+ if (err.RE_EXN_ID === "JsExn") {
174
+ globalThis.expect(Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "")).toContain(`Plugin "Leaner" subgraph document is not valid standalone`);
175
+ return;
176
+ }
177
+ throw err;
178
+ }
179
+ return JestGlobals.Runner.fail("expected composeSchema to throw");
180
+ });
181
+
182
+ let Server;
183
+
184
+ export {
185
+ Server,
186
+ intResolver,
187
+ resolversOf,
188
+ sharedTypeSdl,
189
+ dataField,
190
+ exnMessage,
191
+ }
192
+ /* Not a pure module */
@@ -1,131 +0,0 @@
1
- // Behavior GWT for the ApiFragmentRegistry SINGLETON AGGREGATE (the platform API-schema
2
- // fragment registry — event-sourced-fragment-registries plan). Unlike the retired slice, the
3
- // aggregate emits a SECOND event ApiSchemaComputed{snapshot} alongside each ApiFragment* fact,
4
- // carrying the whole consistent per-plugin fragment set after the change (the reactive
5
- // SideEffect's trigger). RecordApiFragmentPush changes no fragments, so it emits NO snapshot.
6
- open ReventlessCore
7
- module P = Reventless.Plugin // P.Domain / P.Platform — avoid shadowing apiSchemaFragment's labels
8
-
9
- module Test = ReventlessGwt.Behavior_GWT.MakeFromAggregate(ApiFragmentRegistrySpec, ApiFragmentRegistryBehavior)
10
- open Test
11
- open ApiFragmentRegistrySpec
12
-
13
- let fragment1: Reventless.Plugin.apiSchemaFragment = {
14
- encoded: `{"types":["type Catalog_Product { id: ID! }"],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
15
- protocol: "graphql",
16
- }
17
-
18
- let fragment2: Reventless.Plugin.apiSchemaFragment = {
19
- encoded: `{"types":["type Catalog_Product { id: ID!\\n name: String! }"],"mutations":[],"queries":[],"subscriptions":[],"subscriptionSources":[]}`,
20
- protocol: "graphql",
21
- }
22
-
23
- // A snapshot entry mirrors the behaviour's `snapshotOf` fold (pluginId + the fragment's
24
- // encoded/protocol + its target).
25
- let entry = (~pluginId, ~fragment: Reventless.Plugin.apiSchemaFragment, ~apiTarget): fragmentSnapshotEntry => {
26
- pluginId,
27
- encoded: fragment.encoded,
28
- protocol: fragment.protocol,
29
- apiTarget,
30
- }
31
-
32
- describe("ApiFragmentRegistry aggregate", () => {
33
- test("register on empty state emits ApiFragmentRegistered + ApiSchemaComputed", () =>
34
- givenEvents([])
35
- ->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"}))
36
- ->thenEvents([
37
- ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"}),
38
- ApiSchemaComputed({snapshot: [entry(~pluginId="p1", ~fragment=fragment1, ~apiTarget=P.Domain)]}),
39
- ])
40
- )
41
-
42
- test("re-registering an identical fragment + target is idempotent (no event)", () =>
43
- givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
44
- ->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t1"}))
45
- ->thenNoEvent
46
- )
47
-
48
- test("registering a changed fragment emits ApiFragmentUpdated + ApiSchemaComputed", () =>
49
- givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
50
- ->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment2, apiTarget: P.Domain, at: "t1"}))
51
- ->thenEvents([
52
- ApiFragmentUpdated({
53
- pluginId: "p1",
54
- previousFragment: fragment1,
55
- newFragment: fragment2,
56
- apiTarget: P.Domain,
57
- at: "t1",
58
- }),
59
- ApiSchemaComputed({snapshot: [entry(~pluginId="p1", ~fragment=fragment2, ~apiTarget=P.Domain)]}),
60
- ])
61
- )
62
-
63
- test("retargeting a plugin with an identical fragment emits ApiFragmentUpdated (fields move APIs)", () =>
64
- givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
65
- ->whenCmd(RegisterApiFragment({pluginId: "p1", fragment: fragment1, apiTarget: P.Platform, at: "t1"}))
66
- ->thenEvents([
67
- ApiFragmentUpdated({
68
- pluginId: "p1",
69
- previousFragment: fragment1,
70
- newFragment: fragment1,
71
- apiTarget: P.Platform,
72
- at: "t1",
73
- }),
74
- ApiSchemaComputed({snapshot: [entry(~pluginId="p1", ~fragment=fragment1, ~apiTarget=P.Platform)]}),
75
- ])
76
- )
77
-
78
- test("registering a second plugin carries BOTH plugins in the snapshot (consistent whole-registry fold)", () =>
79
- givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
80
- ->whenCmd(RegisterApiFragment({pluginId: "p2", fragment: fragment2, apiTarget: P.Platform, at: "t1"}))
81
- ->thenEvents([
82
- ApiFragmentRegistered({pluginId: "p2", fragment: fragment2, apiTarget: P.Platform, at: "t1"}),
83
- ApiSchemaComputed({
84
- snapshot: [
85
- entry(~pluginId="p1", ~fragment=fragment1, ~apiTarget=P.Domain),
86
- entry(~pluginId="p2", ~fragment=fragment2, ~apiTarget=P.Platform),
87
- ],
88
- }),
89
- ])
90
- )
91
-
92
- test("deregister emits ApiFragmentDeregistered + an empty-registry ApiSchemaComputed", () =>
93
- givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
94
- ->whenCmd(DeregisterApiFragment({pluginId: "p1"}))
95
- ->thenEvents([ApiFragmentDeregistered({pluginId: "p1"}), ApiSchemaComputed({snapshot: []})])
96
- )
97
-
98
- test("deregistering an absent fragment is idempotent (no event)", () =>
99
- givenEvents([])->whenCmd(DeregisterApiFragment({pluginId: "p1"}))->thenNoEvent
100
- )
101
-
102
- test("recording a push outcome emits ONLY ApiFragmentPushRecorded (no snapshot, no recompute loop)", () =>
103
- givenEvents([ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"})])
104
- ->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: true, message: "", at: "t1"}))
105
- ->thenEvent(ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}))
106
- )
107
-
108
- test("redelivering an identical push record is idempotent (no event)", () =>
109
- givenEvents([
110
- ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"}),
111
- ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}),
112
- ])
113
- ->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: true, message: "", at: "t1"}))
114
- ->thenNoEvent
115
- )
116
-
117
- test("a later distinct push outcome is recorded again", () =>
118
- givenEvents([
119
- ApiFragmentRegistered({pluginId: "p1", fragment: fragment1, apiTarget: P.Domain, at: "t0"}),
120
- ApiFragmentPushRecorded({pluginId: "p1", ok: true, message: "", at: "t1"}),
121
- ])
122
- ->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: false, message: "stitch failed", at: "t2"}))
123
- ->thenEvent(ApiFragmentPushRecorded({pluginId: "p1", ok: false, message: "stitch failed", at: "t2"}))
124
- )
125
-
126
- test("recording a push for an unregistered plugin is dropped (no event)", () =>
127
- givenEvents([])
128
- ->whenCmd(RecordApiFragmentPush({pluginId: "p1", ok: true, message: "", at: "t1"}))
129
- ->thenNoEvent
130
- )
131
- })