@tenonhq/dovetail-core 0.0.85

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 (78) hide show
  1. package/README.md +6 -0
  2. package/dist/FileLogger.js +207 -0
  3. package/dist/FileUtils.js +387 -0
  4. package/dist/Logger.js +88 -0
  5. package/dist/MultiScopeWatcher.js +615 -0
  6. package/dist/PluginManager.js +125 -0
  7. package/dist/_defaultOptionsOLD.js +44 -0
  8. package/dist/allScopesCommands.js +418 -0
  9. package/dist/appUtils.js +929 -0
  10. package/dist/benchmark.js +104 -0
  11. package/dist/bootstrap.js +62 -0
  12. package/dist/claudeCommand.js +60 -0
  13. package/dist/clickupCommands.js +472 -0
  14. package/dist/clickupPushHelper.js +33 -0
  15. package/dist/commander.js +466 -0
  16. package/dist/commands.js +378 -0
  17. package/dist/config.js +697 -0
  18. package/dist/constants.js +15 -0
  19. package/dist/createRecordCommand.js +314 -0
  20. package/dist/dashboardCommand.js +38 -0
  21. package/dist/defaultOptions.js +14 -0
  22. package/dist/deleteRecordCommand.js +282 -0
  23. package/dist/flowDesigner/values.js +55 -0
  24. package/dist/genericUtils.js +189 -0
  25. package/dist/gitUtils.js +99 -0
  26. package/dist/index.js +35 -0
  27. package/dist/initSystem/corePlugin.js +344 -0
  28. package/dist/initSystem/discovery.js +107 -0
  29. package/dist/initSystem/orchestrator.js +408 -0
  30. package/dist/logMessages.js +82 -0
  31. package/dist/loginCommand.js +20 -0
  32. package/dist/migrateCommand.js +123 -0
  33. package/dist/projectFiles.js +32 -0
  34. package/dist/recentEdits.js +66 -0
  35. package/dist/schemaCommand.js +85 -0
  36. package/dist/snClient.js +519 -0
  37. package/dist/tests/benchmarkRefresh.test.js +265 -0
  38. package/dist/tests/clickupCommands.test.js +384 -0
  39. package/dist/tests/clickupPushHelper.test.js +124 -0
  40. package/dist/tests/configFallback.test.js +68 -0
  41. package/dist/tests/cred.test.js +59 -0
  42. package/dist/tests/discovery.test.js +67 -0
  43. package/dist/tests/ensureUpdateSetWarnings.test.js +218 -0
  44. package/dist/tests/errorLogLevels.test.js +273 -0
  45. package/dist/tests/example.test.js +6 -0
  46. package/dist/tests/fileContextSkipReason.test.js +116 -0
  47. package/dist/tests/globalDebounce.test.js +308 -0
  48. package/dist/tests/migrateCommand.test.js +113 -0
  49. package/dist/tests/multi-scope-watcher.test.js +558 -0
  50. package/dist/tests/orchestrator.test.js +23 -0
  51. package/dist/tests/pushFiles.test.js +248 -0
  52. package/dist/tests/rateLimitCoordination.test.js +271 -0
  53. package/dist/tests/resolveConfigReadOnly.test.js +67 -0
  54. package/dist/tests/retryOnHttpErr.test.js +154 -0
  55. package/dist/tests/scopeCaching.test.js +115 -0
  56. package/dist/tests/serializeUpdateSetConfig.test.js +325 -0
  57. package/dist/tests/snClient.fallback.test.js +120 -0
  58. package/dist/tests/syncManifestRefresh.test.js +287 -0
  59. package/dist/tests/syncManifestWhitelist.test.js +192 -0
  60. package/dist/tests/taskClear.test.js +170 -0
  61. package/dist/tests/taskStaleness.test.js +220 -0
  62. package/dist/tests/unwrapSNResponseError.test.js +145 -0
  63. package/dist/tests/v2Values.test.js +79 -0
  64. package/dist/tests/validateTaskId.test.js +304 -0
  65. package/dist/tests/verifyUpdateSetSwitch.test.js +277 -0
  66. package/dist/tests/writeEnvVar.test.js +119 -0
  67. package/dist/updateSetCommands.js +702 -0
  68. package/dist/wizard.js +40 -0
  69. package/package.json +69 -0
  70. package/skills/dove-configure-pipeline.md +158 -0
  71. package/skills/dove-create-plugin.md +174 -0
  72. package/skills/dove-create-record.md +135 -0
  73. package/skills/dove-debug-build.md +117 -0
  74. package/skills/dove-delete-record.md +104 -0
  75. package/skills/dove-manage-tables.md +174 -0
  76. package/skills/dove-manage-update-sets.md +88 -0
  77. package/skills/dove-setup-project.md +154 -0
  78. package/skills/dove-troubleshoot-sync.md +113 -0
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for US-010: Validate task ID before ServiceNow queries
4
+ *
5
+ * Validates:
6
+ * - taskId is validated as non-empty string before constructing nameLIKECU-{taskId} query
7
+ * - Empty/undefined taskId logs an error and skips update set lookup
8
+ * - readActiveTask() validates required fields (taskId, updateSetName) and returns null if missing
9
+ * - Missing required fields produce a clear error, not a crash
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ // --- Mock setup ---
46
+ const mockSNClient = {
47
+ getScopeId: jest.fn(),
48
+ getUserSysId: jest.fn(),
49
+ getCurrentAppUserPrefSysId: jest.fn(),
50
+ updateCurrentAppUserPref: jest.fn(),
51
+ createCurrentAppUserPref: jest.fn(),
52
+ changeScope: jest.fn().mockResolvedValue(undefined),
53
+ createUpdateSet: jest.fn(),
54
+ changeUpdateSet: jest.fn(),
55
+ getCurrentUpdateSet: jest.fn(),
56
+ client: {
57
+ get: jest.fn(),
58
+ },
59
+ };
60
+ jest.mock("../snClient", () => ({
61
+ defaultClient: jest.fn(() => mockSNClient),
62
+ unwrapSNResponse: jest.fn((val) => val),
63
+ }));
64
+ jest.mock("../FileUtils", () => ({
65
+ getFileContextFromPath: jest.fn(),
66
+ getFileContextWithSkipReason: jest.fn(),
67
+ }));
68
+ jest.mock("../appUtils", () => ({
69
+ groupAppFiles: jest.fn(),
70
+ pushFiles: jest.fn(),
71
+ }));
72
+ jest.mock("../logMessages", () => ({
73
+ logFilePush: jest.fn(),
74
+ }));
75
+ jest.mock("../recentEdits", () => ({
76
+ writeRecentEdit: jest.fn(),
77
+ }));
78
+ jest.mock("../Logger", () => ({
79
+ logger: {
80
+ info: jest.fn(),
81
+ error: jest.fn(),
82
+ warn: jest.fn(),
83
+ debug: jest.fn(),
84
+ success: jest.fn(),
85
+ getLogLevel: jest.fn().mockReturnValue("info"),
86
+ },
87
+ }));
88
+ jest.mock("../config", () => ({
89
+ loadConfigs: jest.fn().mockResolvedValue(undefined),
90
+ getConfig: jest.fn(),
91
+ getRootDir: jest.fn().mockReturnValue("/project"),
92
+ updateManifest: jest.fn(),
93
+ getManifest: jest.fn(),
94
+ getSourcePath: jest.fn().mockReturnValue("/project/src"),
95
+ getScopeManifestPath: jest.fn((scope) => `/project/dove.manifest.${scope}.json`),
96
+ getManifestPath: jest.fn().mockReturnValue("/project/dove.manifest.json"),
97
+ }));
98
+ // Filesystem mock store
99
+ var mockFsStore = {};
100
+ jest.mock("fs", () => ({
101
+ existsSync: jest.fn((p) => {
102
+ return p in mockFsStore;
103
+ }),
104
+ readFileSync: jest.fn((p) => {
105
+ if (p in mockFsStore)
106
+ return mockFsStore[p];
107
+ throw new Error("ENOENT: " + p);
108
+ }),
109
+ writeFileSync: jest.fn((p, data) => {
110
+ mockFsStore[p] = data;
111
+ }),
112
+ statSync: jest.fn(() => ({ mtimeMs: Date.now() })),
113
+ promises: {
114
+ readFile: jest.fn(),
115
+ writeFile: jest.fn(),
116
+ readdir: jest.fn(),
117
+ mkdir: jest.fn(),
118
+ access: jest.fn(),
119
+ stat: jest.fn(),
120
+ },
121
+ }));
122
+ jest.mock("chokidar", () => ({
123
+ watch: jest.fn(() => ({
124
+ on: jest.fn().mockReturnThis(),
125
+ close: jest.fn(),
126
+ })),
127
+ }));
128
+ jest.mock("lodash", () => {
129
+ var actual = jest.requireActual("lodash");
130
+ return {
131
+ ...actual,
132
+ debounce: jest.fn((fn) => {
133
+ var wrapper = jest.fn();
134
+ wrapper.cancel = jest.fn();
135
+ wrapper.flush = jest.fn(() => fn());
136
+ return wrapper;
137
+ }),
138
+ };
139
+ });
140
+ jest.mock("axios", () => ({
141
+ default: {
142
+ create: jest.fn(() => ({
143
+ get: jest.fn().mockResolvedValue({ data: { result: null } }),
144
+ })),
145
+ },
146
+ }));
147
+ // --- Imports ---
148
+ const Logger_1 = require("../Logger");
149
+ const MultiScopeWatcher_1 = require("../MultiScopeWatcher");
150
+ const path = __importStar(require("path"));
151
+ // --- Tests ---
152
+ describe("US-010: Validate task ID before ServiceNow queries", () => {
153
+ beforeEach(() => {
154
+ jest.clearAllMocks();
155
+ mockFsStore = {};
156
+ // Default: scope switching succeeds
157
+ mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
158
+ mockSNClient.getUserSysId.mockResolvedValue([{ sys_id: "user_sys_id" }]);
159
+ mockSNClient.getCurrentAppUserPrefSysId.mockResolvedValue([{ sys_id: "pref_sys_id" }]);
160
+ mockSNClient.updateCurrentAppUserPref.mockResolvedValue({});
161
+ mockSNClient.createCurrentAppUserPref.mockResolvedValue({});
162
+ });
163
+ afterEach(() => {
164
+ (0, MultiScopeWatcher_1.stopMultiScopeWatching)();
165
+ });
166
+ describe("readActiveTask() validation", () => {
167
+ it("returns null and logs error when taskId is empty string", () => {
168
+ var taskPath = path.resolve(process.cwd(), ".dove-active-task.json");
169
+ mockFsStore[taskPath] = JSON.stringify({
170
+ taskId: "",
171
+ taskName: "Test Task",
172
+ updateSetName: "CU- Test Task",
173
+ description: "Test",
174
+ taskUrl: "",
175
+ scopes: {},
176
+ });
177
+ var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
178
+ expect(result).toBeNull();
179
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("missing a valid taskId"));
180
+ });
181
+ it("returns null and logs error when taskId is undefined", () => {
182
+ var taskPath = path.resolve(process.cwd(), ".dove-active-task.json");
183
+ mockFsStore[taskPath] = JSON.stringify({
184
+ taskName: "Test Task",
185
+ updateSetName: "CU-abc Test Task",
186
+ description: "Test",
187
+ taskUrl: "",
188
+ scopes: {},
189
+ });
190
+ var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
191
+ expect(result).toBeNull();
192
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("missing a valid taskId"));
193
+ });
194
+ it("returns null and logs error when updateSetName is empty string", () => {
195
+ var taskPath = path.resolve(process.cwd(), ".dove-active-task.json");
196
+ mockFsStore[taskPath] = JSON.stringify({
197
+ taskId: "abc123",
198
+ taskName: "Test Task",
199
+ updateSetName: "",
200
+ description: "Test",
201
+ taskUrl: "",
202
+ scopes: {},
203
+ });
204
+ var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
205
+ expect(result).toBeNull();
206
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("missing a valid updateSetName"));
207
+ });
208
+ it("returns null and logs error when updateSetName is missing", () => {
209
+ var taskPath = path.resolve(process.cwd(), ".dove-active-task.json");
210
+ mockFsStore[taskPath] = JSON.stringify({
211
+ taskId: "abc123",
212
+ taskName: "Test Task",
213
+ description: "Test",
214
+ taskUrl: "",
215
+ scopes: {},
216
+ });
217
+ var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
218
+ expect(result).toBeNull();
219
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("missing a valid updateSetName"));
220
+ });
221
+ it("returns valid task when both taskId and updateSetName are present", () => {
222
+ var taskPath = path.resolve(process.cwd(), ".dove-active-task.json");
223
+ mockFsStore[taskPath] = JSON.stringify({
224
+ taskId: "abc123",
225
+ taskName: "Test Task",
226
+ updateSetName: "CU-abc123 Test Task",
227
+ description: "Test",
228
+ taskUrl: "",
229
+ scopes: {},
230
+ });
231
+ var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
232
+ expect(result).not.toBeNull();
233
+ expect(result.taskId).toBe("abc123");
234
+ expect(result.updateSetName).toBe("CU-abc123 Test Task");
235
+ expect(Logger_1.logger.error).not.toHaveBeenCalled();
236
+ });
237
+ });
238
+ describe("ensureUpdateSetForScope() taskId validation", () => {
239
+ it("skips update set lookup when taskId is whitespace-only", async () => {
240
+ var taskPath = path.resolve(process.cwd(), ".dove-active-task.json");
241
+ mockFsStore[taskPath] = JSON.stringify({
242
+ taskId: " ",
243
+ taskName: "Test Task",
244
+ updateSetName: "CU- Test Task",
245
+ description: "Test",
246
+ taskUrl: "",
247
+ scopes: {},
248
+ });
249
+ await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_test_scope");
250
+ // readActiveTask should have returned null due to whitespace-only taskId
251
+ // which triggers the no-active-task warning instead
252
+ expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("No update set configured for scope"));
253
+ // Should NOT have made any ServiceNow API calls
254
+ expect(mockSNClient.changeScope).not.toHaveBeenCalled();
255
+ expect(mockSNClient.client.get).not.toHaveBeenCalled();
256
+ expect(mockSNClient.createUpdateSet).not.toHaveBeenCalled();
257
+ });
258
+ it("does not crash with missing required fields in task file", async () => {
259
+ var taskPath = path.resolve(process.cwd(), ".dove-active-task.json");
260
+ mockFsStore[taskPath] = JSON.stringify({
261
+ taskName: "Partial Task",
262
+ });
263
+ // Should not throw
264
+ await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_test_scope");
265
+ // readActiveTask returns null → falls through to no-active-task warning
266
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("missing a valid taskId"));
267
+ expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("No update set configured for scope"));
268
+ // No ServiceNow API calls
269
+ expect(mockSNClient.changeScope).not.toHaveBeenCalled();
270
+ });
271
+ it("proceeds with valid taskId and makes ServiceNow query", async () => {
272
+ var taskPath = path.resolve(process.cwd(), ".dove-active-task.json");
273
+ mockFsStore[taskPath] = JSON.stringify({
274
+ taskId: "abc123",
275
+ taskName: "Test Task",
276
+ updateSetName: "CU-abc123 Test Task",
277
+ description: "Test",
278
+ taskUrl: "",
279
+ scopes: {},
280
+ });
281
+ // Mock: scope found, existing update set found
282
+ mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
283
+ mockSNClient.client.get.mockResolvedValue({
284
+ data: { result: [{ sys_id: "us_123", name: "CU-abc123 Test Task", state: "in progress" }] },
285
+ });
286
+ mockSNClient.changeUpdateSet.mockResolvedValue(undefined);
287
+ mockSNClient.getCurrentUpdateSet.mockResolvedValue({
288
+ data: { result: { sysId: "us_123", name: "CU-abc123 Test Task" } },
289
+ });
290
+ await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_valid_scope");
291
+ // Should have proceeded to search ServiceNow
292
+ expect(mockSNClient.client.get).toHaveBeenCalled();
293
+ // The query should contain the taskId
294
+ var getCall = mockSNClient.client.get.mock.calls[0];
295
+ expect(getCall[1].params.sysparm_query).toContain("CU-abc123");
296
+ // No "empty taskId" error
297
+ var errorCalls = Logger_1.logger.error.mock.calls.map(function (c) { return c[0]; });
298
+ var hasEmptyTaskIdError = errorCalls.some(function (msg) {
299
+ return msg.indexOf("empty taskId") !== -1;
300
+ });
301
+ expect(hasEmptyTaskIdError).toBe(false);
302
+ });
303
+ });
304
+ });
@@ -0,0 +1,277 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for US-007: Verify update set is active after creation
4
+ *
5
+ * Validates:
6
+ * - After switchToUpdateSet(), getCurrentUpdateSet() is called to verify the sys_id matches
7
+ * - Verification failure triggers a retry
8
+ * - Retry failure produces explicit error with actual update set name
9
+ * - MultiScopeWatcher's ensureUpdateSetForScope() includes the same verification
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ // --- Mock setup (MultiScopeWatcher tests) ---
13
+ var mockSNClient = {
14
+ getScopeId: jest.fn(),
15
+ getUserSysId: jest.fn(),
16
+ getCurrentAppUserPrefSysId: jest.fn(),
17
+ updateCurrentAppUserPref: jest.fn(),
18
+ createCurrentAppUserPref: jest.fn(),
19
+ changeScope: jest.fn().mockResolvedValue(undefined),
20
+ createUpdateSet: jest.fn(),
21
+ changeUpdateSet: jest.fn(),
22
+ getCurrentUpdateSet: jest.fn(),
23
+ client: {
24
+ get: jest.fn(),
25
+ },
26
+ };
27
+ jest.mock("../snClient", () => ({
28
+ defaultClient: jest.fn(() => mockSNClient),
29
+ unwrapSNResponse: jest.fn((val) => val),
30
+ }));
31
+ jest.mock("../FileUtils", () => ({
32
+ getFileContextFromPath: jest.fn(),
33
+ getFileContextWithSkipReason: jest.fn(),
34
+ }));
35
+ jest.mock("../appUtils", () => ({
36
+ groupAppFiles: jest.fn(),
37
+ pushFiles: jest.fn(),
38
+ }));
39
+ jest.mock("../logMessages", () => ({
40
+ logFilePush: jest.fn(),
41
+ }));
42
+ jest.mock("../recentEdits", () => ({
43
+ writeRecentEdit: jest.fn(),
44
+ }));
45
+ jest.mock("../Logger", () => ({
46
+ logger: {
47
+ info: jest.fn(),
48
+ error: jest.fn(),
49
+ warn: jest.fn(),
50
+ debug: jest.fn(),
51
+ success: jest.fn(),
52
+ getLogLevel: jest.fn().mockReturnValue("info"),
53
+ },
54
+ }));
55
+ jest.mock("../config", () => ({
56
+ loadConfigs: jest.fn().mockResolvedValue(undefined),
57
+ getConfig: jest.fn(),
58
+ getRootDir: jest.fn().mockReturnValue("/project"),
59
+ updateManifest: jest.fn(),
60
+ getManifest: jest.fn(),
61
+ getSourcePath: jest.fn().mockReturnValue("/project/src"),
62
+ getScopeManifestPath: jest.fn((scope) => `/project/dove.manifest.${scope}.json`),
63
+ getManifestPath: jest.fn().mockReturnValue("/project/dove.manifest.json"),
64
+ }));
65
+ var mockFsStore = {};
66
+ jest.mock("fs", () => ({
67
+ existsSync: jest.fn((p) => {
68
+ return p in mockFsStore;
69
+ }),
70
+ readFileSync: jest.fn((p) => {
71
+ if (p in mockFsStore)
72
+ return mockFsStore[p];
73
+ throw new Error("ENOENT: " + p);
74
+ }),
75
+ writeFileSync: jest.fn((p, data) => {
76
+ mockFsStore[p] = data;
77
+ }),
78
+ statSync: jest.fn(() => ({ mtimeMs: Date.now() })),
79
+ promises: {
80
+ readFile: jest.fn(),
81
+ writeFile: jest.fn(),
82
+ readdir: jest.fn(),
83
+ mkdir: jest.fn(),
84
+ access: jest.fn(),
85
+ stat: jest.fn(),
86
+ },
87
+ }));
88
+ jest.mock("chokidar", () => ({
89
+ watch: jest.fn(() => ({
90
+ on: jest.fn().mockReturnThis(),
91
+ close: jest.fn(),
92
+ })),
93
+ }));
94
+ jest.mock("lodash", () => {
95
+ var actual = jest.requireActual("lodash");
96
+ return {
97
+ ...actual,
98
+ debounce: jest.fn((fn) => {
99
+ var wrapper = jest.fn();
100
+ wrapper.cancel = jest.fn();
101
+ wrapper.flush = jest.fn(() => fn());
102
+ return wrapper;
103
+ }),
104
+ };
105
+ });
106
+ jest.mock("axios", () => ({
107
+ default: {
108
+ create: jest.fn(() => ({
109
+ get: jest.fn().mockResolvedValue({ data: { result: null } }),
110
+ })),
111
+ },
112
+ }));
113
+ // --- Imports ---
114
+ const Logger_1 = require("../Logger");
115
+ const MultiScopeWatcher_1 = require("../MultiScopeWatcher");
116
+ // --- Tests ---
117
+ describe("US-007: Verify update set is active after creation", () => {
118
+ beforeEach(() => {
119
+ jest.clearAllMocks();
120
+ mockFsStore = {};
121
+ // Default: scope switching succeeds
122
+ mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
123
+ mockSNClient.getUserSysId.mockResolvedValue([{ sys_id: "user_sys_id" }]);
124
+ mockSNClient.getCurrentAppUserPrefSysId.mockResolvedValue([{ sys_id: "pref_sys_id" }]);
125
+ mockSNClient.updateCurrentAppUserPref.mockResolvedValue({});
126
+ mockSNClient.createCurrentAppUserPref.mockResolvedValue({});
127
+ });
128
+ afterEach(() => {
129
+ (0, MultiScopeWatcher_1.stopMultiScopeWatching)();
130
+ });
131
+ it("verifies update set switch by calling getCurrentUpdateSet after changeUpdateSet", async () => {
132
+ // Setup: active task + no existing config
133
+ var taskPath = require("path").resolve(process.cwd(), ".dove-active-task.json");
134
+ mockFsStore[taskPath] = JSON.stringify({
135
+ taskId: "abc123",
136
+ taskName: "Test Task",
137
+ taskDescription: "Test",
138
+ updateSetName: "CU-abc123 Test Task",
139
+ description: "Test update set",
140
+ taskUrl: "https://example.com",
141
+ scopes: {},
142
+ });
143
+ // Scope exists
144
+ mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
145
+ // Search returns existing update set
146
+ mockSNClient.client.get.mockResolvedValue({
147
+ data: { result: [{ sys_id: "us_123", name: "CU-abc123 Test Task" }] }
148
+ });
149
+ // changeUpdateSet succeeds
150
+ mockSNClient.changeUpdateSet.mockResolvedValue(undefined);
151
+ // getCurrentUpdateSet confirms the correct sys_id
152
+ mockSNClient.getCurrentUpdateSet.mockResolvedValue({
153
+ data: { result: { sysId: "us_123", name: "CU-abc123 Test Task" } }
154
+ });
155
+ await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_test_scope");
156
+ // changeUpdateSet was called
157
+ expect(mockSNClient.changeUpdateSet).toHaveBeenCalledWith({ sysId: "us_123" });
158
+ // getCurrentUpdateSet was called for verification
159
+ expect(mockSNClient.getCurrentUpdateSet).toHaveBeenCalledWith("x_test_scope");
160
+ // Verify debug confirmation logged
161
+ expect(Logger_1.logger.debug).toHaveBeenCalledWith(expect.stringContaining("Update set switch verified"));
162
+ });
163
+ it("retries switch when verification shows wrong update set, succeeds on retry", async () => {
164
+ var taskPath = require("path").resolve(process.cwd(), ".dove-active-task.json");
165
+ mockFsStore[taskPath] = JSON.stringify({
166
+ taskId: "abc123",
167
+ taskName: "Test Task",
168
+ taskDescription: "Test",
169
+ updateSetName: "CU-abc123 Test Task",
170
+ description: "Test update set",
171
+ taskUrl: "https://example.com",
172
+ scopes: {},
173
+ });
174
+ mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
175
+ mockSNClient.client.get.mockResolvedValue({
176
+ data: { result: [{ sys_id: "us_123", name: "CU-abc123 Test Task" }] }
177
+ });
178
+ mockSNClient.changeUpdateSet.mockResolvedValue(undefined);
179
+ // First verification fails (wrong sys_id), second succeeds
180
+ mockSNClient.getCurrentUpdateSet
181
+ .mockResolvedValueOnce({
182
+ data: { result: { sysId: "wrong_us_id", name: "Default" } }
183
+ })
184
+ .mockResolvedValueOnce({
185
+ data: { result: { sysId: "us_123", name: "CU-abc123 Test Task" } }
186
+ });
187
+ await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_test_scope");
188
+ // changeUpdateSet called twice (initial + retry)
189
+ expect(mockSNClient.changeUpdateSet).toHaveBeenCalledTimes(2);
190
+ // Warning logged about retry
191
+ expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("verification failed, retrying"));
192
+ // Should succeed — no error about "could not be activated"
193
+ var errorCalls = Logger_1.logger.error.mock.calls.map(function (c) { return c[0]; });
194
+ var hasActivationError = errorCalls.some(function (msg) {
195
+ return typeof msg === "string" && msg.indexOf("could not be activated") !== -1;
196
+ });
197
+ expect(hasActivationError).toBe(false);
198
+ });
199
+ it("logs explicit error when both switch attempts fail verification", async () => {
200
+ var taskPath = require("path").resolve(process.cwd(), ".dove-active-task.json");
201
+ mockFsStore[taskPath] = JSON.stringify({
202
+ taskId: "abc123",
203
+ taskName: "Test Task",
204
+ taskDescription: "Test",
205
+ updateSetName: "CU-abc123 Test Task",
206
+ description: "Test update set",
207
+ taskUrl: "https://example.com",
208
+ scopes: {},
209
+ });
210
+ mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
211
+ mockSNClient.client.get.mockResolvedValue({
212
+ data: { result: [{ sys_id: "us_123", name: "CU-abc123 Test Task" }] }
213
+ });
214
+ mockSNClient.changeUpdateSet.mockResolvedValue(undefined);
215
+ // Both verifications fail — always returns wrong sys_id
216
+ mockSNClient.getCurrentUpdateSet.mockResolvedValue({
217
+ data: { result: { sysId: "wrong_us_id", name: "Default" } }
218
+ });
219
+ await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_test_scope");
220
+ // changeUpdateSet called twice (initial + retry)
221
+ expect(mockSNClient.changeUpdateSet).toHaveBeenCalledTimes(2);
222
+ // Error message should include both the update set name and the actual active one
223
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("could not be activated"));
224
+ expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("Default"));
225
+ });
226
+ it("does not retry when verification passes on first attempt", async () => {
227
+ var taskPath = require("path").resolve(process.cwd(), ".dove-active-task.json");
228
+ mockFsStore[taskPath] = JSON.stringify({
229
+ taskId: "abc123",
230
+ taskName: "Test Task",
231
+ taskDescription: "Test",
232
+ updateSetName: "CU-abc123 Test Task",
233
+ description: "Test update set",
234
+ taskUrl: "https://example.com",
235
+ scopes: {},
236
+ });
237
+ mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
238
+ mockSNClient.client.get.mockResolvedValue({
239
+ data: { result: [{ sys_id: "us_123", name: "CU-abc123 Test Task" }] }
240
+ });
241
+ mockSNClient.changeUpdateSet.mockResolvedValue(undefined);
242
+ // Verification passes on first attempt
243
+ mockSNClient.getCurrentUpdateSet.mockResolvedValue({
244
+ data: { result: { sysId: "us_123", name: "CU-abc123 Test Task" } }
245
+ });
246
+ await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_test_scope");
247
+ // changeUpdateSet called only once
248
+ expect(mockSNClient.changeUpdateSet).toHaveBeenCalledTimes(1);
249
+ // getCurrentUpdateSet called once for verification
250
+ expect(mockSNClient.getCurrentUpdateSet).toHaveBeenCalledTimes(1);
251
+ // No retry warning
252
+ expect(Logger_1.logger.warn).not.toHaveBeenCalledWith(expect.stringContaining("verification failed, retrying"));
253
+ });
254
+ it("handles getCurrentUpdateSet throwing during verification gracefully", async () => {
255
+ var taskPath = require("path").resolve(process.cwd(), ".dove-active-task.json");
256
+ mockFsStore[taskPath] = JSON.stringify({
257
+ taskId: "abc123",
258
+ taskName: "Test Task",
259
+ taskDescription: "Test",
260
+ updateSetName: "CU-abc123 Test Task",
261
+ description: "Test update set",
262
+ taskUrl: "https://example.com",
263
+ scopes: {},
264
+ });
265
+ mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
266
+ mockSNClient.client.get.mockResolvedValue({
267
+ data: { result: [{ sys_id: "us_123", name: "CU-abc123 Test Task" }] }
268
+ });
269
+ mockSNClient.changeUpdateSet.mockResolvedValue(undefined);
270
+ // getCurrentUpdateSet throws (network error etc.)
271
+ mockSNClient.getCurrentUpdateSet.mockRejectedValue(new Error("Network error"));
272
+ // Should not crash — the outer try/catch in ensureUpdateSetForScope handles it
273
+ await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_test_scope");
274
+ // Should have attempted verification
275
+ expect(mockSNClient.getCurrentUpdateSet).toHaveBeenCalled();
276
+ });
277
+ });