@digital-alchemy/hass 25.11.16 → 25.11.23

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 (54) hide show
  1. package/dist/helpers/entity-state.d.mts +20 -3
  2. package/dist/helpers/fetch/service-list.d.mts +86 -5
  3. package/dist/helpers/id-by.d.mts +1 -1
  4. package/dist/helpers/utility.d.mts +1 -1
  5. package/dist/mock_assistant/mock-assistant.module.mjs.map +1 -1
  6. package/dist/mock_assistant/services/websocket-api.service.d.mts +3 -1
  7. package/dist/mock_assistant/services/websocket-api.service.mjs +43 -4
  8. package/dist/mock_assistant/services/websocket-api.service.mjs.map +1 -1
  9. package/dist/services/feature.service.d.mts +2 -2
  10. package/dist/services/feature.service.mjs.map +1 -1
  11. package/dist/services/id-by.service.mjs +4 -1
  12. package/dist/services/id-by.service.mjs.map +1 -1
  13. package/dist/services/reference.service.d.mts +1 -1
  14. package/dist/services/reference.service.mjs +62 -7
  15. package/dist/services/reference.service.mjs.map +1 -1
  16. package/dist/testing/area.spec.mjs +142 -1
  17. package/dist/testing/area.spec.mjs.map +1 -1
  18. package/dist/testing/call-proxy.spec.d.mts +1 -0
  19. package/dist/testing/call-proxy.spec.mjs +204 -0
  20. package/dist/testing/call-proxy.spec.mjs.map +1 -0
  21. package/dist/testing/conversation.spec.mjs +1 -1
  22. package/dist/testing/conversation.spec.mjs.map +1 -1
  23. package/dist/testing/entity.spec.mjs +14 -0
  24. package/dist/testing/entity.spec.mjs.map +1 -1
  25. package/dist/testing/id-by.spec.mjs +38 -0
  26. package/dist/testing/id-by.spec.mjs.map +1 -1
  27. package/dist/testing/ref-by.spec.mjs +805 -4
  28. package/dist/testing/ref-by.spec.mjs.map +1 -1
  29. package/dist/testing/scheduler.spec.d.mts +1 -0
  30. package/dist/testing/scheduler.spec.mjs +412 -0
  31. package/dist/testing/scheduler.spec.mjs.map +1 -0
  32. package/dist/testing/websocket.spec.mjs +25 -0
  33. package/dist/testing/websocket.spec.mjs.map +1 -1
  34. package/dist/testing/workflow.spec.mjs +1 -1
  35. package/dist/testing/workflow.spec.mjs.map +1 -1
  36. package/package.json +17 -16
  37. package/src/helpers/entity-state.mts +20 -3
  38. package/src/helpers/fetch/service-list.mts +89 -5
  39. package/src/helpers/id-by.mts +1 -0
  40. package/src/helpers/utility.mts +1 -1
  41. package/src/mock_assistant/mock-assistant.module.mts +0 -1
  42. package/src/mock_assistant/services/websocket-api.service.mts +46 -4
  43. package/src/services/feature.service.mts +9 -6
  44. package/src/services/id-by.service.mts +4 -1
  45. package/src/services/reference.service.mts +78 -9
  46. package/src/testing/area.spec.mts +166 -2
  47. package/src/testing/call-proxy.spec.mts +241 -0
  48. package/src/testing/conversation.spec.mts +1 -1
  49. package/src/testing/entity.spec.mts +15 -0
  50. package/src/testing/id-by.spec.mts +50 -0
  51. package/src/testing/ref-by.spec.mts +965 -4
  52. package/src/testing/scheduler.spec.mts +444 -0
  53. package/src/testing/websocket.spec.mts +33 -0
  54. package/src/testing/workflow.spec.mts +1 -1
