@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.
- package/README.md +6 -0
- package/dist/FileLogger.js +207 -0
- package/dist/FileUtils.js +387 -0
- package/dist/Logger.js +88 -0
- package/dist/MultiScopeWatcher.js +615 -0
- package/dist/PluginManager.js +125 -0
- package/dist/_defaultOptionsOLD.js +44 -0
- package/dist/allScopesCommands.js +418 -0
- package/dist/appUtils.js +929 -0
- package/dist/benchmark.js +104 -0
- package/dist/bootstrap.js +62 -0
- package/dist/claudeCommand.js +60 -0
- package/dist/clickupCommands.js +472 -0
- package/dist/clickupPushHelper.js +33 -0
- package/dist/commander.js +466 -0
- package/dist/commands.js +378 -0
- package/dist/config.js +697 -0
- package/dist/constants.js +15 -0
- package/dist/createRecordCommand.js +314 -0
- package/dist/dashboardCommand.js +38 -0
- package/dist/defaultOptions.js +14 -0
- package/dist/deleteRecordCommand.js +282 -0
- package/dist/flowDesigner/values.js +55 -0
- package/dist/genericUtils.js +189 -0
- package/dist/gitUtils.js +99 -0
- package/dist/index.js +35 -0
- package/dist/initSystem/corePlugin.js +344 -0
- package/dist/initSystem/discovery.js +107 -0
- package/dist/initSystem/orchestrator.js +408 -0
- package/dist/logMessages.js +82 -0
- package/dist/loginCommand.js +20 -0
- package/dist/migrateCommand.js +123 -0
- package/dist/projectFiles.js +32 -0
- package/dist/recentEdits.js +66 -0
- package/dist/schemaCommand.js +85 -0
- package/dist/snClient.js +519 -0
- package/dist/tests/benchmarkRefresh.test.js +265 -0
- package/dist/tests/clickupCommands.test.js +384 -0
- package/dist/tests/clickupPushHelper.test.js +124 -0
- package/dist/tests/configFallback.test.js +68 -0
- package/dist/tests/cred.test.js +59 -0
- package/dist/tests/discovery.test.js +67 -0
- package/dist/tests/ensureUpdateSetWarnings.test.js +218 -0
- package/dist/tests/errorLogLevels.test.js +273 -0
- package/dist/tests/example.test.js +6 -0
- package/dist/tests/fileContextSkipReason.test.js +116 -0
- package/dist/tests/globalDebounce.test.js +308 -0
- package/dist/tests/migrateCommand.test.js +113 -0
- package/dist/tests/multi-scope-watcher.test.js +558 -0
- package/dist/tests/orchestrator.test.js +23 -0
- package/dist/tests/pushFiles.test.js +248 -0
- package/dist/tests/rateLimitCoordination.test.js +271 -0
- package/dist/tests/resolveConfigReadOnly.test.js +67 -0
- package/dist/tests/retryOnHttpErr.test.js +154 -0
- package/dist/tests/scopeCaching.test.js +115 -0
- package/dist/tests/serializeUpdateSetConfig.test.js +325 -0
- package/dist/tests/snClient.fallback.test.js +120 -0
- package/dist/tests/syncManifestRefresh.test.js +287 -0
- package/dist/tests/syncManifestWhitelist.test.js +192 -0
- package/dist/tests/taskClear.test.js +170 -0
- package/dist/tests/taskStaleness.test.js +220 -0
- package/dist/tests/unwrapSNResponseError.test.js +145 -0
- package/dist/tests/v2Values.test.js +79 -0
- package/dist/tests/validateTaskId.test.js +304 -0
- package/dist/tests/verifyUpdateSetSwitch.test.js +277 -0
- package/dist/tests/writeEnvVar.test.js +119 -0
- package/dist/updateSetCommands.js +702 -0
- package/dist/wizard.js +40 -0
- package/package.json +69 -0
- package/skills/dove-configure-pipeline.md +158 -0
- package/skills/dove-create-plugin.md +174 -0
- package/skills/dove-create-record.md +135 -0
- package/skills/dove-debug-build.md +117 -0
- package/skills/dove-delete-record.md +104 -0
- package/skills/dove-manage-tables.md +174 -0
- package/skills/dove-manage-update-sets.md +88 -0
- package/skills/dove-setup-project.md +154 -0
- 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
|
+
});
|