@reventlessdev/reventless-local 3.0.0-alpha.318 → 3.0.0-alpha.320
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/CHANGELOG.md +19 -0
- package/package.json +11 -11
- package/src/LocalPlatformRegistry.res +7 -1
- package/src/LocalPlatformRegistry.res.mjs +7 -5
- package/src/Platform.res +16 -0
- package/src/Platform.res.mjs +16 -2
- package/src/adapter/Api/LocalEvents_Server.res +34 -3
- package/src/adapter/Api/LocalEvents_Server.res.mjs +31 -3
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res +24 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogEntry.res.mjs +28 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_InMemory.res +26 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_InMemory.res.mjs +35 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res +61 -0
- package/src/adapter/CommandOutcomeLog/CommandOutcomeLogStorage_Sqlite.res.mjs +75 -0
- package/src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res +88 -0
- package/src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res.mjs +112 -0
- package/src/adapter/DomainGraphQL_Server.res +2 -1
- package/src/adapter/DomainGraphQL_Server.res.mjs +3 -1
- package/src/adapter/LocalFeed.res +16 -2
- package/src/adapter/LocalFeed.res.mjs +19 -2
- package/src/adapter/LocalIdleExit.res +71 -0
- package/src/adapter/LocalIdleExit.res.mjs +96 -0
- package/src/adapter/LocalMetrics.res +29 -0
- package/src/adapter/LocalMetrics.res.mjs +61 -0
- package/src/adapter/LocalPlatformObserve.res +129 -5
- package/src/adapter/LocalPlatformObserve.res.mjs +144 -2
- package/src/adapter/LocalRequestLog.res +62 -0
- package/src/adapter/LocalRequestLog.res.mjs +80 -0
- package/src/adapter/LocalTapLines.res +2 -0
- package/src/adapter/LocalTapLines.res.mjs +4 -1
- package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +11 -3
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +10 -3
- package/src/adapter/ViewChangeLog/LocalViewChangeLog.res +95 -0
- package/src/adapter/ViewChangeLog/LocalViewChangeLog.res.mjs +116 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogEntry.res +33 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogEntry.res.mjs +30 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_InMemory.res +28 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_InMemory.res.mjs +40 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_Sqlite.res +76 -0
- package/src/adapter/ViewChangeLog/ViewChangeLogStorage_Sqlite.res.mjs +88 -0
- package/src/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/LocalAutomationSchedules.res.mjs +1 -1
- package/src/components/LocalAutomationSweep.res.mjs +1 -1
- package/tests/LocalPlatformRegistryTest.res +17 -0
- package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
- package/tests/LocalPlatformStartTest.res.mjs +1 -1
- package/tests/adapter/GraphQL_SchemaInspectorTest.res +3 -2
- package/tests/adapter/GraphQL_SchemaInspectorTest.res.mjs +1 -1
- package/tests/adapter/LocalEvents_ServerTest.res +11 -0
- package/tests/adapter/LocalEvents_ServerTest.res.mjs +7 -1
- package/tests/adapter/LocalIdleExitTest.res +57 -0
- package/tests/adapter/LocalIdleExitTest.res.mjs +62 -0
- package/tests/adapter/LocalPlatformObserveTest.res +183 -0
- package/tests/adapter/LocalPlatformObserveTest.res.mjs +181 -0
- package/tests/adapter/LocalRequestLogTest.res +46 -0
- package/tests/adapter/LocalRequestLogTest.res.mjs +61 -0
- package/tests/adapter/LocalTapLinesTest.res +25 -0
- package/tests/adapter/LocalTapLinesTest.res.mjs +35 -3
|
@@ -32,6 +32,17 @@ let descriptor = (~id: string): JSON.t =>
|
|
|
32
32
|
describe("LocalEvents_Server", () => {
|
|
33
33
|
beforeEach(() => LocalEvents_Server.resetConnections())
|
|
34
34
|
|
|
35
|
+
testSync("a subscription is told by its channel and who subscribed", () => {
|
|
36
|
+
let conn = LocalEvents_Server.addConnection(~send=_ => (), ~user="alice")
|
|
37
|
+
expect(
|
|
38
|
+
LocalEvents_Server.subscriptionLine(conn, ~verb="subscribe", ~channel="/default/orders/*"),
|
|
39
|
+
)->toBe("subscribe /default/orders/* by alice")
|
|
40
|
+
let anonymous = LocalEvents_Server.addConnection(~send=_ => ())
|
|
41
|
+
expect(
|
|
42
|
+
LocalEvents_Server.subscriptionLine(anonymous, ~verb="unsubscribe", ~channel="/default/x"),
|
|
43
|
+
)->toBe("unsubscribe /default/x")
|
|
44
|
+
})
|
|
45
|
+
|
|
35
46
|
describe("channelMatches", () => {
|
|
36
47
|
testSync(
|
|
37
48
|
"exact match",
|
|
@@ -17,7 +17,7 @@ function makeConn() {
|
|
|
17
17
|
sentFrames.contents = [];
|
|
18
18
|
return LocalEvents_Server$ReventlessLocal.addConnection(f => {
|
|
19
19
|
sentFrames.contents.push(f);
|
|
20
|
-
});
|
|
20
|
+
}, undefined);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function parseFrame(s) {
|
|
@@ -47,6 +47,12 @@ function descriptor(id) {
|
|
|
47
47
|
|
|
48
48
|
globalThis.describe("LocalEvents_Server", () => {
|
|
49
49
|
globalThis.beforeEach(() => LocalEvents_Server$ReventlessLocal.resetConnections());
|
|
50
|
+
globalThis.test("a subscription is told by its channel and who subscribed", () => {
|
|
51
|
+
let conn = LocalEvents_Server$ReventlessLocal.addConnection(param => {}, "alice");
|
|
52
|
+
globalThis.expect(LocalEvents_Server$ReventlessLocal.subscriptionLine(conn, "subscribe", "/default/orders/*")).toBe("subscribe /default/orders/* by alice");
|
|
53
|
+
let anonymous = LocalEvents_Server$ReventlessLocal.addConnection(param => {}, undefined);
|
|
54
|
+
globalThis.expect(LocalEvents_Server$ReventlessLocal.subscriptionLine(anonymous, "unsubscribe", "/default/x")).toBe("unsubscribe /default/x");
|
|
55
|
+
});
|
|
50
56
|
globalThis.describe("channelMatches", () => {
|
|
51
57
|
globalThis.test("exact match", () => {
|
|
52
58
|
globalThis.expect(LocalEvents_Server$ReventlessLocal.channelMatches("/default/Product/p-1", "/default/Product/p-1")).toBe(true);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// A platform left running for an editor stops once nothing has watched it for its grace, and keeps
|
|
2
|
+
// running while something does.
|
|
3
|
+
|
|
4
|
+
open JestGlobals
|
|
5
|
+
|
|
6
|
+
let wait = ms =>
|
|
7
|
+
Promise.make((resolve, _) => {
|
|
8
|
+
let _ = setTimeout(() => resolve(), ms)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe("LocalIdleExit", () => {
|
|
12
|
+
let stopped = ref(0)
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
LocalIdleExit.reset()
|
|
15
|
+
stopped := 0
|
|
16
|
+
LocalIdleExit.onIdle := (() => stopped := stopped.contents + 1)
|
|
17
|
+
})
|
|
18
|
+
afterEach(() => LocalIdleExit.reset())
|
|
19
|
+
|
|
20
|
+
testSync("reads its grace in seconds, and nothing else turns it on", () => {
|
|
21
|
+
expect(LocalIdleExit.graceOf(Some("60")))->toEqual(Some(60_000))
|
|
22
|
+
expect(LocalIdleExit.graceOf(Some("0")))->toEqual(None)
|
|
23
|
+
expect(LocalIdleExit.graceOf(Some("soon")))->toEqual(None)
|
|
24
|
+
expect(LocalIdleExit.graceOf(None))->toEqual(None)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
testPromise("stops when nothing connects within its grace", async () => {
|
|
28
|
+
LocalIdleExit.start(~env=dict{"REVENTLESS_IDLE_EXIT": "1"})
|
|
29
|
+
await wait(1200)
|
|
30
|
+
expect(stopped.contents)->toBe(1)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
testPromise(
|
|
34
|
+
"keeps running while watched, and a watcher back in time keeps it running",
|
|
35
|
+
async () => {
|
|
36
|
+
LocalIdleExit.start(~env=dict{"REVENTLESS_IDLE_EXIT": "1"})
|
|
37
|
+
LocalIdleExit.connected()
|
|
38
|
+
await wait(1200)
|
|
39
|
+
expect(stopped.contents)->toBe(0)
|
|
40
|
+
// A reload: the watcher goes and comes back within the grace.
|
|
41
|
+
LocalIdleExit.disconnected()
|
|
42
|
+
await wait(500)
|
|
43
|
+
LocalIdleExit.connected()
|
|
44
|
+
await wait(800)
|
|
45
|
+
expect(stopped.contents)->toBe(0)
|
|
46
|
+
LocalIdleExit.disconnected()
|
|
47
|
+
await wait(1200)
|
|
48
|
+
expect(stopped.contents)->toBe(1)
|
|
49
|
+
},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
testPromise("does nothing without the variable", async () => {
|
|
53
|
+
LocalIdleExit.start(~env=dict{})
|
|
54
|
+
await wait(1200)
|
|
55
|
+
expect(stopped.contents)->toBe(0)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as LocalIdleExit$ReventlessLocal from "../../src/adapter/LocalIdleExit.res.mjs";
|
|
4
|
+
|
|
5
|
+
function wait(ms) {
|
|
6
|
+
return new Promise((resolve, param) => {
|
|
7
|
+
setTimeout(() => resolve(), ms);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
globalThis.describe("LocalIdleExit", () => {
|
|
12
|
+
let stopped = {
|
|
13
|
+
contents: 0
|
|
14
|
+
};
|
|
15
|
+
globalThis.beforeEach(() => {
|
|
16
|
+
LocalIdleExit$ReventlessLocal.reset();
|
|
17
|
+
stopped.contents = 0;
|
|
18
|
+
LocalIdleExit$ReventlessLocal.onIdle.contents = () => {
|
|
19
|
+
stopped.contents = stopped.contents + 1 | 0;
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
globalThis.afterEach(() => LocalIdleExit$ReventlessLocal.reset());
|
|
23
|
+
globalThis.test("reads its grace in seconds, and nothing else turns it on", () => {
|
|
24
|
+
globalThis.expect(LocalIdleExit$ReventlessLocal.graceOf("60")).toEqual(60000);
|
|
25
|
+
globalThis.expect(LocalIdleExit$ReventlessLocal.graceOf("0")).toEqual(undefined);
|
|
26
|
+
globalThis.expect(LocalIdleExit$ReventlessLocal.graceOf("soon")).toEqual(undefined);
|
|
27
|
+
globalThis.expect(LocalIdleExit$ReventlessLocal.graceOf(undefined)).toEqual(undefined);
|
|
28
|
+
});
|
|
29
|
+
globalThis.test("stops when nothing connects within its grace", async () => {
|
|
30
|
+
LocalIdleExit$ReventlessLocal.start({
|
|
31
|
+
REVENTLESS_IDLE_EXIT: "1"
|
|
32
|
+
});
|
|
33
|
+
await wait(1200);
|
|
34
|
+
globalThis.expect(stopped.contents).toBe(1);
|
|
35
|
+
});
|
|
36
|
+
globalThis.test("keeps running while watched, and a watcher back in time keeps it running", async () => {
|
|
37
|
+
LocalIdleExit$ReventlessLocal.start({
|
|
38
|
+
REVENTLESS_IDLE_EXIT: "1"
|
|
39
|
+
});
|
|
40
|
+
LocalIdleExit$ReventlessLocal.connected();
|
|
41
|
+
await wait(1200);
|
|
42
|
+
globalThis.expect(stopped.contents).toBe(0);
|
|
43
|
+
LocalIdleExit$ReventlessLocal.disconnected();
|
|
44
|
+
await wait(500);
|
|
45
|
+
LocalIdleExit$ReventlessLocal.connected();
|
|
46
|
+
await wait(800);
|
|
47
|
+
globalThis.expect(stopped.contents).toBe(0);
|
|
48
|
+
LocalIdleExit$ReventlessLocal.disconnected();
|
|
49
|
+
await wait(1200);
|
|
50
|
+
globalThis.expect(stopped.contents).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
globalThis.test("does nothing without the variable", async () => {
|
|
53
|
+
LocalIdleExit$ReventlessLocal.start({});
|
|
54
|
+
await wait(1200);
|
|
55
|
+
globalThis.expect(stopped.contents).toBe(0);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
wait,
|
|
61
|
+
}
|
|
62
|
+
/* Not a pure module */
|
|
@@ -123,8 +123,11 @@ describe("the platform API's watching fields", () => {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
expect(await refused(s.getQueryResolver(LocalPlatformObserve.eventLogField)))->toBe(true)
|
|
126
|
+
expect(await refused(s.getQueryResolver(LocalPlatformObserve.viewChangesField)))->toBe(true)
|
|
127
|
+
expect(await refused(s.getQueryResolver(LocalPlatformObserve.commandOutcomesField)))->toBe(true)
|
|
126
128
|
expect(await refused(s.getQueryResolver(LocalPlatformObserve.logLevelsField)))->toBe(true)
|
|
127
129
|
expect(await refused(s.getMutationResolver(LocalPlatformObserve.setLogLevelField)))->toBe(true)
|
|
130
|
+
expect(await refused(s.getQueryResolver(LocalPlatformObserve.metricsField)))->toBe(true)
|
|
128
131
|
})
|
|
129
132
|
|
|
130
133
|
testPromise("set and list the log levels for an admin", async () => {
|
|
@@ -156,6 +159,186 @@ describe("the platform API's watching fields", () => {
|
|
|
156
159
|
expect(answer->field("pageInfo")->field("hasNextPage"))->toEqual(JSON.Encode.bool(false))
|
|
157
160
|
})
|
|
158
161
|
|
|
162
|
+
// Two flows: two changes in one, one in the other.
|
|
163
|
+
let keepChanges = () => {
|
|
164
|
+
let change = (~view, ~key, ~flow, msgId): ReventlessCore.ViewChange.t => {
|
|
165
|
+
view,
|
|
166
|
+
key,
|
|
167
|
+
kind: Changed,
|
|
168
|
+
fields: ["status"],
|
|
169
|
+
after: JSON.parseOrThrow(`{"status":"shipped"}`),
|
|
170
|
+
sources: [{msgId, correlationId: flow}],
|
|
171
|
+
correlationId: flow,
|
|
172
|
+
}
|
|
173
|
+
LocalViewChangeLog.record(change(~view="Orders", ~key="o-1", ~flow="k-1", "m-1"))
|
|
174
|
+
LocalViewChangeLog.record(change(~view="Customers", ~key="c-1", ~flow="k-2", "m-2"))
|
|
175
|
+
LocalViewChangeLog.record(change(~view="Customers", ~key="c-1", ~flow="k-1", "m-3"))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let readChanges = async (args: string) => {
|
|
179
|
+
let s = server()
|
|
180
|
+
let read = s.getQueryResolver(LocalPlatformObserve.viewChangesField)->Option.getOrThrow
|
|
181
|
+
await read(JSON.Encode.null, JSON.parseOrThrow(args), admin())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let views = (answer: JSON.t) =>
|
|
185
|
+
answer
|
|
186
|
+
->field("edges")
|
|
187
|
+
->JSON.Decode.array
|
|
188
|
+
->Option.getOr([])
|
|
189
|
+
->Array.map(e => e->field("node")->field("view")->JSON.Decode.string->Option.getOr(""))
|
|
190
|
+
|
|
191
|
+
let readsBack = async () => {
|
|
192
|
+
keepChanges()
|
|
193
|
+
let flow = await readChanges(`{"filter":{"correlationId":"k-1"}}`)
|
|
194
|
+
let byEvent = await readChanges(`{"filter":{"msgId":"m-2"}}`)
|
|
195
|
+
let firstPage = await readChanges(`{"filter":{"correlationId":"k-1"},"first":1}`)
|
|
196
|
+
let cursor = firstPage->field("pageInfo")->field("endCursor")->JSON.Decode.string
|
|
197
|
+
let secondPage = await readChanges(
|
|
198
|
+
`{"filter":{"correlationId":"k-1"},"first":1,"after":"${cursor->Option.getOr("")}"}`,
|
|
199
|
+
)
|
|
200
|
+
(
|
|
201
|
+
views(flow),
|
|
202
|
+
views(byEvent),
|
|
203
|
+
views(firstPage),
|
|
204
|
+
firstPage->field("pageInfo")->field("hasNextPage"),
|
|
205
|
+
views(secondPage),
|
|
206
|
+
secondPage->field("pageInfo")->field("hasNextPage"),
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
let expected = (
|
|
211
|
+
["Customers", "Orders"],
|
|
212
|
+
["Customers"],
|
|
213
|
+
["Customers"],
|
|
214
|
+
JSON.Encode.bool(true),
|
|
215
|
+
["Orders"],
|
|
216
|
+
JSON.Encode.bool(false),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
testPromise(
|
|
220
|
+
"the view changes answer an admin by flow and event, newest first, in pages",
|
|
221
|
+
async () => {
|
|
222
|
+
BackendState.setMemory()
|
|
223
|
+
LocalViewChangeLog.resetForTests()
|
|
224
|
+
expect(await readsBack())->toEqual(expected)
|
|
225
|
+
LocalViewChangeLog.resetForTests()
|
|
226
|
+
},
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
testPromise("the view changes are kept in a SQLite store alike", async () => {
|
|
230
|
+
let db = SqliteDriver.openDb(~path=":memory:")
|
|
231
|
+
BackendState.setSqlite(~db, ~path=":memory:")
|
|
232
|
+
LocalViewChangeLog.resetForTests()
|
|
233
|
+
expect(await readsBack())->toEqual(expected)
|
|
234
|
+
LocalViewChangeLog.resetForTests()
|
|
235
|
+
BackendState.setMemory()
|
|
236
|
+
SqliteDriver.close(db)
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
// Three commands: two in one flow, one rejected.
|
|
240
|
+
let keepOutcomes = () => {
|
|
241
|
+
let report = (
|
|
242
|
+
~flow,
|
|
243
|
+
~outcome,
|
|
244
|
+
msgId,
|
|
245
|
+
): ReventlessCore.CommandTopic_Helpers.commandOutcomeReport => {
|
|
246
|
+
component: "Order",
|
|
247
|
+
reference: msgId,
|
|
248
|
+
correlationId: flow,
|
|
249
|
+
outcome,
|
|
250
|
+
}
|
|
251
|
+
let accepted = ReventlessCore.CommandTopic_Helpers.OutcomeAccepted({
|
|
252
|
+
entityId: Some("o-1"),
|
|
253
|
+
eventCount: 1,
|
|
254
|
+
})
|
|
255
|
+
LocalCommandOutcomeLog.record(report(~flow="k-1", ~outcome=accepted, "c-1"))
|
|
256
|
+
LocalCommandOutcomeLog.record(report(~flow="k-2", ~outcome=accepted, "c-2"))
|
|
257
|
+
LocalCommandOutcomeLog.record(
|
|
258
|
+
report(
|
|
259
|
+
~flow="k-1",
|
|
260
|
+
~outcome=OutcomeRejected({
|
|
261
|
+
errorCode: "AlreadyShipped",
|
|
262
|
+
errorDetail: "",
|
|
263
|
+
cause: ReventlessCore.CommandTopic_Helpers.DomainRejection,
|
|
264
|
+
}),
|
|
265
|
+
"c-3",
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let outcomesRead = async () => {
|
|
271
|
+
keepOutcomes()
|
|
272
|
+
let s = server()
|
|
273
|
+
let read = s.getQueryResolver(LocalPlatformObserve.commandOutcomesField)->Option.getOrThrow
|
|
274
|
+
let ask = async args => {
|
|
275
|
+
let answer = await read(JSON.Encode.null, JSON.parseOrThrow(args), admin())
|
|
276
|
+
answer
|
|
277
|
+
->field("edges")
|
|
278
|
+
->JSON.Decode.array
|
|
279
|
+
->Option.getOr([])
|
|
280
|
+
->Array.map(e => {
|
|
281
|
+
let node = e->field("node")
|
|
282
|
+
(
|
|
283
|
+
node->field("msgId")->JSON.Decode.string->Option.getOr(""),
|
|
284
|
+
node->field("outcome")->JSON.Decode.string->Option.getOr(""),
|
|
285
|
+
)
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
(await ask(`{"filter":{"correlationId":"k-1"}}`), await ask(`{"filter":{"msgId":"c-2"}}`))
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let outcomesExpected = ([("c-3", "rejected"), ("c-1", "accepted")], [("c-2", "accepted")])
|
|
292
|
+
|
|
293
|
+
testPromise(
|
|
294
|
+
"the command outcomes answer an admin by flow and by command, newest first",
|
|
295
|
+
async () => {
|
|
296
|
+
BackendState.setMemory()
|
|
297
|
+
LocalCommandOutcomeLog.resetForTests()
|
|
298
|
+
expect(await outcomesRead())->toEqual(outcomesExpected)
|
|
299
|
+
LocalCommandOutcomeLog.resetForTests()
|
|
300
|
+
},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
testPromise("the command outcomes are kept in a SQLite store alike", async () => {
|
|
304
|
+
let db = SqliteDriver.openDb(~path=":memory:")
|
|
305
|
+
BackendState.setSqlite(~db, ~path=":memory:")
|
|
306
|
+
LocalCommandOutcomeLog.resetForTests()
|
|
307
|
+
expect(await outcomesRead())->toEqual(outcomesExpected)
|
|
308
|
+
LocalCommandOutcomeLog.resetForTests()
|
|
309
|
+
BackendState.setMemory()
|
|
310
|
+
SqliteDriver.close(db)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
testPromise("the metrics answer an admin with the counts, narrowed by plugin", async () => {
|
|
314
|
+
let counts = LocalMetrics.counts
|
|
315
|
+
ReventlessCore.Metrics.Counts.clear(counts)
|
|
316
|
+
let sample = (~metric, ~plugin, ~value=1): ReventlessCore.Metrics.sample => {
|
|
317
|
+
metric,
|
|
318
|
+
slice: "PlaceOrder",
|
|
319
|
+
plugin,
|
|
320
|
+
value,
|
|
321
|
+
unit: "Count",
|
|
322
|
+
at: Date.now(),
|
|
323
|
+
}
|
|
324
|
+
counts->ReventlessCore.Metrics.Counts.add(sample(~metric="AppendRetry", ~plugin="Ordering"))
|
|
325
|
+
counts->ReventlessCore.Metrics.Counts.add(sample(~metric="AppendRetry", ~plugin="Ordering"))
|
|
326
|
+
counts->ReventlessCore.Metrics.Counts.add(sample(~metric="AppendRetry", ~plugin="Catalog"))
|
|
327
|
+
let s = server()
|
|
328
|
+
let read = s.getQueryResolver(LocalPlatformObserve.metricsField)->Option.getOrThrow
|
|
329
|
+
let answer = await read(
|
|
330
|
+
JSON.Encode.null,
|
|
331
|
+
JSON.parseOrThrow(`{"plugins":["Ordering"]}`),
|
|
332
|
+
admin(),
|
|
333
|
+
)
|
|
334
|
+
let rows = answer->JSON.Decode.array->Option.getOr([])
|
|
335
|
+
expect(rows->Array.length)->toBe(1)
|
|
336
|
+
let row = rows->Array.getUnsafe(0)
|
|
337
|
+
expect(row->field("plugin"))->toEqual(JSON.Encode.string("Ordering"))
|
|
338
|
+
expect(row->field("count"))->toEqual(JSON.Encode.int(2))
|
|
339
|
+
ReventlessCore.Metrics.Counts.clear(counts)
|
|
340
|
+
})
|
|
341
|
+
|
|
159
342
|
testSync("the SDL builds into a schema", () => {
|
|
160
343
|
let s = server()
|
|
161
344
|
let sdl = s.buildSdl()
|
|
@@ -7,12 +7,17 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
7
7
|
import * as Role$Reventless from "@reventlessdev/reventless-spec/src/types/Role.res.mjs";
|
|
8
8
|
import * as Logger$ReventlessCore from "@reventlessdev/reventless-core/src/util/Logger.res.mjs";
|
|
9
9
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
10
|
+
import * as Metrics$ReventlessCore from "@reventlessdev/reventless-core/src/util/Metrics.res.mjs";
|
|
10
11
|
import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
|
|
11
12
|
import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
|
|
12
13
|
import * as BackendState$ReventlessLocal from "../../src/adapter/BackendState.res.mjs";
|
|
13
14
|
import * as LocalLogFeed$ReventlessLocal from "../../src/adapter/LocalLogFeed.res.mjs";
|
|
15
|
+
import * as LocalMetrics$ReventlessLocal from "../../src/adapter/LocalMetrics.res.mjs";
|
|
16
|
+
import * as SqliteDriver$ReventlessLocal from "../../src/adapter/SqliteDriver.res.mjs";
|
|
14
17
|
import * as LocalEventLogRead$ReventlessLocal from "../../src/adapter/LocalEventLogRead.res.mjs";
|
|
18
|
+
import * as LocalViewChangeLog$ReventlessLocal from "../../src/adapter/ViewChangeLog/LocalViewChangeLog.res.mjs";
|
|
15
19
|
import * as LocalPlatformObserve$ReventlessLocal from "../../src/adapter/LocalPlatformObserve.res.mjs";
|
|
20
|
+
import * as LocalCommandOutcomeLog$ReventlessLocal from "../../src/adapter/CommandOutcomeLog/LocalCommandOutcomeLog.res.mjs";
|
|
16
21
|
import * as LocalDcbEventLogStorage$ReventlessLocal from "../../src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs";
|
|
17
22
|
import * as GraphQL_ServerInstance$ReventlessGraphqlServer from "@reventlessdev/reventless-graphql-server/src/GraphQL_ServerInstance.res.mjs";
|
|
18
23
|
|
|
@@ -135,8 +140,11 @@ globalThis.describe("the platform API's watching fields", () => {
|
|
|
135
140
|
}
|
|
136
141
|
};
|
|
137
142
|
globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.eventLogField))).toBe(true);
|
|
143
|
+
globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.viewChangesField))).toBe(true);
|
|
144
|
+
globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.commandOutcomesField))).toBe(true);
|
|
138
145
|
globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.logLevelsField))).toBe(true);
|
|
139
146
|
globalThis.expect(await refused(s.getMutationResolver(LocalPlatformObserve$ReventlessLocal.setLogLevelField))).toBe(true);
|
|
147
|
+
globalThis.expect(await refused(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.metricsField))).toBe(true);
|
|
140
148
|
});
|
|
141
149
|
globalThis.test("set and list the log levels for an admin", async () => {
|
|
142
150
|
let s = server();
|
|
@@ -157,6 +165,179 @@ globalThis.describe("the platform API's watching fields", () => {
|
|
|
157
165
|
globalThis.expect(field(answer, "edges")).toEqual([]);
|
|
158
166
|
globalThis.expect(field(field(answer, "pageInfo"), "hasNextPage")).toEqual(false);
|
|
159
167
|
});
|
|
168
|
+
let keepChanges = () => {
|
|
169
|
+
let change = (view, key, flow, msgId) => ({
|
|
170
|
+
view: view,
|
|
171
|
+
key: key,
|
|
172
|
+
kind: "Changed",
|
|
173
|
+
fields: ["status"],
|
|
174
|
+
after: JSON.parse(`{"status":"shipped"}`),
|
|
175
|
+
sources: [{
|
|
176
|
+
msgId: msgId,
|
|
177
|
+
correlationId: flow
|
|
178
|
+
}],
|
|
179
|
+
correlationId: flow
|
|
180
|
+
});
|
|
181
|
+
LocalViewChangeLog$ReventlessLocal.record(change("Orders", "o-1", "k-1", "m-1"));
|
|
182
|
+
LocalViewChangeLog$ReventlessLocal.record(change("Customers", "c-1", "k-2", "m-2"));
|
|
183
|
+
LocalViewChangeLog$ReventlessLocal.record(change("Customers", "c-1", "k-1", "m-3"));
|
|
184
|
+
};
|
|
185
|
+
let readChanges = async args => {
|
|
186
|
+
let s = server();
|
|
187
|
+
let read = Stdlib_Option.getOrThrow(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.viewChangesField), undefined);
|
|
188
|
+
return await read(null, JSON.parse(args), context([Role$Reventless.adminGroup()]));
|
|
189
|
+
};
|
|
190
|
+
let views = answer => Stdlib_Option.getOr(Stdlib_JSON.Decode.array(field(answer, "edges")), []).map(e => Stdlib_Option.getOr(Stdlib_JSON.Decode.string(field(field(e, "node"), "view")), ""));
|
|
191
|
+
let readsBack = async () => {
|
|
192
|
+
keepChanges();
|
|
193
|
+
let flow = await readChanges(`{"filter":{"correlationId":"k-1"}}`);
|
|
194
|
+
let byEvent = await readChanges(`{"filter":{"msgId":"m-2"}}`);
|
|
195
|
+
let firstPage = await readChanges(`{"filter":{"correlationId":"k-1"},"first":1}`);
|
|
196
|
+
let cursor = Stdlib_JSON.Decode.string(field(field(firstPage, "pageInfo"), "endCursor"));
|
|
197
|
+
let secondPage = await readChanges(`{"filter":{"correlationId":"k-1"},"first":1,"after":"` + Stdlib_Option.getOr(cursor, "") + `"}`);
|
|
198
|
+
return [
|
|
199
|
+
views(flow),
|
|
200
|
+
views(byEvent),
|
|
201
|
+
views(firstPage),
|
|
202
|
+
field(field(firstPage, "pageInfo"), "hasNextPage"),
|
|
203
|
+
views(secondPage),
|
|
204
|
+
field(field(secondPage, "pageInfo"), "hasNextPage")
|
|
205
|
+
];
|
|
206
|
+
};
|
|
207
|
+
let expected_0 = [
|
|
208
|
+
"Customers",
|
|
209
|
+
"Orders"
|
|
210
|
+
];
|
|
211
|
+
let expected_1 = ["Customers"];
|
|
212
|
+
let expected_2 = ["Customers"];
|
|
213
|
+
let expected_4 = ["Orders"];
|
|
214
|
+
let expected = [
|
|
215
|
+
expected_0,
|
|
216
|
+
expected_1,
|
|
217
|
+
expected_2,
|
|
218
|
+
true,
|
|
219
|
+
expected_4,
|
|
220
|
+
false
|
|
221
|
+
];
|
|
222
|
+
globalThis.test("the view changes answer an admin by flow and event, newest first, in pages", async () => {
|
|
223
|
+
BackendState$ReventlessLocal.setMemory();
|
|
224
|
+
LocalViewChangeLog$ReventlessLocal.resetForTests();
|
|
225
|
+
globalThis.expect(await readsBack()).toEqual(expected);
|
|
226
|
+
return LocalViewChangeLog$ReventlessLocal.resetForTests();
|
|
227
|
+
});
|
|
228
|
+
globalThis.test("the view changes are kept in a SQLite store alike", async () => {
|
|
229
|
+
let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
|
|
230
|
+
BackendState$ReventlessLocal.setSqlite(db, ":memory:");
|
|
231
|
+
LocalViewChangeLog$ReventlessLocal.resetForTests();
|
|
232
|
+
globalThis.expect(await readsBack()).toEqual(expected);
|
|
233
|
+
LocalViewChangeLog$ReventlessLocal.resetForTests();
|
|
234
|
+
BackendState$ReventlessLocal.setMemory();
|
|
235
|
+
return SqliteDriver$ReventlessLocal.close(db);
|
|
236
|
+
});
|
|
237
|
+
let keepOutcomes = () => {
|
|
238
|
+
let report = (flow, outcome, msgId) => ({
|
|
239
|
+
component: "Order",
|
|
240
|
+
reference: msgId,
|
|
241
|
+
correlationId: flow,
|
|
242
|
+
outcome: outcome
|
|
243
|
+
});
|
|
244
|
+
let accepted = {
|
|
245
|
+
TAG: "OutcomeAccepted",
|
|
246
|
+
entityId: "o-1",
|
|
247
|
+
eventCount: 1
|
|
248
|
+
};
|
|
249
|
+
LocalCommandOutcomeLog$ReventlessLocal.record(report("k-1", accepted, "c-1"));
|
|
250
|
+
LocalCommandOutcomeLog$ReventlessLocal.record(report("k-2", accepted, "c-2"));
|
|
251
|
+
LocalCommandOutcomeLog$ReventlessLocal.record({
|
|
252
|
+
component: "Order",
|
|
253
|
+
reference: "c-3",
|
|
254
|
+
correlationId: "k-1",
|
|
255
|
+
outcome: {
|
|
256
|
+
TAG: "OutcomeRejected",
|
|
257
|
+
errorCode: "AlreadyShipped",
|
|
258
|
+
errorDetail: "",
|
|
259
|
+
cause: "DomainRejection"
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
};
|
|
263
|
+
let outcomesRead = async () => {
|
|
264
|
+
keepOutcomes();
|
|
265
|
+
let s = server();
|
|
266
|
+
let read = Stdlib_Option.getOrThrow(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.commandOutcomesField), undefined);
|
|
267
|
+
let ask = async args => {
|
|
268
|
+
let answer = await read(null, JSON.parse(args), context([Role$Reventless.adminGroup()]));
|
|
269
|
+
return Stdlib_Option.getOr(Stdlib_JSON.Decode.array(field(answer, "edges")), []).map(e => {
|
|
270
|
+
let node = field(e, "node");
|
|
271
|
+
return [
|
|
272
|
+
Stdlib_Option.getOr(Stdlib_JSON.Decode.string(field(node, "msgId")), ""),
|
|
273
|
+
Stdlib_Option.getOr(Stdlib_JSON.Decode.string(field(node, "outcome")), "")
|
|
274
|
+
];
|
|
275
|
+
});
|
|
276
|
+
};
|
|
277
|
+
return [
|
|
278
|
+
await ask(`{"filter":{"correlationId":"k-1"}}`),
|
|
279
|
+
await ask(`{"filter":{"msgId":"c-2"}}`)
|
|
280
|
+
];
|
|
281
|
+
};
|
|
282
|
+
let outcomesExpected_0 = [
|
|
283
|
+
[
|
|
284
|
+
"c-3",
|
|
285
|
+
"rejected"
|
|
286
|
+
],
|
|
287
|
+
[
|
|
288
|
+
"c-1",
|
|
289
|
+
"accepted"
|
|
290
|
+
]
|
|
291
|
+
];
|
|
292
|
+
let outcomesExpected_1 = [[
|
|
293
|
+
"c-2",
|
|
294
|
+
"accepted"
|
|
295
|
+
]];
|
|
296
|
+
let outcomesExpected = [
|
|
297
|
+
outcomesExpected_0,
|
|
298
|
+
outcomesExpected_1
|
|
299
|
+
];
|
|
300
|
+
globalThis.test("the command outcomes answer an admin by flow and by command, newest first", async () => {
|
|
301
|
+
BackendState$ReventlessLocal.setMemory();
|
|
302
|
+
LocalCommandOutcomeLog$ReventlessLocal.resetForTests();
|
|
303
|
+
globalThis.expect(await outcomesRead()).toEqual(outcomesExpected);
|
|
304
|
+
return LocalCommandOutcomeLog$ReventlessLocal.resetForTests();
|
|
305
|
+
});
|
|
306
|
+
globalThis.test("the command outcomes are kept in a SQLite store alike", async () => {
|
|
307
|
+
let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
|
|
308
|
+
BackendState$ReventlessLocal.setSqlite(db, ":memory:");
|
|
309
|
+
LocalCommandOutcomeLog$ReventlessLocal.resetForTests();
|
|
310
|
+
globalThis.expect(await outcomesRead()).toEqual(outcomesExpected);
|
|
311
|
+
LocalCommandOutcomeLog$ReventlessLocal.resetForTests();
|
|
312
|
+
BackendState$ReventlessLocal.setMemory();
|
|
313
|
+
return SqliteDriver$ReventlessLocal.close(db);
|
|
314
|
+
});
|
|
315
|
+
globalThis.test("the metrics answer an admin with the counts, narrowed by plugin", async () => {
|
|
316
|
+
Metrics$ReventlessCore.Counts.clear(LocalMetrics$ReventlessLocal.counts);
|
|
317
|
+
let sample = (metric, plugin, valueOpt) => {
|
|
318
|
+
let value = valueOpt !== undefined ? valueOpt : 1;
|
|
319
|
+
return {
|
|
320
|
+
metric: metric,
|
|
321
|
+
slice: "PlaceOrder",
|
|
322
|
+
plugin: plugin,
|
|
323
|
+
value: value,
|
|
324
|
+
unit: "Count",
|
|
325
|
+
at: Date.now()
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
Metrics$ReventlessCore.Counts.add(LocalMetrics$ReventlessLocal.counts, sample("AppendRetry", "Ordering", undefined));
|
|
329
|
+
Metrics$ReventlessCore.Counts.add(LocalMetrics$ReventlessLocal.counts, sample("AppendRetry", "Ordering", undefined));
|
|
330
|
+
Metrics$ReventlessCore.Counts.add(LocalMetrics$ReventlessLocal.counts, sample("AppendRetry", "Catalog", undefined));
|
|
331
|
+
let s = server();
|
|
332
|
+
let read = Stdlib_Option.getOrThrow(s.getQueryResolver(LocalPlatformObserve$ReventlessLocal.metricsField), undefined);
|
|
333
|
+
let answer = await read(null, JSON.parse(`{"plugins":["Ordering"]}`), context([Role$Reventless.adminGroup()]));
|
|
334
|
+
let rows = Stdlib_Option.getOr(Stdlib_JSON.Decode.array(answer), []);
|
|
335
|
+
globalThis.expect(rows.length).toBe(1);
|
|
336
|
+
let row = rows[0];
|
|
337
|
+
globalThis.expect(field(row, "plugin")).toEqual("Ordering");
|
|
338
|
+
globalThis.expect(field(row, "count")).toEqual(2);
|
|
339
|
+
return Metrics$ReventlessCore.Counts.clear(LocalMetrics$ReventlessLocal.counts);
|
|
340
|
+
});
|
|
160
341
|
globalThis.test("the SDL builds into a schema", () => {
|
|
161
342
|
let s = server();
|
|
162
343
|
let sdl = s.buildSdl();
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// The line the app's API writes for each GraphQL operation: how it reads, and at which level.
|
|
2
|
+
|
|
3
|
+
open JestGlobals
|
|
4
|
+
|
|
5
|
+
let report = (
|
|
6
|
+
~kind="query",
|
|
7
|
+
~operationName=?,
|
|
8
|
+
~fields=["Catalog_Products"],
|
|
9
|
+
~user=?,
|
|
10
|
+
~ms=12.,
|
|
11
|
+
~errors=[],
|
|
12
|
+
(),
|
|
13
|
+
): GraphqlYoga.requestReport => {kind, ?operationName, fields, ?user, ms, errors}
|
|
14
|
+
|
|
15
|
+
let line = r => LocalRequestLog.lineOf(r)
|
|
16
|
+
|
|
17
|
+
testSync("a query is a debug line naming it, who asked and how long it took", () => {
|
|
18
|
+
let l = line(report(~operationName="Products", ~user="alice", ()))
|
|
19
|
+
expect(l.message)->toBe("query Products by alice in 12 ms")
|
|
20
|
+
expect(l.level == Debug)->toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
testSync("an operation without a name goes by its root fields", () => {
|
|
24
|
+
let l = line(report(~fields=["Catalog_Products", "Catalog_Categories"], ()))
|
|
25
|
+
expect(l.message)->toBe("query Catalog_Products, Catalog_Categories in 12 ms")
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
testSync("a mutation is an info line", () => {
|
|
29
|
+
let l = line(report(~kind="mutation", ~fields=["Catalog_AddProduct"], ~user="bob", ~ms=40., ()))
|
|
30
|
+
expect(l.message)->toBe("mutation Catalog_AddProduct by bob in 40 ms")
|
|
31
|
+
expect(l.level == Info)->toBe(true)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
testSync("an operation that answered with errors is a warning with the first of them", () => {
|
|
35
|
+
let l = line(
|
|
36
|
+
report(~kind="mutation", ~fields=["Catalog_AddProduct"], ~errors=["Forbidden", "Other"], ()),
|
|
37
|
+
)
|
|
38
|
+
expect(l.message)->toBe("mutation Catalog_AddProduct failed in 12 ms: Forbidden (and 1 more)")
|
|
39
|
+
expect(l.level == Warn)->toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
testSync("a subscription is said when it starts, with no time", () => {
|
|
43
|
+
let l = line(report(~kind="subscription", ~fields=["onOrderChanged"], ~ms=0., ()))
|
|
44
|
+
expect(l.message)->toBe("subscription onOrderChanged")
|
|
45
|
+
expect(l.level == Debug)->toBe(true)
|
|
46
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as LocalRequestLog$ReventlessLocal from "../../src/adapter/LocalRequestLog.res.mjs";
|
|
4
|
+
|
|
5
|
+
function report(kindOpt, operationName, fieldsOpt, user, msOpt, errorsOpt, param) {
|
|
6
|
+
let kind = kindOpt !== undefined ? kindOpt : "query";
|
|
7
|
+
let fields = fieldsOpt !== undefined ? fieldsOpt : ["Catalog_Products"];
|
|
8
|
+
let ms = msOpt !== undefined ? msOpt : 12;
|
|
9
|
+
let errors = errorsOpt !== undefined ? errorsOpt : [];
|
|
10
|
+
return {
|
|
11
|
+
kind: kind,
|
|
12
|
+
operationName: operationName,
|
|
13
|
+
fields: fields,
|
|
14
|
+
user: user,
|
|
15
|
+
ms: ms,
|
|
16
|
+
errors: errors
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let line = LocalRequestLog$ReventlessLocal.lineOf;
|
|
21
|
+
|
|
22
|
+
globalThis.test("a query is a debug line naming it, who asked and how long it took", () => {
|
|
23
|
+
let l = LocalRequestLog$ReventlessLocal.lineOf(report(undefined, "Products", undefined, "alice", undefined, undefined, undefined));
|
|
24
|
+
globalThis.expect(l.message).toBe("query Products by alice in 12 ms");
|
|
25
|
+
globalThis.expect(l.level === "Debug").toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
globalThis.test("an operation without a name goes by its root fields", () => {
|
|
29
|
+
let l = LocalRequestLog$ReventlessLocal.lineOf(report(undefined, undefined, [
|
|
30
|
+
"Catalog_Products",
|
|
31
|
+
"Catalog_Categories"
|
|
32
|
+
], undefined, undefined, undefined, undefined));
|
|
33
|
+
globalThis.expect(l.message).toBe("query Catalog_Products, Catalog_Categories in 12 ms");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
globalThis.test("a mutation is an info line", () => {
|
|
37
|
+
let l = LocalRequestLog$ReventlessLocal.lineOf(report("mutation", undefined, ["Catalog_AddProduct"], "bob", 40, undefined, undefined));
|
|
38
|
+
globalThis.expect(l.message).toBe("mutation Catalog_AddProduct by bob in 40 ms");
|
|
39
|
+
globalThis.expect(l.level === "Info").toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
globalThis.test("an operation that answered with errors is a warning with the first of them", () => {
|
|
43
|
+
let l = LocalRequestLog$ReventlessLocal.lineOf(report("mutation", undefined, ["Catalog_AddProduct"], undefined, undefined, [
|
|
44
|
+
"Forbidden",
|
|
45
|
+
"Other"
|
|
46
|
+
], undefined));
|
|
47
|
+
globalThis.expect(l.message).toBe("mutation Catalog_AddProduct failed in 12 ms: Forbidden (and 1 more)");
|
|
48
|
+
globalThis.expect(l.level === "Warn").toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
globalThis.test("a subscription is said when it starts, with no time", () => {
|
|
52
|
+
let l = LocalRequestLog$ReventlessLocal.lineOf(report("subscription", undefined, ["onOrderChanged"], undefined, 0, undefined, undefined));
|
|
53
|
+
globalThis.expect(l.message).toBe("subscription onOrderChanged");
|
|
54
|
+
globalThis.expect(l.level === "Debug").toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
report,
|
|
59
|
+
line,
|
|
60
|
+
}
|
|
61
|
+
/* Not a pure module */
|