@digital-alchemy/hass 25.11.17-beta.0 → 25.11.27

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 (60) hide show
  1. package/dist/helpers/fetch/service-list.d.mts +86 -5
  2. package/dist/helpers/id-by.d.mts +1 -1
  3. package/dist/helpers/interfaces.d.mts +1 -1
  4. package/dist/helpers/interfaces.mjs.map +1 -1
  5. package/dist/mock_assistant/main.mjs +3 -3
  6. package/dist/mock_assistant/main.mjs.map +1 -1
  7. package/dist/mock_assistant/mock-assistant.module.mjs +2 -2
  8. package/dist/mock_assistant/mock-assistant.module.mjs.map +1 -1
  9. package/dist/mock_assistant/services/fixtures.service.mjs +1 -1
  10. package/dist/mock_assistant/services/fixtures.service.mjs.map +1 -1
  11. package/dist/mock_assistant/services/websocket-api.service.mjs +1 -1
  12. package/dist/mock_assistant/services/websocket-api.service.mjs.map +1 -1
  13. package/dist/services/config.service.mjs +2 -1
  14. package/dist/services/config.service.mjs.map +1 -1
  15. package/dist/services/diagnostics.service.mjs +1 -1
  16. package/dist/services/diagnostics.service.mjs.map +1 -1
  17. package/dist/services/id-by.service.mjs +4 -1
  18. package/dist/services/id-by.service.mjs.map +1 -1
  19. package/dist/services/internal.service.mjs +3 -3
  20. package/dist/services/internal.service.mjs.map +1 -1
  21. package/dist/services/reference.service.mjs +3 -2
  22. package/dist/services/reference.service.mjs.map +1 -1
  23. package/dist/services/websocket-api.service.mjs +1 -1
  24. package/dist/services/websocket-api.service.mjs.map +1 -1
  25. package/dist/testing/area.spec.mjs +143 -2
  26. package/dist/testing/area.spec.mjs.map +1 -1
  27. package/dist/testing/call-proxy.spec.d.mts +1 -0
  28. package/dist/testing/call-proxy.spec.mjs +204 -0
  29. package/dist/testing/call-proxy.spec.mjs.map +1 -0
  30. package/dist/testing/config.spec.mjs +1 -1
  31. package/dist/testing/config.spec.mjs.map +1 -1
  32. package/dist/testing/conversation.spec.mjs +1 -1
  33. package/dist/testing/conversation.spec.mjs.map +1 -1
  34. package/dist/testing/id-by.spec.mjs +38 -0
  35. package/dist/testing/id-by.spec.mjs.map +1 -1
  36. package/dist/testing/ref-by.spec.mjs +4 -3
  37. package/dist/testing/ref-by.spec.mjs.map +1 -1
  38. package/dist/testing/websocket.spec.mjs +25 -0
  39. package/dist/testing/websocket.spec.mjs.map +1 -1
  40. package/package.json +7 -6
  41. package/src/helpers/fetch/service-list.mts +89 -5
  42. package/src/helpers/id-by.mts +1 -0
  43. package/src/helpers/interfaces.mts +2 -1
  44. package/src/mock_assistant/main.mts +4 -3
  45. package/src/mock_assistant/mock-assistant.module.mts +3 -2
  46. package/src/mock_assistant/services/fixtures.service.mts +2 -1
  47. package/src/mock_assistant/services/websocket-api.service.mts +2 -1
  48. package/src/services/config.service.mts +2 -1
  49. package/src/services/diagnostics.service.mts +2 -1
  50. package/src/services/id-by.service.mts +4 -1
  51. package/src/services/internal.service.mts +4 -3
  52. package/src/services/reference.service.mts +3 -2
  53. package/src/services/websocket-api.service.mts +2 -1
  54. package/src/testing/area.spec.mts +168 -3
  55. package/src/testing/call-proxy.spec.mts +241 -0
  56. package/src/testing/config.spec.mts +2 -1
  57. package/src/testing/conversation.spec.mts +1 -1
  58. package/src/testing/id-by.spec.mts +50 -0
  59. package/src/testing/ref-by.spec.mts +4 -3
  60. package/src/testing/websocket.spec.mts +33 -0
