matterbridge-lg-thinq-plugin 0.1.0

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 (210) hide show
  1. package/.claude/CLAUDE.md +43 -0
  2. package/.claude/agents/compiler.md +91 -0
  3. package/.claude/agents/direct-executor.md +98 -0
  4. package/.claude/agents/documenter.md +78 -0
  5. package/.claude/agents/finalizer.md +233 -0
  6. package/.claude/agents/implementer.md +112 -0
  7. package/.claude/agents/investigator.md +100 -0
  8. package/.claude/agents/release-manager.md +203 -0
  9. package/.claude/agents/reviewer.md +131 -0
  10. package/.claude/agents/technical-architect.md +403 -0
  11. package/.claude/agents/test-writer.md +159 -0
  12. package/.claude/agents/wiki-manager.md +147 -0
  13. package/.claude/instructions/agent-prompts.md +90 -0
  14. package/.claude/instructions/shared-rules.md +11 -0
  15. package/.claude/instructions/team-orchestrator-policy.md +165 -0
  16. package/.claude/memory.md +186 -0
  17. package/.claude/output-styles/engineer-manager.md +38 -0
  18. package/.claude/settings.json +40 -0
  19. package/.claude/settings.local.json +30 -0
  20. package/.claude/skills/load-policy/SKILL.md +13 -0
  21. package/.claude/skills/ref-idea/SKILL.md +44 -0
  22. package/.claude/skills/status-of/SKILL.md +38 -0
  23. package/.claude/skills/workspace-console/SKILL.md +49 -0
  24. package/.claude/statusline-command.sh +130 -0
  25. package/.claude/statusline.md +53 -0
  26. package/.claude/templates/approval-packet.template.html +171 -0
  27. package/.claude/templates/reference-workspaces.md +16 -0
  28. package/.claude/tools/cli-runner-server.cjs +220 -0
  29. package/.claude/tools/discord-send-server.cjs +188 -0
  30. package/.claude/tools/github-release-server.cjs +181 -0
  31. package/.claude/tools/glob-grep-server.cjs +267 -0
  32. package/.claude/tools/read-log-server.cjs +175 -0
  33. package/.github/codeql/codeql-config.yml +14 -0
  34. package/.github/workflows/build.yml +54 -0
  35. package/.github/workflows/codeql.yml +59 -0
  36. package/.github/workflows/coverage.yml +70 -0
  37. package/.github/workflows/publish.yml +103 -0
  38. package/.mcp.json +34 -0
  39. package/CHANGELOG.md +195 -0
  40. package/LICENSE +21 -0
  41. package/dist/cli/commands/devices.js +52 -0
  42. package/dist/cli/commands/energy.js +122 -0
  43. package/dist/cli/commands/login.js +54 -0
  44. package/dist/cli/help.js +50 -0
  45. package/dist/cli/main.js +56 -0
  46. package/dist/cli/session.js +13 -0
  47. package/dist/cli/types.js +1 -0
  48. package/dist/cli/utils.js +29 -0
  49. package/dist/cli.js +2 -0
  50. package/dist/constants/index.js +1 -0
  51. package/dist/core/domain/entities/ThinqDevice.js +43 -0
  52. package/dist/core/domain/value-objects/AirConditionerCapabilities.js +22 -0
  53. package/dist/core/domain/value-objects/ThinqFilterState.js +35 -0
  54. package/dist/core/domain/value-objects/ThinqSnapshot.js +125 -0
  55. package/dist/errors/AuthenticationError.js +26 -0
  56. package/dist/errors/BaseError.js +23 -0
  57. package/dist/errors/index.js +2 -0
  58. package/dist/model/LgThinkqPluginPlatformConfig.js +23 -0
  59. package/dist/model/LgThinqPluginPlatformConfig.js +23 -0
  60. package/dist/module.js +212 -0
  61. package/dist/platform/deviceRegistry.js +26 -0
  62. package/dist/platform/platformConfigManager.js +89 -0
  63. package/dist/platform/platformState.js +9 -0
  64. package/dist/platform/thinq/thinqAirConditionerCommandHandlers.js +190 -0
  65. package/dist/platform/thinq/thinqAirConditionerEndpointFactory.js +53 -0
  66. package/dist/platform/thinq/thinqAirConditionerFilterResetCommandHandler.js +54 -0
  67. package/dist/platform/thinq/thinqAirConditionerFilterStateSync.js +19 -0
  68. package/dist/platform/thinq/thinqAirConditionerStateSync.js +205 -0
  69. package/dist/platform/thinq/thinqDeviceConfigReconciler.js +26 -0
  70. package/dist/platform/thinq/thinqDeviceConfigurator.js +124 -0
  71. package/dist/platform/thinq/thinqDeviceDiscovery.js +33 -0
  72. package/dist/platform/thinq/thinqWasherCommandHandlers.js +54 -0
  73. package/dist/platform/thinq/thinqWasherEndpointFactory.js +6 -0
  74. package/dist/platform/thinq/thinqWasherStateSync.js +31 -0
  75. package/dist/platform/thinq/thinqWasherStopCommandResolver.js +38 -0
  76. package/dist/services/authentication/AccountAuthStrategy.js +135 -0
  77. package/dist/services/authentication/AuthContext.js +1 -0
  78. package/dist/services/authentication/AuthenticationCoordinator.js +29 -0
  79. package/dist/services/authentication/IAuthStrategy.js +1 -0
  80. package/dist/services/authentication/TokenAuthStrategy.js +27 -0
  81. package/dist/services/authentication/UserDataRepository.js +32 -0
  82. package/dist/services/thinq/gateway.js +61 -0
  83. package/dist/services/thinq/mqtt/mqttCertificate.js +63 -0
  84. package/dist/services/thinq/mqtt/mqttConnectionEvents.js +37 -0
  85. package/dist/services/thinq/mqtt/mqttKeyRepository.js +33 -0
  86. package/dist/services/thinq/mqtt/mqttRetry.js +25 -0
  87. package/dist/services/thinq/mqtt/thinqMqttListener.js +97 -0
  88. package/dist/services/thinq/serviceContainer.js +73 -0
  89. package/dist/services/thinq/session.js +48 -0
  90. package/dist/services/thinq/thinqApiClient.js +258 -0
  91. package/dist/services/thinq/thinqDeviceService.js +75 -0
  92. package/dist/services/thinq/thinqFilterMonitoringService.js +40 -0
  93. package/dist/settings.js +9 -0
  94. package/dist/tests/helpers/testUtils.js +87 -0
  95. package/dist/utils/mask.js +39 -0
  96. package/docs/README_CLI.md +166 -0
  97. package/eslint.config.js +194 -0
  98. package/matterbridge-lg-thinq-plugin.config.json +27 -0
  99. package/matterbridge-lg-thinq-plugin.schema.json +520 -0
  100. package/package.json +107 -0
  101. package/prettier.config.js +49 -0
  102. package/scripts/parse-junit-failures.mjs +45 -0
  103. package/scripts/parse-tsc-failures.mjs +48 -0
  104. package/scripts/run-build-local-summary.mjs +29 -0
  105. package/scripts/run-format-summary.mjs +36 -0
  106. package/scripts/run-lint-fix-summary.mjs +38 -0
  107. package/scripts/run-precommit-summary.mjs +94 -0
  108. package/src/cli/commands/devices.ts +67 -0
  109. package/src/cli/commands/energy.ts +160 -0
  110. package/src/cli/commands/login.ts +74 -0
  111. package/src/cli/help.ts +53 -0
  112. package/src/cli/main.ts +66 -0
  113. package/src/cli/session.ts +15 -0
  114. package/src/cli/types.ts +10 -0
  115. package/src/cli/utils.ts +30 -0
  116. package/src/cli.ts +3 -0
  117. package/src/constants/index.ts +8 -0
  118. package/src/core/domain/entities/ThinqDevice.test.ts +224 -0
  119. package/src/core/domain/entities/ThinqDevice.ts +74 -0
  120. package/src/core/domain/value-objects/AirConditionerCapabilities.ts +50 -0
  121. package/src/core/domain/value-objects/ThinqFilterState.ts +50 -0
  122. package/src/core/domain/value-objects/ThinqSnapshot.ts +162 -0
  123. package/src/errors/AuthenticationError.ts +37 -0
  124. package/src/errors/BaseError.ts +38 -0
  125. package/src/errors/index.ts +24 -0
  126. package/src/model/LgThinqPluginPlatformConfig.ts +94 -0
  127. package/src/module.ts +298 -0
  128. package/src/platform/deviceRegistry.ts +58 -0
  129. package/src/platform/platformConfigManager.ts +133 -0
  130. package/src/platform/platformState.ts +16 -0
  131. package/src/platform/thinq/thinqAirConditionerCommandHandlers.ts +291 -0
  132. package/src/platform/thinq/thinqAirConditionerEndpointFactory.ts +145 -0
  133. package/src/platform/thinq/thinqAirConditionerFilterResetCommandHandler.ts +96 -0
  134. package/src/platform/thinq/thinqAirConditionerFilterStateSync.ts +42 -0
  135. package/src/platform/thinq/thinqAirConditionerStateSync.ts +343 -0
  136. package/src/platform/thinq/thinqDeviceConfigReconciler.ts +42 -0
  137. package/src/platform/thinq/thinqDeviceConfigurator.test.ts +90 -0
  138. package/src/platform/thinq/thinqDeviceConfigurator.ts +191 -0
  139. package/src/platform/thinq/thinqDeviceDiscovery.test.ts +173 -0
  140. package/src/platform/thinq/thinqDeviceDiscovery.ts +51 -0
  141. package/src/platform/thinq/thinqWasherCommandHandlers.ts +120 -0
  142. package/src/platform/thinq/thinqWasherEndpointFactory.ts +37 -0
  143. package/src/platform/thinq/thinqWasherStateSync.ts +60 -0
  144. package/src/platform/thinq/thinqWasherStopCommandResolver.ts +62 -0
  145. package/src/services/authentication/AccountAuthStrategy.test.ts +358 -0
  146. package/src/services/authentication/AccountAuthStrategy.ts +209 -0
  147. package/src/services/authentication/AuthContext.ts +19 -0
  148. package/src/services/authentication/AuthenticationCoordinator.test.ts +217 -0
  149. package/src/services/authentication/AuthenticationCoordinator.ts +59 -0
  150. package/src/services/authentication/IAuthStrategy.ts +10 -0
  151. package/src/services/authentication/TokenAuthStrategy.test.ts +167 -0
  152. package/src/services/authentication/TokenAuthStrategy.ts +40 -0
  153. package/src/services/authentication/UserDataRepository.test.ts +149 -0
  154. package/src/services/authentication/UserDataRepository.ts +46 -0
  155. package/src/services/thinq/gateway.ts +86 -0
  156. package/src/services/thinq/mqtt/mqttCertificate.test.ts +344 -0
  157. package/src/services/thinq/mqtt/mqttCertificate.ts +102 -0
  158. package/src/services/thinq/mqtt/mqttConnectionEvents.test.ts +276 -0
  159. package/src/services/thinq/mqtt/mqttConnectionEvents.ts +76 -0
  160. package/src/services/thinq/mqtt/mqttKeyRepository.test.ts +164 -0
  161. package/src/services/thinq/mqtt/mqttKeyRepository.ts +43 -0
  162. package/src/services/thinq/mqtt/mqttRetry.test.ts +239 -0
  163. package/src/services/thinq/mqtt/mqttRetry.ts +43 -0
  164. package/src/services/thinq/mqtt/thinqMqttListener.test.ts +349 -0
  165. package/src/services/thinq/mqtt/thinqMqttListener.ts +120 -0
  166. package/src/services/thinq/serviceContainer.test.ts +203 -0
  167. package/src/services/thinq/serviceContainer.ts +105 -0
  168. package/src/services/thinq/session.test.ts +287 -0
  169. package/src/services/thinq/session.ts +69 -0
  170. package/src/services/thinq/thinqApiClient.test.ts +702 -0
  171. package/src/services/thinq/thinqApiClient.ts +418 -0
  172. package/src/services/thinq/thinqDeviceService.test.ts +415 -0
  173. package/src/services/thinq/thinqDeviceService.ts +101 -0
  174. package/src/services/thinq/thinqFilterMonitoringService.test.ts +264 -0
  175. package/src/services/thinq/thinqFilterMonitoringService.ts +53 -0
  176. package/src/settings.ts +18 -0
  177. package/src/tests/cli/commands/devices.test.ts +293 -0
  178. package/src/tests/cli/commands/login.test.ts +240 -0
  179. package/src/tests/cli/help.test.ts +46 -0
  180. package/src/tests/cli/main.test.ts +214 -0
  181. package/src/tests/cli/session.test.ts +198 -0
  182. package/src/tests/cli/utils.test.ts +194 -0
  183. package/src/tests/core/domain/value-objects/AirConditionerCapabilities.test.ts +685 -0
  184. package/src/tests/core/domain/value-objects/ThinqFilterState.test.ts +210 -0
  185. package/src/tests/core/domain/value-objects/ThinqSnapshot.test.ts +1035 -0
  186. package/src/tests/helpers/testUtils.ts +118 -0
  187. package/src/tests/model/LgThinqPluginPlatformConfig.test.ts +81 -0
  188. package/src/tests/platform/deviceRegistry.test.ts +177 -0
  189. package/src/tests/platform/platformConfigManager.test.ts +790 -0
  190. package/src/tests/platform/platformState.test.ts +41 -0
  191. package/src/tests/platform/thinq/thinqAirConditionerCommandHandlers.test.ts +879 -0
  192. package/src/tests/platform/thinq/thinqAirConditionerEndpointFactory.test.ts +993 -0
  193. package/src/tests/platform/thinq/thinqAirConditionerFilterResetCommandHandler.test.ts +508 -0
  194. package/src/tests/platform/thinq/thinqAirConditionerFilterStateSync.test.ts +212 -0
  195. package/src/tests/platform/thinq/thinqAirConditionerStateSync.test.ts +1586 -0
  196. package/src/tests/platform/thinq/thinqDeviceConfigReconciler.test.ts +269 -0
  197. package/src/tests/platform/thinq/thinqDeviceConfigurator.test.ts +492 -0
  198. package/src/tests/platform/thinq/thinqWasherCommandHandlers.test.ts +594 -0
  199. package/src/tests/platform/thinq/thinqWasherEndpointFactory.test.ts +84 -0
  200. package/src/tests/platform/thinq/thinqWasherStateSync.test.ts +373 -0
  201. package/src/tests/platform/thinq/thinqWasherStopCommandResolver.test.ts +392 -0
  202. package/src/tests/settings.test.ts +33 -0
  203. package/src/utils/mask.ts +50 -0
  204. package/tsconfig.base.json +25 -0
  205. package/tsconfig.json +9 -0
  206. package/tsconfig.production.json +13 -0
  207. package/tsconfig.vitest.json +5 -0
  208. package/vite.config.ts +43 -0
  209. package/wiki/memory-archive.md +12 -0
  210. package/wiki/reference-workspaces.md +23 -0