@@ -0,0 +1,444 @@
1
+ import { CronExpression, HOUR, MINUTE, SECOND, TestRunner } from "@digital-alchemy/core";
2
+ import dayjs from "dayjs";
3
+
4
+ describe("Scheduler", () => {
5
+ afterEach(async () => {
6
+ vi.restoreAllMocks();
7
+ });
8
+
9
+ describe("cron", () => {
10
+ it("runs a cron schedule", async () => {
11
+ vi.useFakeTimers();
12
+ const spy = vi.fn();
13
+ const app = await TestRunner().run(({ scheduler }) => {
14
+ scheduler.cron({
15
+ exec: spy,
16
+ schedule: CronExpression.EVERY_MINUTE,
17
+ });
18
+ });
19
+ vi.advanceTimersByTime(60 * MINUTE);
20
+ await app.teardown();
21
+ expect(spy).toHaveBeenCalledTimes(60);
22
+ vi.useRealTimers();
23
+ });
24
+ });
25
+
26
+ describe("interval", () => {
27
+ it("runs an interval schedule", async () => {
28
+ vi.useFakeTimers();
29
+ const spy = vi.fn();
30
+ const app = await TestRunner().run(({ scheduler }) => {
31
+ // @ts-expect-error it's temporarily still here
32
+ scheduler.interval({
33
+ exec: spy,
34
+ interval: MINUTE,
35
+ });
36
+ });
37
+ vi.advanceTimersByTime(60 * MINUTE);
38
+ expect(spy).toHaveBeenCalledTimes(60);
39
+ vi.useRealTimers();
40
+ await app.teardown();
41
+ });
42
+ });
43
+
44
+ describe("setInterval", () => {
45
+ it("runs", async () => {
46
+ vi.useFakeTimers();
47
+ const spy = vi.fn();
48
+ const app = await TestRunner().run(({ scheduler }) => {
49
+ scheduler.setInterval(spy, MINUTE);
50
+ });
51
+ vi.advanceTimersByTime(60 * MINUTE);
52
+ expect(spy).toHaveBeenCalledTimes(60);
53
+ vi.useRealTimers();
54
+ await app.teardown();
55
+ });
56
+
57
+ it("stops", async () => {
58
+ vi.useFakeTimers();
59
+ const spy = vi.fn();
60
+ const app = await TestRunner().run(({ scheduler, lifecycle }) => {
61
+ const remove = scheduler.setInterval(spy, MINUTE);
62
+ lifecycle.onReady(() => {
63
+ setTimeout(() => remove(), 30 * MINUTE);
64
+ });
65
+ });
66
+ vi.advanceTimersByTime(60 * MINUTE);
67
+ expect(spy).toHaveBeenCalledTimes(30);
68
+ vi.useRealTimers();
69
+ await app.teardown();
70
+ });
71
+
72
+ it("stops early", async () => {
73
+ const spy = vi.fn();
74
+ const intervalSpy = vi.spyOn(globalThis, "setInterval");
75
+ const app = await TestRunner().run(({ scheduler }) => {
76
+ const remove = scheduler.setInterval(spy, MINUTE);
77
+ remove();
78
+ });
79
+ expect(intervalSpy).not.toHaveBeenCalled();
80
+ await app.teardown();
81
+ });
82
+
83
+ it("handles tuple offset [amount, unit]", async () => {
84
+ vi.useFakeTimers();
85
+ const spy = vi.fn();
86
+ const app = await TestRunner().run(({ scheduler }) => {
87
+ scheduler.setInterval(spy, [30, "seconds"]);
88
+ });
89
+ vi.advanceTimersByTime(29 * SECOND);
90
+ expect(spy).not.toHaveBeenCalled();
91
+ vi.advanceTimersByTime(2 * SECOND);
92
+ expect(spy).toHaveBeenCalledTimes(1);
93
+ vi.advanceTimersByTime(30 * SECOND);
94
+ expect(spy).toHaveBeenCalledTimes(2);
95
+ vi.useRealTimers();
96
+ await app.teardown();
97
+ });
98
+
99
+ it("handles object offset (DurationUnitsObjectType)", async () => {
100
+ vi.useFakeTimers();
101
+ const spy = vi.fn();
102
+ const app = await TestRunner().run(({ scheduler }) => {
103
+ scheduler.setInterval(spy, { minutes: 1, seconds: 30 });
104
+ });
105
+ vi.advanceTimersByTime(MINUTE + 29 * SECOND);
106
+ expect(spy).not.toHaveBeenCalled();
107
+ vi.advanceTimersByTime(2 * SECOND);
108
+ expect(spy).toHaveBeenCalledTimes(1);
109
+ vi.advanceTimersByTime(MINUTE + 30 * SECOND);
110
+ expect(spy).toHaveBeenCalledTimes(2);
111
+ vi.useRealTimers();
112
+ await app.teardown();
113
+ });
114
+
115
+ it("handles string offset (ISO 8601 partial)", async () => {
116
+ vi.useFakeTimers();
117
+ const spy = vi.fn();
118
+ const app = await TestRunner().run(({ scheduler }) => {
119
+ scheduler.setInterval(spy, "1M30S"); // 1 minute 30 seconds
120
+ });
121
+ vi.advanceTimersByTime(MINUTE + 29 * SECOND);
122
+ expect(spy).not.toHaveBeenCalled();
123
+ vi.advanceTimersByTime(2 * SECOND);
124
+ expect(spy).toHaveBeenCalledTimes(1);
125
+ vi.advanceTimersByTime(MINUTE + 30 * SECOND);
126
+ expect(spy).toHaveBeenCalledTimes(2);
127
+ vi.useRealTimers();
128
+ await app.teardown();
129
+ });
130
+
131
+ it("handles function offset", async () => {
132
+ vi.useFakeTimers();
133
+ const spy = vi.fn();
134
+ const app = await TestRunner().run(({ scheduler }) => {
135
+ scheduler.setInterval(spy, () => 2 * MINUTE);
136
+ });
137
+ vi.advanceTimersByTime(2 * MINUTE - SECOND);
138
+ expect(spy).not.toHaveBeenCalled();
139
+ vi.advanceTimersByTime(2 * SECOND);
140
+ expect(spy).toHaveBeenCalledTimes(1);
141
+ vi.advanceTimersByTime(2 * MINUTE);
142
+ expect(spy).toHaveBeenCalledTimes(2);
143
+ vi.useRealTimers();
144
+ await app.teardown();
145
+ });
146
+
147
+ it("handles Duration object", async () => {
148
+ vi.useFakeTimers();
149
+ const spy = vi.fn();
150
+ const app = await TestRunner().run(({ scheduler }) => {
151
+ const duration = dayjs.duration({ minutes: 2, seconds: 30 });
152
+ scheduler.setInterval(spy, duration);
153
+ });
154
+ vi.advanceTimersByTime(2 * MINUTE + 29 * SECOND);
155
+ expect(spy).not.toHaveBeenCalled();
156
+ vi.advanceTimersByTime(2 * SECOND);
157
+ expect(spy).toHaveBeenCalledTimes(1);
158
+ vi.advanceTimersByTime(2 * MINUTE + 30 * SECOND);
159
+ expect(spy).toHaveBeenCalledTimes(2);
160
+ vi.useRealTimers();
161
+ await app.teardown();
162
+ });
163
+
164
+ it("handles tuple with hours", async () => {
165
+ vi.useFakeTimers();
166
+ const spy = vi.fn();
167
+ const app = await TestRunner().run(({ scheduler }) => {
168
+ scheduler.setInterval(spy, [1, "hour"]);
169
+ });
170
+ vi.advanceTimersByTime(HOUR - SECOND);
171
+ expect(spy).not.toHaveBeenCalled();
172
+ vi.advanceTimersByTime(2 * SECOND);
173
+ expect(spy).toHaveBeenCalledTimes(1);
174
+ vi.advanceTimersByTime(HOUR);
175
+ expect(spy).toHaveBeenCalledTimes(2);
176
+ vi.useRealTimers();
177
+ await app.teardown();
178
+ });
179
+ });
180
+
181
+ describe("setTimeout", () => {
182
+ it("runs", async () => {
183
+ vi.useFakeTimers();
184
+ const spy = vi.fn();
185
+ const app = await TestRunner().run(({ scheduler }) => {
186
+ scheduler.setTimeout(spy, MINUTE);
187
+ });
188
+ vi.advanceTimersByTime(59 * SECOND);
189
+ expect(spy).not.toHaveBeenCalled();
190
+ vi.advanceTimersByTime(3 * SECOND);
191
+ expect(spy).toHaveBeenCalledTimes(1);
192
+ vi.useRealTimers();
193
+ await app.teardown();
194
+ });
195
+
196
+ it("stops", async () => {
197
+ vi.useFakeTimers();
198
+ const spy = vi.fn();
199
+ const app = await TestRunner().run(({ scheduler }) => {
200
+ const remove = scheduler.setTimeout(spy, MINUTE);
201
+ setTimeout(() => remove(), 30 * SECOND);
202
+ });
203
+ vi.advanceTimersByTime(5 * MINUTE);
204
+ expect(spy).not.toHaveBeenCalled();
205
+ vi.useRealTimers();
206
+ await app.teardown();
207
+ });
208
+
209
+ it("stops early", async () => {
210
+ const spy = vi.fn();
211
+ const intervalSpy = vi.spyOn(globalThis, "setTimeout");
212
+ const app = await TestRunner().run(({ scheduler }) => {
213
+ const remove = scheduler.setTimeout(spy, MINUTE);
214
+ remove();
215
+ });
216
+ expect(intervalSpy).not.toHaveBeenCalled();
217
+ await app.teardown();
218
+ });
219
+
220
+ it("handles tuple offset [amount, unit]", async () => {
221
+ vi.useFakeTimers();
222
+ const spy = vi.fn();
223
+ const app = await TestRunner().run(({ scheduler }) => {
224
+ scheduler.setTimeout(spy, [30, "seconds"]);
225
+ });
226
+ vi.advanceTimersByTime(29 * SECOND);
227
+ expect(spy).not.toHaveBeenCalled();
228
+ vi.advanceTimersByTime(2 * SECOND);
229
+ expect(spy).toHaveBeenCalledTimes(1);
230
+ vi.useRealTimers();
231
+ await app.teardown();
232
+ });
233
+
234
+ it("handles object offset (DurationUnitsObjectType)", async () => {
235
+ vi.useFakeTimers();
236
+ const spy = vi.fn();
237
+ const app = await TestRunner().run(({ scheduler }) => {
238
+ scheduler.setTimeout(spy, { minutes: 1, seconds: 30 });
239
+ });
240
+ vi.advanceTimersByTime(MINUTE + 29 * SECOND);
241
+ expect(spy).not.toHaveBeenCalled();
242
+ vi.advanceTimersByTime(2 * SECOND);
243
+ expect(spy).toHaveBeenCalledTimes(1);
244
+ vi.useRealTimers();
245
+ await app.teardown();
246
+ });
247
+
248
+ it("handles string offset (ISO 8601 partial)", async () => {
249
+ vi.useFakeTimers();
250
+ const spy = vi.fn();
251
+ const app = await TestRunner().run(({ scheduler }) => {
252
+ scheduler.setTimeout(spy, "1M30S"); // 1 minute 30 seconds
253
+ });
254
+ vi.advanceTimersByTime(MINUTE + 29 * SECOND);
255
+ expect(spy).not.toHaveBeenCalled();
256
+ vi.advanceTimersByTime(2 * SECOND);
257
+ expect(spy).toHaveBeenCalledTimes(1);
258
+ vi.useRealTimers();
259
+ await app.teardown();
260
+ });
261
+
262
+ it("handles function offset", async () => {
263
+ vi.useFakeTimers();
264
+ const spy = vi.fn();
265
+ const app = await TestRunner().run(({ scheduler }) => {
266
+ scheduler.setTimeout(spy, () => 2 * MINUTE);
267
+ });
268
+ vi.advanceTimersByTime(2 * MINUTE - SECOND);
269
+ expect(spy).not.toHaveBeenCalled();
270
+ vi.advanceTimersByTime(2 * SECOND);
271
+ expect(spy).toHaveBeenCalledTimes(1);
272
+ vi.useRealTimers();
273
+ await app.teardown();
274
+ });
275
+
276
+ it("handles Duration object", async () => {
277
+ vi.useFakeTimers();
278
+ const spy = vi.fn();
279
+ const app = await TestRunner().run(({ scheduler }) => {
280
+ const duration = dayjs.duration({ minutes: 2, seconds: 30 });
281
+ scheduler.setTimeout(spy, duration);
282
+ });
283
+ vi.advanceTimersByTime(2 * MINUTE + 29 * SECOND);
284
+ expect(spy).not.toHaveBeenCalled();
285
+ vi.advanceTimersByTime(2 * SECOND);
286
+ expect(spy).toHaveBeenCalledTimes(1);
287
+ vi.useRealTimers();
288
+ await app.teardown();
289
+ });
290
+
291
+ it("handles zero timeout", async () => {
292
+ vi.useFakeTimers();
293
+ const spy = vi.fn();
294
+ const app = await TestRunner().run(({ scheduler }) => {
295
+ scheduler.setTimeout(spy, 0);
296
+ });
297
+ vi.advanceTimersByTime(1);
298
+ expect(spy).toHaveBeenCalledTimes(1);
299
+ vi.useRealTimers();
300
+ await app.teardown();
301
+ });
302
+
303
+ it("handles tuple with hours", async () => {
304
+ vi.useFakeTimers();
305
+ const spy = vi.fn();
306
+ const app = await TestRunner().run(({ scheduler }) => {
307
+ scheduler.setTimeout(spy, [1, "hour"]);
308
+ });
309
+ vi.advanceTimersByTime(HOUR - SECOND);
310
+ expect(spy).not.toHaveBeenCalled();
311
+ vi.advanceTimersByTime(2 * SECOND);
312
+ expect(spy).toHaveBeenCalledTimes(1);
313
+ vi.useRealTimers();
314
+ await app.teardown();
315
+ });
316
+ });
317
+
318
+ describe("sliding", () => {
319
+ it("requires an next method", async () => {
320
+ expect.assertions(1);
321
+
322
+ await TestRunner().run(({ scheduler }) => {
323
+ expect(() => {
324
+ scheduler.sliding({
325
+ exec: () => {},
326
+ next: undefined,
327
+ reset: "",
328
+ });
329
+ }).toThrow();
330
+ });
331
+ });
332
+
333
+ it("requires an exec method", async () => {
334
+ expect.assertions(1);
335
+
336
+ await TestRunner().run(({ scheduler }) => {
337
+ expect(() => {
338
+ scheduler.sliding({
339
+ exec: undefined,
340
+ next: () => undefined,
341
+ reset: "",
342
+ });
343
+ }).toThrow();
344
+ });
345
+ });
346
+
347
+ // This works, but the test is being weird with fake timers
348
+ it.skip("runs a sliding schedule", async () => {
349
+ vi.useFakeTimers();
350
+ const advanceHours = 5;
351
+ vi.setSystemTime(dayjs("2024-09-13T00:00:00.000Z").toDate());
352
+ const spy = vi.fn();
353
+ const app = await TestRunner().run(({ scheduler }) => {
354
+ scheduler.sliding({
355
+ exec: spy,
356
+ next: () => dayjs().add(1, "minute"),
357
+ reset: CronExpression.EVERY_HOUR,
358
+ });
359
+ });
360
+ vi.advanceTimersByTime(advanceHours * HOUR);
361
+
362
+ vi.useRealTimers();
363
+ await app.teardown();
364
+ expect(spy).toHaveBeenCalledTimes(advanceHours);
365
+ });
366
+
367
+ it("can stop a schedule", async () => {
368
+ vi.useFakeTimers();
369
+ const advanceHours = 3;
370
+ vi.setSystemTime(dayjs("2024-09-13T00:00:00.000Z").toDate());
371
+ const spy = vi.fn();
372
+ let stop: () => void;
373
+ const app = await TestRunner().run(({ scheduler }) => {
374
+ stop = scheduler.sliding({
375
+ exec: spy,
376
+ next: () => dayjs().add(1, "minute"),
377
+ reset: CronExpression.EVERY_HOUR,
378
+ });
379
+ });
380
+ stop();
381
+ vi.advanceTimersByTime(advanceHours * HOUR);
382
+
383
+ expect(spy).toHaveBeenCalledTimes(0);
384
+ vi.useRealTimers();
385
+ await app.teardown();
386
+ });
387
+
388
+ it("can stop a schedule while waiting for next call", async () => {
389
+ vi.useFakeTimers();
390
+ vi.setSystemTime(dayjs("2024-09-13T00:00:00.000Z").toDate());
391
+ const spy = vi.fn();
392
+ let stop: () => void;
393
+ const app = await TestRunner().run(({ scheduler }) => {
394
+ stop = scheduler.sliding({
395
+ exec: spy,
396
+ next: () => dayjs().add(2, "hour"),
397
+ reset: CronExpression.EVERY_DAY_AT_MIDNIGHT,
398
+ });
399
+ });
400
+ vi.advanceTimersByTime(1 * HOUR);
401
+ stop();
402
+ vi.advanceTimersByTime(2 * HOUR);
403
+ expect(spy).toHaveBeenCalledTimes(0);
404
+ vi.useRealTimers();
405
+ await app.teardown();
406
+ });
407
+
408
+ it("does not exec if a next time isn't returned", async () => {
409
+ vi.useFakeTimers();
410
+ const advanceDays = 1;
411
+ vi.setSystemTime(dayjs("2024-09-13T00:00:00.000Z").toDate());
412
+ const spy = vi.fn();
413
+ const app = await TestRunner().run(({ scheduler }) => {
414
+ scheduler.sliding({
415
+ exec: spy,
416
+ next: () => undefined,
417
+ reset: CronExpression.MONDAY_TO_FRIDAY_AT_8AM,
418
+ });
419
+ });
420
+ vi.advanceTimersByTime(advanceDays * HOUR);
421
+ expect(spy).toHaveBeenCalledTimes(0);
422
+ vi.useRealTimers();
423
+ await app.teardown();
424
+ });
425
+
426
+ it("does not exec if a next time is in past", async () => {
427
+ vi.useFakeTimers();
428
+ const advanceDays = 1;
429
+ vi.setSystemTime(dayjs("2024-09-13T00:00:00.000Z").toDate());
430
+ const spy = vi.fn();
431
+ const app = await TestRunner().run(({ scheduler }) => {
432
+ scheduler.sliding({
433
+ exec: spy,
434
+ next: () => dayjs().subtract(1, "minute"),
435
+ reset: CronExpression.MONDAY_TO_FRIDAY_AT_8AM,
436
+ });
437
+ });
438
+ vi.advanceTimersByTime(advanceDays * HOUR);
439
+ expect(spy).toHaveBeenCalledTimes(0);
440
+ vi.useRealTimers();
441
+ await app.teardown();
442
+ });
443
+ });
444
+ });
@@ -271,6 +271,39 @@ describe("Websocket", () => {
271
271
  });