@@ -0,0 +1,241 @@
1
+ import type { HassServiceDTO } from "../helpers/index.mts";
2
+ import { hassTestRunner } from "../mock_assistant/index.mts";
3
+
4
+ afterEach(async () => {
5
+ await hassTestRunner.teardown();
6
+ vi.restoreAllMocks();
7
+ });
8
+
9
+ describe("CallProxy", () => {
10
+ describe("pauseMessages", () => {
11
+ it("should return undefined when pauseMessages is true", async () => {
12
+ expect.assertions(2);
13
+ await hassTestRunner.run(({ lifecycle, hass }) => {
14
+ const spy = vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
15
+ hass.socket.pauseMessages = true;
16
+
17
+ lifecycle.onReady(async () => {
18
+ const result = await hass.call.switch.turn_on({
19
+ entity_id: "switch.porch_light",
20
+ });
21
+
22
+ expect(result).toBeUndefined();
23
+ const callServiceCalls = spy.mock.calls.filter(
24
+ call =>
25
+ call[0]?.type === "call_service" &&
26
+ call[0]?.domain === "switch" &&
27
+ call[0]?.service === "turn_on",
28
+ );
29
+ expect(callServiceCalls).toHaveLength(0);
30
+ });
31
+ });
32
+ });
33
+
34
+ it("should call sendMessage when pauseMessages is false", async () => {
35
+ expect.assertions(1);
36
+ await hassTestRunner.run(({ lifecycle, hass }) => {
37
+ const spy = vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
38
+ hass.socket.pauseMessages = false;
39
+
40
+ lifecycle.onReady(async () => {
41
+ await hass.call.switch.turn_on({
42
+ entity_id: "switch.porch_light",
43
+ });
44
+
45
+ expect(spy).toHaveBeenCalledWith(
46
+ expect.objectContaining({
47
+ domain: "switch",
48
+ service: "turn_on",
49
+ service_data: {
50
+ entity_id: "switch.porch_light",
51
+ },
52
+ type: "call_service",
53
+ }),
54
+ true,
55
+ );
56
+ });
57
+ });
58
+ });
59
+ });
60
+
61
+ describe("return_response", () => {
62
+ it("should include return_response in payload when service has response.optional", async () => {
63
+ expect.assertions(1);
64
+ const mockServices: HassServiceDTO[] = [
65
+ {
66
+ domain: "switch",
67
+ services: {
68
+ turn_on: {
69
+ description: "Test service",
70
+ fields: {},
71
+ name: "turn_on",
72
+ response: { optional: true },
73
+ },
74
+ },
75
+ },
76
+ ];
77
+
78
+ await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
79
+ mock_assistant.services.loadFixtures(mockServices);
80
+ vi.spyOn(hass.fetch, "listServices").mockImplementation(async () => mockServices);
81
+
82
+ const spy = vi
83
+ .spyOn(hass.socket, "sendMessage")
84
+ .mockImplementation(async () => ({ response: { success: true } }));
85
+
86
+ lifecycle.onReady(async () => {
87
+ await hass.call.switch.turn_on({ entity_id: "switch.bedroom_lamp" });
88
+
89
+ expect(spy).toHaveBeenCalledWith(
90
+ expect.objectContaining({
91
+ domain: "switch",
92
+ return_response: true,
93
+ service: "turn_on",
94
+ type: "call_service",
95
+ }),
96
+ true,
97
+ );
98
+ });
99
+ });
100
+ });
101
+
102
+ it("should return response when service returns a response", async () => {
103
+ expect.assertions(1);
104
+ const mockServices: HassServiceDTO[] = [
105
+ {
106
+ domain: "switch",
107
+ services: {
108
+ turn_on: {
109
+ description: "Test service",
110
+ fields: {},
111
+ name: "turn_on",
112
+ response: { optional: true },
113
+ },
114
+ },
115
+ },
116
+ ];
117
+
118
+ await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
119
+ mock_assistant.services.loadFixtures(mockServices);
120
+ vi.spyOn(hass.fetch, "listServices").mockImplementation(async () => mockServices);
121
+
122
+ const mockResponse = { data: "test_response" };
123
+ vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => ({
124
+ response: mockResponse,
125
+ }));
126
+
127
+ lifecycle.onReady(async () => {
128
+ const result = await hass.call.switch.turn_on({ entity_id: "switch.bedroom_lamp" });
129
+ expect(result).toEqual(mockResponse);
130
+ });
131
+ });
132
+ });
133
+
134
+ it("should return undefined when service does not return a response", async () => {
135
+ expect.assertions(1);
136
+ const mockServices: HassServiceDTO[] = [
137
+ {
138
+ domain: "switch",
139
+ services: {
140
+ turn_on: {
141
+ description: "Test service",
142
+ fields: {},
143
+ name: "turn_on",
144
+ response: { optional: true },
145
+ },
146
+ },
147
+ },
148
+ ];
149
+
150
+ await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
151
+ mock_assistant.services.loadFixtures(mockServices);
152
+ vi.spyOn(hass.fetch, "listServices").mockImplementation(async () => mockServices);
153
+
154
+ vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => ({}));
155
+
156
+ lifecycle.onReady(async () => {
157
+ const result = await hass.call.switch.turn_on({ entity_id: "switch.bedroom_lamp" });
158
+ expect(result).toBeUndefined();
159
+ });
160
+ });
161
+ });
162
+ });
163
+
164
+ describe("early access", () => {
165
+ it("should call console.trace when accessed before load with LOG_LEVEL trace", async () => {
166
+ expect.assertions(1);
167
+ const traceSpy = vi.spyOn(console, "trace").mockImplementation(() => {});
168
+
169
+ await hassTestRunner.configure({ boilerplate: { LOG_LEVEL: "trace" } }).run(({ hass }) => {
170
+ hass.call.switch;
171
+ });
172
+ expect(traceSpy).toHaveBeenCalledWith(`hass.call`);
173
+ });
174
+
175
+ it("should not log error when accessed before load with LOG_LEVEL not trace", async () => {
176
+ expect.assertions(2);
177
+ await hassTestRunner
178
+ .configure({ boilerplate: { LOG_LEVEL: "warn" } })
179
+ .run(({ lifecycle, hass, internal }) => {
180
+ const logger = internal.boilerplate.logger.getBaseLogger();
181
+ const errorSpy = vi.spyOn(logger, "error").mockImplementation(() => {});
182
+ const traceSpy = vi.spyOn(console, "trace").mockImplementation(() => {});
183
+
184
+ // Access hass.call in onPreInit, which runs before onBootstrap (where services load)
185
+ hass.call.switch;
186
+
187
+ lifecycle.onReady(() => {
188
+ expect(errorSpy).not.toHaveBeenCalled();
189
+ expect(traceSpy).not.toHaveBeenCalled();
190
+ });
191
+ });
192
+ });
193
+ });
194
+
195
+ describe("proxy methods", () => {
196
+ it("should return true for has when domain exists", async () => {
197
+ expect.assertions(1);
198
+ await hassTestRunner.run(({ lifecycle, hass }) => {
199
+ lifecycle.onReady(() => {
200
+ expect("switch" in hass.call).toBe(true);
201
+ });
202
+ });
203
+ });
204
+
205
+ it("should return false for has when domain does not exist", async () => {
206
+ expect.assertions(1);
207
+ await hassTestRunner.run(({ lifecycle, hass }) => {
208
+ lifecycle.onReady(() => {
209
+ expect("nonexistent_domain" in hass.call).toBe(false);
210
+ });
211
+ });
212
+ });
213
+
214
+ it("should return ownKeys with all domain keys", async () => {
215
+ expect.assertions(2);
216
+ await hassTestRunner.run(({ lifecycle, hass }) => {
217
+ lifecycle.onReady(() => {
218
+ const keys = Object.keys(hass.call);
219
+ expect(keys).toContain("switch");
220
+ expect(keys.length).toBeGreaterThan(0);
221
+ });
222
+ });
223
+ });
224
+
225
+ it("should return false when trying to set a property", async () => {
226
+ expect.assertions(1);
227
+ await hassTestRunner.run(({ lifecycle, hass }) => {
228
+ lifecycle.onReady(() => {
229
+ try {
230
+ // @ts-expect-error testing
231
+ hass.call.test_domain = {};
232
+ } catch {
233
+ // Some environments throw, others return false
234
+ }
235
+ // Verify the property was not set
236
+ expect("test_domain" in hass.call).toBe(false);
237
+ });
238
+ });
239
+ });
240
+ });
241
+ });
@@ -1,4 +1,5 @@
1
- import { env } from "process";
1
+ import { env } from "node:process";
2
+
2
3
  import type { MockInstance } from "vitest";