@@ -0,0 +1,240 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ let mockCoordinator: any;
4
+
5
+ vi.mock('../../../services/authentication/AccountAuthStrategy.js', () => ({
6
+ AccountAuthStrategy: vi.fn(),
7
+ }));
8
+
9
+ vi.mock('../../../services/authentication/TokenAuthStrategy.js', () => ({
10
+ TokenAuthStrategy: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('../../../services/authentication/AuthenticationCoordinator.js', () => ({
14
+ AuthenticationCoordinator: vi.fn(function () {
15
+ if (!mockCoordinator) {
16
+ mockCoordinator = { authenticate: vi.fn() };
17
+ }
18
+ return mockCoordinator;
19
+ }),
20
+ }));
21
+
22
+ vi.mock('../../../services/thinq/thinqApiClient.js', () => ({
23
+ ThinqApiClient: vi.fn(),
24
+ }));
25
+
26
+ vi.mock('../../../services/thinq/session.js', () => ({
27
+ ThinqSession: vi.fn(),
28
+ }));
29
+
30
+ vi.mock('../../../cli/session.js', () => ({
31
+ saveSession: vi.fn(),
32
+ }));
33
+
34
+ vi.mock('../../../cli/utils.js', () => ({
35
+ maskSecret: vi.fn((secret: string) => '*'.repeat(secret?.length || 0)),
36
+ parseArgs: vi.fn(),
37
+ prompt: vi.fn(),
38
+ }));
39
+
40
+ import { cmdLogin } from '../../../cli/commands/login.js';
41
+ import { saveSession } from '../../../cli/session.js';
42
+ import { maskSecret, prompt } from '../../../cli/utils.js';
43
+ import { createMockLogger } from '../../helpers/testUtils.js';
44
+
45
+ describe('cmdLogin', () => {
46
+ let mockLogger: any;
47
+ let consoleLogSpy: any;
48
+ let consoleErrorSpy: any;
49
+
50
+ beforeEach(() => {
51
+ vi.clearAllMocks();
52
+ mockLogger = createMockLogger();
53
+ mockCoordinator = {
54
+ authenticate: vi.fn(),
55
+ };
56
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
57
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
58
+ vi.mocked(prompt).mockResolvedValue('mocked_answer');
59
+ vi.mocked(maskSecret).mockImplementation((secret?: string) => '*'.repeat(secret?.length || 0));
60
+ });
61
+
62
+ afterEach(() => {
63
+ vi.clearAllMocks();
64
+ consoleLogSpy.mockRestore();
65
+ consoleErrorSpy.mockRestore();
66
+ });
67
+
68
+ it('should prompt for refresh token when type is "token"', async () => {
69
+ // Arrange
70
+ vi.mocked(prompt).mockResolvedValueOnce('refresh_token_value');
71
+ mockCoordinator.authenticate.mockResolvedValue(undefined);
72
+
73
+ // Act
74
+ await cmdLogin('token', 'US', 'en-US', mockLogger);
75
+
76
+ // Assert
77
+ expect(prompt).toHaveBeenCalledWith('Refresh token: ');
78
+ });
79
+
80
+ it('should prompt for username and password when type is "account"', async () => {
81
+ // Arrange
82
+ vi.mocked(prompt).mockResolvedValueOnce('user@example.com').mockResolvedValueOnce('password123');
83
+ mockCoordinator.authenticate.mockResolvedValue(undefined);
84
+
85
+ // Act
86
+ await cmdLogin('account', 'US', 'en-US', mockLogger);
87
+
88
+ // Assert
89
+ expect(prompt).toHaveBeenNthCalledWith(1, 'Username (email): ');
90
+ expect(prompt).toHaveBeenNthCalledWith(2, 'Password: ');
91
+ });
92
+
93
+ it('should error when authentication returns undefined', async () => {
94
+ // Arrange
95
+ vi.mocked(prompt).mockResolvedValue('answer');
96
+ mockCoordinator.authenticate.mockResolvedValue(undefined);
97
+
98
+ // Act
99
+ await cmdLogin('account', 'US', 'en-US', mockLogger);
100
+
101
+ // Assert
102
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
103
+ 'Login did not complete: no session data was returned (a manual process may be required).',
104
+ );
105
+ expect(saveSession).not.toHaveBeenCalled();
106
+ });
107
+
108
+ it('should save session when authentication succeeds', async () => {
109
+ // Arrange
110
+ const mockUserData = {
111
+ accessToken: 'access_token_123',
112
+ refreshToken: 'refresh_token_456',
113
+ expiresAtEpochSeconds: 1609459200,
114
+ country: 'US',
115
+ language: 'en-US',
116
+ };
117
+ vi.mocked(prompt).mockResolvedValue('answer');
118
+ mockCoordinator.authenticate.mockResolvedValue(mockUserData);
119
+
120
+ // Act
121
+ await cmdLogin('account', 'US', 'en-US', mockLogger);
122
+
123
+ // Assert
124
+ expect(saveSession).toHaveBeenCalledWith({
125
+ loginType: 'account',
126
+ country: 'US',
127
+ language: 'en-US',
128
+ userData: mockUserData,
129
+ });
130
+ });
131
+
132
+ it('should print success message', async () => {
133
+ // Arrange
134
+ const mockUserData = {
135
+ accessToken: 'token',
136
+ refreshToken: 'refresh',
137
+ expiresAtEpochSeconds: 1609459200,
138
+ country: 'US',
139
+ language: 'en-US',
140
+ };
141
+ vi.mocked(prompt).mockResolvedValue('answer');
142
+ mockCoordinator.authenticate.mockResolvedValue(mockUserData);
143
+
144
+ // Act
145
+ await cmdLogin('account', 'US', 'en-US', mockLogger);
146
+
147
+ // Assert
148
+ expect(consoleLogSpy).toHaveBeenCalledWith('Login successful. Session saved to .cli-session.json');
149
+ expect(consoleLogSpy).toHaveBeenCalledWith('Session Summary:');
150
+ });
151
+
152
+ it('should call maskSecret for tokens', async () => {
153
+ // Arrange
154
+ const mockUserData = {
155
+ accessToken: 'access_token_value',
156
+ refreshToken: 'refresh_token_value',
157
+ expiresAtEpochSeconds: 1609459200,
158
+ country: 'US',
159
+ language: 'en-US',
160
+ };
161
+ vi.mocked(prompt).mockResolvedValue('answer');
162
+ mockCoordinator.authenticate.mockResolvedValue(mockUserData);
163
+
164
+ // Act
165
+ await cmdLogin('token', 'US', 'en-US', mockLogger);
166
+
167
+ // Assert
168
+ expect(maskSecret).toHaveBeenCalledWith('access_token_value');
169
+ expect(maskSecret).toHaveBeenCalledWith('refresh_token_value');
170
+ });
171
+
172
+ it('should print formatted expiration date', async () => {
173
+ // Arrange
174
+ const mockUserData = {
175
+ accessToken: 'token',
176
+ refreshToken: 'refresh',
177
+ expiresAtEpochSeconds: 1609459200,
178
+ country: 'US',
179
+ language: 'en-US',
180
+ };
181
+ vi.mocked(prompt).mockResolvedValue('answer');
182
+ mockCoordinator.authenticate.mockResolvedValue(mockUserData);
183
+
184
+ // Act
185
+ await cmdLogin('account', 'US', 'en-US', mockLogger);
186
+
187
+ // Assert
188
+ const expectedDate = new Date(1609459200 * 1000).toISOString();
189
+ expect(consoleLogSpy).toHaveBeenCalledWith(` Expires At: ${expectedDate}`);
190
+ });
191
+
192
+ it('should build auth context for token type', async () => {
193
+ // Arrange
194
+ vi.mocked(prompt).mockResolvedValueOnce('my_refresh_token');
195
+ mockCoordinator.authenticate.mockResolvedValue({
196
+ accessToken: 'token',
197
+ refreshToken: 'refresh',
198
+ expiresAtEpochSeconds: 123,
199
+ country: 'US',
200
+ language: 'en-US',
201
+ });
202
+
203
+ // Act
204
+ await cmdLogin('token', 'KR', 'ko-KR', mockLogger);
205
+
206
+ // Assert
207
+ const callArgs = mockCoordinator.authenticate.mock.calls[0];
208
+ expect(callArgs[0]).toBe('token');
209
+ expect(callArgs[1]).toEqual({
210
+ refreshToken: 'my_refresh_token',
211
+ country: 'KR',
212
+ language: 'ko-KR',
213
+ });
214
+ });
215
+
216
+ it('should build auth context for account type', async () => {
217
+ // Arrange
218
+ vi.mocked(prompt).mockResolvedValueOnce('user@email.com').mockResolvedValueOnce('pass123');
219
+ mockCoordinator.authenticate.mockResolvedValue({
220
+ accessToken: 'token',
221
+ refreshToken: 'refresh',
222
+ expiresAtEpochSeconds: 123,
223
+ country: 'US',
224
+ language: 'en-US',
225
+ });
226
+
227
+ // Act
228
+ await cmdLogin('account', 'KR', 'ko-KR', mockLogger);
229
+
230
+ // Assert
231
+ const callArgs = mockCoordinator.authenticate.mock.calls[0];
232
+ expect(callArgs[0]).toBe('account');
233
+ expect(callArgs[1]).toEqual({
234
+ username: 'user@email.com',
235
+ password: 'pass123',
236
+ country: 'KR',
237
+ language: 'ko-KR',
238
+ });
239
+ });
240
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { HELP_TEXT } from '../../cli/help.js';
4
+
5
+ describe('HELP_TEXT', () => {
6
+ it('should be a non-empty string', () => {
7
+ expect(HELP_TEXT).toBeDefined();
8
+ expect(typeof HELP_TEXT).toBe('string');
9
+ expect(HELP_TEXT.length).toBeGreaterThan(0);
10
+ });
11
+
12
+ it('should contain the "Usage:" section header', () => {
13
+ expect(HELP_TEXT).toContain('Usage:');
14
+ });
15
+
16
+ it('should contain the "Options:" section header', () => {
17
+ expect(HELP_TEXT).toContain('Options:');
18
+ });
19
+
20
+ it('should contain the "Commands:" section header', () => {
21
+ expect(HELP_TEXT).toContain('Commands:');
22
+ });
23
+
24
+ it('should contain the "Examples:" section header', () => {
25
+ expect(HELP_TEXT).toContain('Examples:');
26
+ });
27
+
28
+ it('should contain all three command names', () => {
29
+ expect(HELP_TEXT).toContain('login');
30
+ expect(HELP_TEXT).toContain('devices');
31
+ expect(HELP_TEXT).toContain('help');
32
+ });
33
+
34
+ it('should contain all option flags', () => {
35
+ expect(HELP_TEXT).toContain('--type');
36
+ expect(HELP_TEXT).toContain('--country');
37
+ expect(HELP_TEXT).toContain('--language');
38
+ expect(HELP_TEXT).toContain('--help');
39
+ expect(HELP_TEXT).toContain('--debug');
40
+ });
41
+
42
+ it('should contain descriptions for commands', () => {
43
+ expect(HELP_TEXT).toContain('Authenticate with LG ThinQ');
44
+ expect(HELP_TEXT).toContain('List all discovered ThinQ devices');
45
+ });
46
+ });
@@ -0,0 +1,214 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ vi.mock('../../cli/commands/devices.js');
4
+ vi.mock('../../cli/commands/login.js');
5
+ vi.mock('../../cli/help.js');
6
+ vi.mock('../../cli/session.js');
7
+ vi.mock('../../cli/utils.js');
8
+
9
+ import { cmdDevices } from '../../cli/commands/devices.js';
10
+ import { cmdLogin } from '../../cli/commands/login.js';
11
+ import { HELP_TEXT } from '../../cli/help.js';
12
+ import { main } from '../../cli/main.js';
13
+ import { loadSession } from '../../cli/session.js';
14
+ import { parseArgs } from '../../cli/utils.js';
15
+
16
+ describe('main', () => {
17
+ let consoleLogSpy: any;
18
+ let consoleErrorSpy: any;
19
+ let processExitSpy: any;
20
+
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
24
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
25
+ processExitSpy = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never);
26
+ });
27
+
28
+ afterEach(() => {
29
+ vi.clearAllMocks();
30
+ consoleLogSpy.mockRestore();
31
+ consoleErrorSpy.mockRestore();
32
+ processExitSpy.mockRestore();
33
+ });
34
+
35
+ it('should print help and exit with 1 when no command is provided', async () => {
36
+ // Arrange
37
+ vi.mocked(parseArgs).mockReturnValue({ command: '' });
38
+
39
+ // Act
40
+ await main();
41
+
42
+ // Assert
43
+ expect(consoleLogSpy).toHaveBeenCalledWith(HELP_TEXT);
44
+ expect(processExitSpy).toHaveBeenCalledWith(1);
45
+ });
46
+
47
+ it('should print help and exit with 0 when command is "help"', async () => {
48
+ // Arrange
49
+ vi.mocked(parseArgs).mockReturnValue({ command: 'help' });
50
+
51
+ // Act
52
+ await main();
53
+
54
+ // Assert
55
+ expect(consoleLogSpy).toHaveBeenCalledWith(HELP_TEXT);
56
+ expect(processExitSpy).toHaveBeenCalledWith(0);
57
+ });
58
+
59
+ it('should print help and exit with 0 when --help flag is set', async () => {
60
+ // Arrange
61
+ vi.mocked(parseArgs).mockReturnValue({ help: 'true' });
62
+
63
+ // Act
64
+ await main();
65
+
66
+ // Assert
67
+ expect(consoleLogSpy).toHaveBeenCalledWith(HELP_TEXT);
68
+ expect(processExitSpy).toHaveBeenCalledWith(0);
69
+ });
70
+
71
+ it('should call cmdLogin when command is "login"', async () => {
72
+ // Arrange
73
+ vi.mocked(parseArgs).mockReturnValue({
74
+ command: 'login',
75
+ type: 'account',
76
+ country: 'US',
77
+ language: 'en-US',
78
+ });
79
+ vi.mocked(cmdLogin).mockResolvedValue(undefined);
80
+
81
+ // Act
82
+ await main();
83
+
84
+ // Assert
85
+ expect(cmdLogin).toHaveBeenCalled();
86
+ });
87
+
88
+ it('should call cmdLogin with default values when flags are omitted', async () => {
89
+ // Arrange
90
+ vi.mocked(parseArgs).mockReturnValue({ command: 'login' });
91
+ vi.mocked(cmdLogin).mockResolvedValue(undefined);
92
+
93
+ // Act
94
+ await main();
95
+
96
+ // Assert
97
+ expect(cmdLogin).toHaveBeenCalled();
98
+ });
99
+
100
+ it('should call cmdDevices when command is "devices" and session exists', async () => {
101
+ // Arrange
102
+ const mockSession = {
103
+ loginType: 'account' as const,
104
+ country: 'US',
105
+ language: 'en-US',
106
+ userData: {
107
+ accessToken: 'token',
108
+ refreshToken: 'refresh',
109
+ expiresAtEpochSeconds: 123,
110
+ country: 'US',
111
+ language: 'en-US',
112
+ },
113
+ };
114
+ vi.mocked(parseArgs).mockReturnValue({ command: 'devices' });
115
+ vi.mocked(loadSession).mockReturnValue(mockSession);
116
+ vi.mocked(cmdDevices).mockResolvedValue(undefined);
117
+
118
+ // Act
119
+ await main();
120
+
121
+ // Assert
122
+ expect(cmdDevices).toHaveBeenCalledWith(mockSession, expect.any(Object), undefined, undefined);
123
+ });
124
+
125
+ it('should call cmdDevices with dump-snapshot flag when --dump-snapshot is provided', async () => {
126
+ // Arrange
127
+ const mockSession = {
128
+ loginType: 'account' as const,
129
+ country: 'US',
130
+ language: 'en-US',
131
+ userData: {
132
+ accessToken: 'token',
133
+ refreshToken: 'refresh',
134
+ expiresAtEpochSeconds: 123,
135
+ country: 'US',
136
+ language: 'en-US',
137
+ },
138
+ };
139
+ vi.mocked(parseArgs).mockReturnValue({ command: 'devices', 'dump-snapshot': 'washer-123' });
140
+ vi.mocked(loadSession).mockReturnValue(mockSession);
141
+ vi.mocked(cmdDevices).mockResolvedValue(undefined);
142
+
143
+ // Act
144
+ await main();
145
+
146
+ // Assert
147
+ expect(cmdDevices).toHaveBeenCalledWith(mockSession, expect.any(Object), 'washer-123', undefined);
148
+ });
149
+
150
+ it('should error when command is "devices" but session does not exist', async () => {
151
+ // Arrange
152
+ vi.mocked(parseArgs).mockReturnValue({ command: 'devices' });
153
+ vi.mocked(loadSession).mockReturnValue(null);
154
+
155
+ // Act
156
+ await main();
157
+
158
+ // Assert
159
+ expect(consoleErrorSpy).toHaveBeenCalledWith('No session found. Run `--command login` first.');
160
+ expect(cmdDevices).not.toHaveBeenCalled();
161
+ });
162
+
163
+ it('should error and exit when command is unknown', async () => {
164
+ // Arrange
165
+ vi.mocked(parseArgs).mockReturnValue({ command: 'unknown' });
166
+
167
+ // Act
168
+ await main();
169
+
170
+ // Assert
171
+ expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Unknown command: unknown'));
172
+ expect(processExitSpy).toHaveBeenCalledWith(1);
173
+ });
174
+
175
+ it('should catch and handle Error thrown from cmdLogin', async () => {
176
+ // Arrange
177
+ vi.mocked(parseArgs).mockReturnValue({ command: 'login' });
178
+ vi.mocked(cmdLogin).mockRejectedValue(new Error('Login failed'));
179
+
180
+ // Act
181
+ await main();
182
+
183
+ // Assert
184
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error:', 'Login failed');
185
+ expect(processExitSpy).toHaveBeenCalledWith(1);
186
+ });
187
+
188
+ it('should handle non-Error thrown value', async () => {
189
+ // Arrange
190
+ vi.mocked(parseArgs).mockReturnValue({ command: 'login' });
191
+ vi.mocked(cmdLogin).mockRejectedValue('String error');
192
+
193
+ // Act
194
+ await main();
195
+
196
+ // Assert
197
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error:', 'String error');
198
+ expect(processExitSpy).toHaveBeenCalledWith(1);
199
+ });
200
+
201
+ it('should return without further processing after cmdLogin succeeds', async () => {
202
+ // Arrange
203
+ vi.mocked(parseArgs).mockReturnValue({ command: 'login' });
204
+ vi.mocked(cmdLogin).mockResolvedValue(undefined);
205
+
206
+ // Act
207
+ await main();
208
+
209
+ // Assert
210
+ expect(cmdLogin).toHaveBeenCalled();
211
+ expect(processExitSpy).not.toHaveBeenCalled();
212
+ expect(consoleErrorSpy).not.toHaveBeenCalled();
213
+ });
214
+ });
@@ -0,0 +1,198 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ // Mock fs before importing saveSession
4
+ vi.mock('node:fs');
5
+
6
+ import fs from 'node:fs';
7
+
8
+ import { loadSession, saveSession } from '../../cli/session.js';
9
+ import type { CliSession } from '../../cli/types.js';
10
+
11
+ describe('saveSession', () => {
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+
16
+ afterEach(() => {
17
+ vi.clearAllMocks();
18
+ });
19
+
20
+ it('should write session to .cli-session.json with correct structure', () => {
21
+ // Arrange
22
+ const mockSession: CliSession = {
23
+ loginType: 'account',
24
+ country: 'US',
25
+ language: 'en-US',
26
+ userData: {
27
+ accessToken: 'a',
28
+ refreshToken: 'b',
29
+ expiresAtEpochSeconds: 0,
30
+ country: 'US',
31
+ language: 'en-US',
32
+ },
33
+ };
34
+
35
+ // Act
36
+ saveSession(mockSession);
37
+
38
+ // Assert
39
+ expect(fs.writeFileSync).toHaveBeenCalledTimes(1);
40
+ const callArgs = vi.mocked(fs.writeFileSync).mock.calls[0];
41
+ expect(callArgs[0]).toBe('.cli-session.json');
42
+
43
+ // Verify the second argument is valid JSON that matches the session
44
+ const writtenJson = callArgs[1] as string;
45
+ const parsedSession = JSON.parse(writtenJson);
46
+ expect(parsedSession).toEqual(mockSession);
47
+ });
48
+
49
+ it('should write session with token login type', () => {
50
+ // Arrange
51
+ const mockSession: CliSession = {
52
+ loginType: 'token',
53
+ country: 'KR',
54
+ language: 'ko-KR',
55
+ userData: {
56
+ accessToken: 'token123',
57
+ refreshToken: 'refresh456',
58
+ expiresAtEpochSeconds: 1234567890,
59
+ country: 'KR',
60
+ language: 'ko-KR',
61
+ },
62
+ };
63
+
64
+ // Act
65
+ saveSession(mockSession);
66
+
67
+ // Assert
68
+ expect(fs.writeFileSync).toHaveBeenCalledTimes(1);
69
+ const callArgs = vi.mocked(fs.writeFileSync).mock.calls[0];
70
+ const writtenJson = callArgs[1] as string;
71
+ const parsedSession = JSON.parse(writtenJson);
72
+ expect(parsedSession.loginType).toBe('token');
73
+ expect(parsedSession.country).toBe('KR');
74
+ expect(parsedSession.language).toBe('ko-KR');
75
+ });
76
+
77
+ it('should format JSON with proper indentation', () => {
78
+ // Arrange
79
+ const mockSession: CliSession = {
80
+ loginType: 'account',
81
+ country: 'US',
82
+ language: 'en-US',
83
+ userData: {
84
+ accessToken: 'a',
85
+ refreshToken: 'b',
86
+ expiresAtEpochSeconds: 0,
87
+ country: 'US',
88
+ language: 'en-US',
89
+ },
90
+ };
91
+
92
+ // Act
93
+ saveSession(mockSession);
94
+
95
+ // Assert
96
+ const callArgs = vi.mocked(fs.writeFileSync).mock.calls[0];
97
+ const writtenJson = callArgs[1] as string;
98
+
99
+ // Check that the JSON is formatted with 2-space indentation
100
+ expect(writtenJson).toContain('\n');
101
+ expect(writtenJson).toContain(' ');
102
+ // Verify it's valid JSON
103
+ const parsed = JSON.parse(writtenJson);
104
+ expect(parsed).toBeDefined();
105
+ });
106
+
107
+ it('should write all session fields correctly', () => {
108
+ // Arrange
109
+ const mockSession: CliSession = {
110
+ loginType: 'account',
111
+ country: 'GB',
112
+ language: 'en-GB',
113
+ userData: {
114
+ accessToken: 'access123',
115
+ refreshToken: 'refresh789',
116
+ expiresAtEpochSeconds: 1609459200,
117
+ country: 'GB',
118
+ language: 'en-GB',
119
+ },
120
+ };
121
+
122
+ // Act
123
+ saveSession(mockSession);
124
+
125
+ // Assert
126
+ const callArgs = vi.mocked(fs.writeFileSync).mock.calls[0];
127
+ const writtenJson = callArgs[1] as string;
128
+ const parsedSession = JSON.parse(writtenJson);
129
+
130
+ expect(parsedSession.loginType).toBe('account');
131
+ expect(parsedSession.country).toBe('GB');
132
+ expect(parsedSession.language).toBe('en-GB');
133
+ expect(parsedSession.userData.accessToken).toBe('access123');
134
+ expect(parsedSession.userData.refreshToken).toBe('refresh789');
135
+ expect(parsedSession.userData.expiresAtEpochSeconds).toBe(1609459200);
136
+ expect(parsedSession.userData.country).toBe('GB');
137
+ expect(parsedSession.userData.language).toBe('en-GB');
138
+ });
139
+ });
140
+
141
+ describe('loadSession', () => {
142
+ beforeEach(() => {
143
+ vi.clearAllMocks();
144
+ });
145
+
146
+ afterEach(() => {
147
+ vi.clearAllMocks();
148
+ });
149
+
150
+ it('should return parsed CliSession when file exists and contains valid JSON', () => {
151
+ // Arrange
152
+ const mockSession: CliSession = {
153
+ loginType: 'account',
154
+ country: 'US',
155
+ language: 'en-US',
156
+ userData: {
157
+ accessToken: 'token123',
158
+ refreshToken: 'refresh456',
159
+ expiresAtEpochSeconds: 1609459200,
160
+ country: 'US',
161
+ language: 'en-US',
162
+ },
163
+ };
164
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSession));
165
+
166
+ // Act
167
+ const result = loadSession();
168
+
169
+ // Assert
170
+ expect(result).toEqual(mockSession);
171
+ expect(fs.readFileSync).toHaveBeenCalledWith('.cli-session.json', 'utf-8');
172
+ });
173
+
174
+ it('should return null when file does not exist', () => {
175
+ // Arrange
176
+ vi.mocked(fs.readFileSync).mockImplementation(() => {
177
+ const err = new Error('ENOENT: no such file or directory');
178
+ throw err;
179
+ });
180
+
181
+ // Act
182
+ const result = loadSession();
183
+
184
+ // Assert
185
+ expect(result).toBeNull();
186
+ });
187
+
188
+ it('should return null when file contains invalid JSON', () => {
189
+ // Arrange
190
+ vi.mocked(fs.readFileSync).mockReturnValue('{ invalid json }');
191
+
192
+ // Act
193
+ const result = loadSession();
194
+
195
+ // Assert
196
+ expect(result).toBeNull();
197
+ });
198
+ });