272
272
  });
273
273
 
274
+ describe("Auth Message Handlers", () => {
275
+ it("should handle auth_invalid message and exit", async () => {
276
+ expect.assertions(3);
277
+ const exitSpy = vi
278
+ .spyOn(process, "exit")
279
+ // @ts-expect-error testing
280
+ .mockImplementation(() => {});
281
+
282
+ await hassTestRunner.run(({ lifecycle, hass, logger }) => {
283
+ const fatalSpy = vi.spyOn(logger, "fatal").mockImplementation(() => {});
284
+
285
+ lifecycle.onReady(async () => {
286
+ const message = {
287
+ id: 1,
288
+ type: "auth_invalid" as const,
289
+ };
290
+
291
+ await hass.socket.onMessage(message);
292
+
293
+ expect(exitSpy).toHaveBeenCalled();
294
+ expect(hass.socket.connectionState).toBe("invalid");
295
+ expect(fatalSpy).toHaveBeenCalledWith(
296
+ expect.objectContaining({
297
+ message,
298
+ name: expect.any(Function),
299
+ }),
300
+ "received auth invalid {connecting} => {invalid}",
301
+ );
302
+ });
303
+ });
304
+ });
305
+ });
306
+
274
307
  describe("Subscription Registry", () => {
275
308
  it("should prevent duplicate subscriptions", async () => {
276
309
  expect.assertions(1);
@@ -5,7 +5,7 @@ import { hassTestRunner } from "../mock_assistant/index.mts";
5
5
 
6
6
  describe("Workflows", () => {
7
7
  beforeAll(() => {
8
- hassTestRunner.appendService(({ hass, scheduler }) => {
8
+ hassTestRunner.bootLibrariesFirst().appendService(({ hass, scheduler }) => {
9
9
  scheduler.cron({
10
10
  async exec() {
11
11
  await hass.call.switch.turn_on({