@tenonhq/sincronia-core 0.0.78 → 0.0.80
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 +1 -1
- package/dist/FileUtils.js +19 -13
- package/dist/MultiScopeWatcher.js +212 -84
- package/dist/allScopesCommands.js +4 -2
- package/dist/appUtils.js +23 -12
- package/dist/commander.js +17 -0
- package/dist/commands.js +38 -1
- package/dist/config.js +4 -4
- package/dist/snClient.js +81 -1
- package/dist/tests/ensureUpdateSetWarnings.test.js +218 -0
- package/dist/tests/errorLogLevels.test.js +273 -0
- package/dist/tests/fileContextSkipReason.test.js +116 -0
- package/dist/tests/globalDebounce.test.js +307 -0
- package/dist/tests/multi-scope-watcher.test.js +109 -7
- package/dist/tests/pushFiles.test.js +162 -0
- package/dist/tests/rateLimitCoordination.test.js +271 -0
- package/dist/tests/retryOnHttpErr.test.js +154 -0
- package/dist/tests/scopeCaching.test.js +124 -0
- package/dist/tests/serializeUpdateSetConfig.test.js +325 -0
- package/dist/tests/taskClear.test.js +170 -0
- package/dist/tests/taskStaleness.test.js +220 -0
- package/dist/tests/validateTaskId.test.js +304 -0
- package/dist/tests/verifyUpdateSetSwitch.test.js +277 -0
- package/dist/updateSetCommands.js +59 -2
- package/package.json +1 -1
- package/skills/sinc-configure-pipeline.md +19 -19
- package/skills/sinc-debug-build.md +7 -7
- package/skills/sinc-setup-project.md +2 -2
- package/skills/sinc-troubleshoot-sync.md +5 -5
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for US-012: Add staleness warning for active task
|
|
4
|
+
*
|
|
5
|
+
* Validates:
|
|
6
|
+
* - On startup, if .sinc-active-task.json is older than 7 days, a warning is logged
|
|
7
|
+
* - Fresh task files produce no warning
|
|
8
|
+
* - Warning includes task name and age in days
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
// --- Mock setup ---
|
|
45
|
+
const mockSNClient = {
|
|
46
|
+
getScopeId: jest.fn(),
|
|
47
|
+
getUserSysId: jest.fn(),
|
|
48
|
+
getCurrentAppUserPrefSysId: jest.fn(),
|
|
49
|
+
updateCurrentAppUserPref: jest.fn(),
|
|
50
|
+
createCurrentAppUserPref: jest.fn(),
|
|
51
|
+
changeScope: jest.fn().mockResolvedValue(undefined),
|
|
52
|
+
createUpdateSet: jest.fn(),
|
|
53
|
+
changeUpdateSet: jest.fn(),
|
|
54
|
+
getCurrentUpdateSet: jest.fn(),
|
|
55
|
+
client: {
|
|
56
|
+
get: jest.fn(),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
jest.mock("../snClient", () => ({
|
|
60
|
+
defaultClient: jest.fn(() => mockSNClient),
|
|
61
|
+
unwrapSNResponse: jest.fn((val) => val),
|
|
62
|
+
}));
|
|
63
|
+
jest.mock("../FileUtils", () => ({
|
|
64
|
+
getFileContextFromPath: jest.fn(),
|
|
65
|
+
getFileContextWithSkipReason: jest.fn(),
|
|
66
|
+
}));
|
|
67
|
+
jest.mock("../appUtils", () => ({
|
|
68
|
+
groupAppFiles: jest.fn(),
|
|
69
|
+
pushFiles: jest.fn(),
|
|
70
|
+
}));
|
|
71
|
+
jest.mock("../logMessages", () => ({
|
|
72
|
+
logFilePush: jest.fn(),
|
|
73
|
+
}));
|
|
74
|
+
jest.mock("../recentEdits", () => ({
|
|
75
|
+
writeRecentEdit: jest.fn(),
|
|
76
|
+
}));
|
|
77
|
+
jest.mock("../Logger", () => ({
|
|
78
|
+
logger: {
|
|
79
|
+
info: jest.fn(),
|
|
80
|
+
error: jest.fn(),
|
|
81
|
+
warn: jest.fn(),
|
|
82
|
+
debug: jest.fn(),
|
|
83
|
+
success: jest.fn(),
|
|
84
|
+
getLogLevel: jest.fn().mockReturnValue("info"),
|
|
85
|
+
},
|
|
86
|
+
}));
|
|
87
|
+
jest.mock("../config", () => ({
|
|
88
|
+
loadConfigs: jest.fn().mockResolvedValue(undefined),
|
|
89
|
+
getConfig: jest.fn(),
|
|
90
|
+
getRootDir: jest.fn().mockReturnValue("/project"),
|
|
91
|
+
updateManifest: jest.fn(),
|
|
92
|
+
getManifest: jest.fn(),
|
|
93
|
+
getSourcePath: jest.fn().mockReturnValue("/project/src"),
|
|
94
|
+
getScopeManifestPath: jest.fn((scope) => `/project/sinc.manifest.${scope}.json`),
|
|
95
|
+
getManifestPath: jest.fn().mockReturnValue("/project/sinc.manifest.json"),
|
|
96
|
+
}));
|
|
97
|
+
// Filesystem mock store
|
|
98
|
+
var mockFsStore = {};
|
|
99
|
+
var mockFsStatStore = {};
|
|
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((p) => {
|
|
113
|
+
if (p in mockFsStatStore)
|
|
114
|
+
return mockFsStatStore[p];
|
|
115
|
+
return { mtimeMs: Date.now() };
|
|
116
|
+
}),
|
|
117
|
+
promises: {
|
|
118
|
+
readFile: jest.fn(),
|
|
119
|
+
writeFile: jest.fn(),
|
|
120
|
+
readdir: jest.fn(),
|
|
121
|
+
mkdir: jest.fn(),
|
|
122
|
+
access: jest.fn(),
|
|
123
|
+
stat: jest.fn(),
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
jest.mock("chokidar", () => ({
|
|
127
|
+
watch: jest.fn(() => ({
|
|
128
|
+
on: jest.fn().mockReturnThis(),
|
|
129
|
+
close: jest.fn(),
|
|
130
|
+
})),
|
|
131
|
+
}));
|
|
132
|
+
jest.mock("lodash", () => {
|
|
133
|
+
var actual = jest.requireActual("lodash");
|
|
134
|
+
return {
|
|
135
|
+
...actual,
|
|
136
|
+
debounce: jest.fn((fn) => {
|
|
137
|
+
var wrapper = jest.fn();
|
|
138
|
+
wrapper.cancel = jest.fn();
|
|
139
|
+
wrapper.flush = jest.fn(() => fn());
|
|
140
|
+
return wrapper;
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
jest.mock("axios", () => ({
|
|
145
|
+
default: {
|
|
146
|
+
create: jest.fn(() => ({
|
|
147
|
+
get: jest.fn().mockResolvedValue({ data: { result: null } }),
|
|
148
|
+
})),
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
151
|
+
// --- Imports ---
|
|
152
|
+
const Logger_1 = require("../Logger");
|
|
153
|
+
const MultiScopeWatcher_1 = require("../MultiScopeWatcher");
|
|
154
|
+
const path = __importStar(require("path"));
|
|
155
|
+
// --- Tests ---
|
|
156
|
+
describe("US-012: Add staleness warning for active task", () => {
|
|
157
|
+
var taskPath;
|
|
158
|
+
beforeEach(() => {
|
|
159
|
+
jest.clearAllMocks();
|
|
160
|
+
mockFsStore = {};
|
|
161
|
+
mockFsStatStore = {};
|
|
162
|
+
taskPath = path.resolve(process.cwd(), ".sinc-active-task.json");
|
|
163
|
+
});
|
|
164
|
+
it("should warn when active task file is older than 7 days", () => {
|
|
165
|
+
var validTask = {
|
|
166
|
+
taskId: "TASK-123",
|
|
167
|
+
taskName: "Old Feature",
|
|
168
|
+
updateSetName: "CU-TASK-123",
|
|
169
|
+
};
|
|
170
|
+
mockFsStore[taskPath] = JSON.stringify(validTask);
|
|
171
|
+
// 10 days ago
|
|
172
|
+
mockFsStatStore[taskPath] = { mtimeMs: Date.now() - (10 * 24 * 60 * 60 * 1000) };
|
|
173
|
+
var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
|
|
174
|
+
expect(result).not.toBeNull();
|
|
175
|
+
expect(result.taskId).toBe("TASK-123");
|
|
176
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("Old Feature"));
|
|
177
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("10 days ago"));
|
|
178
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("sinc task clear"));
|
|
179
|
+
});
|
|
180
|
+
it("should not warn when active task file is fresh (under 7 days)", () => {
|
|
181
|
+
var validTask = {
|
|
182
|
+
taskId: "TASK-456",
|
|
183
|
+
taskName: "Recent Feature",
|
|
184
|
+
updateSetName: "CU-TASK-456",
|
|
185
|
+
};
|
|
186
|
+
mockFsStore[taskPath] = JSON.stringify(validTask);
|
|
187
|
+
// 2 days ago
|
|
188
|
+
mockFsStatStore[taskPath] = { mtimeMs: Date.now() - (2 * 24 * 60 * 60 * 1000) };
|
|
189
|
+
var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
|
|
190
|
+
expect(result).not.toBeNull();
|
|
191
|
+
expect(result.taskId).toBe("TASK-456");
|
|
192
|
+
expect(Logger_1.logger.warn).not.toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
it("should use taskId as fallback when taskName is missing", () => {
|
|
195
|
+
var taskNoName = {
|
|
196
|
+
taskId: "TASK-789",
|
|
197
|
+
updateSetName: "CU-TASK-789",
|
|
198
|
+
};
|
|
199
|
+
mockFsStore[taskPath] = JSON.stringify(taskNoName);
|
|
200
|
+
// 14 days ago
|
|
201
|
+
mockFsStatStore[taskPath] = { mtimeMs: Date.now() - (14 * 24 * 60 * 60 * 1000) };
|
|
202
|
+
var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
|
|
203
|
+
expect(result).not.toBeNull();
|
|
204
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("TASK-789"));
|
|
205
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("14 days ago"));
|
|
206
|
+
});
|
|
207
|
+
it("should not warn when task file is exactly 6 days old", () => {
|
|
208
|
+
var validTask = {
|
|
209
|
+
taskId: "TASK-EDGE",
|
|
210
|
+
taskName: "Edge Case",
|
|
211
|
+
updateSetName: "CU-TASK-EDGE",
|
|
212
|
+
};
|
|
213
|
+
mockFsStore[taskPath] = JSON.stringify(validTask);
|
|
214
|
+
// 6 days ago
|
|
215
|
+
mockFsStatStore[taskPath] = { mtimeMs: Date.now() - (6 * 24 * 60 * 60 * 1000) };
|
|
216
|
+
var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
|
|
217
|
+
expect(result).not.toBeNull();
|
|
218
|
+
expect(Logger_1.logger.warn).not.toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -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/sinc.manifest.${scope}.json`),
|
|
96
|
+
getManifestPath: jest.fn().mockReturnValue("/project/sinc.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(), ".sinc-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(), ".sinc-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(), ".sinc-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(), ".sinc-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(), ".sinc-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(), ".sinc-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(), ".sinc-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(), ".sinc-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
|
+
});
|