3
4
 
4
5
  import type { ConfigEntry, HassServiceDTO } from "../helpers/index.mts";
@@ -18,7 +18,7 @@ describe("Conversation Service", () => {
18
18
  },
19
19
  ];
20
20
 
21
- await hassTestRunner.run(({ lifecycle, hass }) => {
21
+ await hassTestRunner.bootLibrariesFirst().run(({ lifecycle, hass }) => {
22
22
  const spy = vi
23
23
  .spyOn(hass.socket, "sendMessage")
24
24
  .mockImplementation(async () => ({ agents: mockAgents }));
@@ -137,6 +137,56 @@ describe("enabled entities", () => {
137
137
  });
138
138
  });
139
139
  });
140
+
141
+ describe("unique_id", () => {
142
+ it("find entity by unique_id without platform", async () => {
143
+ expect.assertions(1);
144
+ await hassTestRunner.run(({ lifecycle, hass }) => {
145
+ lifecycle.onReady(() => {
146
+ const entity = hass.idBy.unique_id(
147
+ "e1806fdc93296bbd5ab42967003cd38729ff9ba6cfeefc3e15a03ad01ac894fe",
148
+ );
149
+ expect(entity).toBe("sensor.magic");
150
+ });
151
+ });
152
+ });
153
+
154
+ it("find entity by unique_id with matching platform", async () => {
155
+ expect.assertions(1);
156
+ await hassTestRunner.run(({ lifecycle, hass }) => {
157
+ lifecycle.onReady(() => {
158
+ const entity = hass.idBy.unique_id(
159
+ "e1806fdc93296bbd5ab42967003cd38729ff9ba6cfeefc3e15a03ad01ac894fe",
160
+ "synapse",
161
+ );
162
+ expect(entity).toBe("sensor.magic");
163
+ });
164
+ });
165
+ });
166
+
167
+ it("find entity by unique_id with non-matching platform", async () => {
168
+ expect.assertions(1);
169
+ await hassTestRunner.run(({ lifecycle, hass }) => {
170
+ lifecycle.onReady(() => {
171
+ const entity = hass.idBy.unique_id(
172
+ "e1806fdc93296bbd5ab42967003cd38729ff9ba6cfeefc3e15a03ad01ac894fe",
173
+ "sun",
174
+ );
175
+ expect(entity).toBeUndefined();
176
+ });
177
+ });
178
+ });
179
+
180
+ it("find entity by unique_id with different platform", async () => {
181
+ expect.assertions(1);
182
+ await hassTestRunner.run(({ lifecycle, hass }) => {
183
+ lifecycle.onReady(() => {
184
+ const entity = hass.idBy.unique_id("5622d76001a335e3ea893c4d60d31b3d-next_dawn", "sun");
185
+ expect(entity).toBe("sensor.sun_next_dawn");
186
+ });
187
+ });
188
+ });
189
+ });
140
190
  });
141
191
 
142
192
  describe("disabled entities", () => {
@@ -174,7 +174,7 @@ describe("References", () => {
174
174
  describe("functionality", () => {
175
175
  describe("operators", () => {
176
176
  it("has", async () => {
177
- expect.assertions(16);
177
+ expect.assertions(18);
178
178
  await hassTestRunner.run(({ lifecycle, hass }) => {
179
179
  lifecycle.onReady(() => {
180
180
  const entity = hass.refBy.id("switch.bedroom_lamp");
@@ -184,7 +184,9 @@ describe("References", () => {
184
184
  "attributes",
185
185
  "entity_id",
186
186
  "history",
187
- "last",
187
+ "last_changed",
188
+ "last_reported",
189
+ "last_updated",
188
190
  "nextState",
189
191
  "once",
190
192
  "onStateFor",
@@ -219,7 +221,6 @@ describe("References", () => {
219
221
  "last_updated",
220
222
  "context",
221
223
  "history",
222
- "last",
223
224
  "nextState",
224
225
  "once",
225
226
  "onStateFor",
@@ -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);