@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
package/dist/commands.js
CHANGED
|
@@ -43,6 +43,7 @@ exports.downloadCommand = downloadCommand;
|
|
|
43
43
|
exports.initCommand = initCommand;
|
|
44
44
|
exports.buildCommand = buildCommand;
|
|
45
45
|
exports.deployCommand = deployCommand;
|
|
46
|
+
exports.taskClearCommand = taskClearCommand;
|
|
46
47
|
exports.statusCommand = statusCommand;
|
|
47
48
|
const ConfigManager = __importStar(require("./config"));
|
|
48
49
|
const AppUtils = __importStar(require("./appUtils"));
|
|
@@ -54,6 +55,8 @@ const snClient_1 = require("./snClient");
|
|
|
54
55
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
55
56
|
const gitUtils_1 = require("./gitUtils");
|
|
56
57
|
const FileUtils_1 = require("./FileUtils");
|
|
58
|
+
const path = __importStar(require("path"));
|
|
59
|
+
const fs = __importStar(require("fs"));
|
|
57
60
|
function setLogLevel(args) {
|
|
58
61
|
Logger_1.logger.setLogLevel(args.logLevel);
|
|
59
62
|
}
|
|
@@ -300,6 +303,26 @@ async function deployCommand(args) {
|
|
|
300
303
|
throw e;
|
|
301
304
|
}
|
|
302
305
|
}
|
|
306
|
+
async function taskClearCommand(args) {
|
|
307
|
+
setLogLevel(args);
|
|
308
|
+
var taskPath = path.resolve(process.cwd(), ".sinc-active-task.json");
|
|
309
|
+
if (fs.existsSync(taskPath)) {
|
|
310
|
+
try {
|
|
311
|
+
var parsed = JSON.parse(fs.readFileSync(taskPath, "utf8"));
|
|
312
|
+
var taskName = parsed.taskName || parsed.taskId || "unknown";
|
|
313
|
+
fs.unlinkSync(taskPath);
|
|
314
|
+
Logger_1.logger.success("Active task '" + taskName + "' cleared.");
|
|
315
|
+
}
|
|
316
|
+
catch (e) {
|
|
317
|
+
// File exists but can't be parsed — still remove it
|
|
318
|
+
fs.unlinkSync(taskPath);
|
|
319
|
+
Logger_1.logger.success("Active task file removed.");
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
Logger_1.logger.info("No active task is currently set.");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
303
326
|
async function statusCommand() {
|
|
304
327
|
try {
|
|
305
328
|
const client = (0, snClient_1.defaultClient)();
|
|
@@ -308,6 +331,17 @@ async function statusCommand() {
|
|
|
308
331
|
Logger_1.logger.info("Instance: " + (process.env.SN_INSTANCE || "not set"));
|
|
309
332
|
Logger_1.logger.info("User: " + (process.env.SN_USER || "not set"));
|
|
310
333
|
Logger_1.logger.info("Active scope: " + scopeObj.scope);
|
|
334
|
+
// Read update set config
|
|
335
|
+
var updateSetConfig = {};
|
|
336
|
+
var updateSetConfigPath = path.resolve(process.cwd(), ".sinc-update-sets.json");
|
|
337
|
+
try {
|
|
338
|
+
if (fs.existsSync(updateSetConfigPath)) {
|
|
339
|
+
updateSetConfig = JSON.parse(fs.readFileSync(updateSetConfigPath, "utf8"));
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (e) {
|
|
343
|
+
Logger_1.logger.warn("Failed to parse .sinc-update-sets.json: " + (e instanceof Error ? e.message : String(e)));
|
|
344
|
+
}
|
|
311
345
|
if (config.scopes) {
|
|
312
346
|
var scopeNames = Object.keys(config.scopes);
|
|
313
347
|
Logger_1.logger.info("\nConfigured scopes (" + scopeNames.length + "):");
|
|
@@ -318,7 +352,10 @@ async function statusCommand() {
|
|
|
318
352
|
? scopeConf.sourceDirectory
|
|
319
353
|
: "src/" + scopeName;
|
|
320
354
|
var marker = scopeName === scopeObj.scope ? " (active)" : "";
|
|
321
|
-
|
|
355
|
+
var updateSetInfo = updateSetConfig[scopeName]
|
|
356
|
+
? " [update set: " + updateSetConfig[scopeName].name + "]"
|
|
357
|
+
: " [no update set configured]";
|
|
358
|
+
Logger_1.logger.info(" " + scopeName + marker + " — " + srcDir + updateSetInfo);
|
|
322
359
|
}
|
|
323
360
|
}
|
|
324
361
|
}
|
package/dist/config.js
CHANGED
|
@@ -223,7 +223,7 @@ function generateConfigFile(params) {
|
|
|
223
223
|
"\t\t\tmatch: /sys_script_include.*\\.ts$/,\n" +
|
|
224
224
|
"\t\t\tplugins: [\n" +
|
|
225
225
|
"\t\t\t\t{\n" +
|
|
226
|
-
"\t\t\t\t\tname: \"@sincronia
|
|
226
|
+
"\t\t\t\t\tname: \"@tenonhq/sincronia-typescript-plugin\",\n" +
|
|
227
227
|
"\t\t\t\t\toptions: {\n" +
|
|
228
228
|
"\t\t\t\t\t\ttranspile: true,\n" +
|
|
229
229
|
"\t\t\t\t\t\ttypeCheck: true,\n" +
|
|
@@ -231,7 +231,7 @@ function generateConfigFile(params) {
|
|
|
231
231
|
"\t\t\t\t\t},\n" +
|
|
232
232
|
"\t\t\t\t},\n" +
|
|
233
233
|
"\t\t\t\t{\n" +
|
|
234
|
-
"\t\t\t\t\tname: \"@sincronia
|
|
234
|
+
"\t\t\t\t\tname: \"@tenonhq/sincronia-eslint-plugin\",\n" +
|
|
235
235
|
"\t\t\t\t\toptions: {\n" +
|
|
236
236
|
"\t\t\t\t\t\tconfigFile: \"./.eslintrc.js\",\n" +
|
|
237
237
|
"\t\t\t\t\t\tfix: false,\n" +
|
|
@@ -239,13 +239,13 @@ function generateConfigFile(params) {
|
|
|
239
239
|
"\t\t\t\t\t},\n" +
|
|
240
240
|
"\t\t\t\t},\n" +
|
|
241
241
|
"\t\t\t\t{\n" +
|
|
242
|
-
"\t\t\t\t\tname: \"@sincronia
|
|
242
|
+
"\t\t\t\t\tname: \"@tenonhq/sincronia-babel-plugin\",\n" +
|
|
243
243
|
"\t\t\t\t\toptions: {\n" +
|
|
244
244
|
"\t\t\t\t\t\tconfigFile: \"./.babelrc\",\n" +
|
|
245
245
|
"\t\t\t\t\t},\n" +
|
|
246
246
|
"\t\t\t\t},\n" +
|
|
247
247
|
"\t\t\t\t{\n" +
|
|
248
|
-
"\t\t\t\t\tname: \"@sincronia
|
|
248
|
+
"\t\t\t\t\tname: \"@tenonhq/sincronia-prettier-plugin\",\n" +
|
|
249
249
|
"\t\t\t\t\toptions: {\n" +
|
|
250
250
|
"\t\t\t\t\t\tconfigFile: \"./.prettierrc.js\",\n" +
|
|
251
251
|
"\t\t\t\t\t},\n" +
|
package/dist/snClient.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.unwrapSNResponse = exports.defaultClient = exports.snClient = exports.processPushResponse = exports.retryOnErr = void 0;
|
|
6
|
+
exports.unwrapSNResponse = exports.defaultClient = exports.snClient = exports.processPushResponse = exports.retryOnHttpErr = exports.retryOnErr = void 0;
|
|
7
7
|
exports.unwrapTableAPIFirstItem = unwrapTableAPIFirstItem;
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
9
|
const axios_cookiejar_support_1 = require("axios-cookiejar-support");
|
|
@@ -41,6 +41,86 @@ const retryOnErr = async (f, allowedRetries, msBetween = 0, onRetry) => {
|
|
|
41
41
|
}
|
|
42
42
|
};
|
|
43
43
|
exports.retryOnErr = retryOnErr;
|
|
44
|
+
function _getHttpStatus(e) {
|
|
45
|
+
if (axios_1.default.isAxiosError(e) && e.response) {
|
|
46
|
+
return e.response.status;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function _getRetryAfterMs(e) {
|
|
51
|
+
if (axios_1.default.isAxiosError(e) && e.response && e.response.headers) {
|
|
52
|
+
var retryAfter = e.response.headers["retry-after"];
|
|
53
|
+
if (retryAfter) {
|
|
54
|
+
var seconds = Number(retryAfter);
|
|
55
|
+
if (!isNaN(seconds) && seconds > 0) {
|
|
56
|
+
return seconds * 1000;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return 10000; // Default 10s for 429 without Retry-After
|
|
61
|
+
}
|
|
62
|
+
const retryOnHttpErr = async (f, recSummary) => {
|
|
63
|
+
var maxServerRetries = 3;
|
|
64
|
+
var backoffMs = 1000;
|
|
65
|
+
var attempt = 0;
|
|
66
|
+
while (true) {
|
|
67
|
+
try {
|
|
68
|
+
return await f();
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
var status = _getHttpStatus(e);
|
|
72
|
+
attempt++;
|
|
73
|
+
// 401/403: Auth failure — fail immediately
|
|
74
|
+
if (status === 401 || status === 403) {
|
|
75
|
+
var authMsg = status === 401 ? "Unauthorized" : "Forbidden";
|
|
76
|
+
Logger_1.logger.error(authMsg + " (" + status + ") pushing " + recSummary +
|
|
77
|
+
". Verify your ServiceNow credentials and permissions.");
|
|
78
|
+
throw e;
|
|
79
|
+
}
|
|
80
|
+
// 404: Record not found — fail immediately
|
|
81
|
+
if (status === 404) {
|
|
82
|
+
Logger_1.logger.error("Record not found (404) pushing " + recSummary +
|
|
83
|
+
". The record may have been deleted from the instance.");
|
|
84
|
+
throw e;
|
|
85
|
+
}
|
|
86
|
+
// 429: Rate limited — honor Retry-After, then retry
|
|
87
|
+
if (status === 429) {
|
|
88
|
+
var retryWait = _getRetryAfterMs(e);
|
|
89
|
+
Logger_1.logger.warn("Rate limited (429) pushing " + recSummary +
|
|
90
|
+
". Waiting " + Math.round(retryWait / 1000) + "s before retry.");
|
|
91
|
+
await (0, genericUtils_1.wait)(retryWait);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// 500/502/503: Server error — exponential backoff, max 3 retries
|
|
95
|
+
if (status === 500 || status === 502 || status === 503) {
|
|
96
|
+
if (attempt > maxServerRetries) {
|
|
97
|
+
Logger_1.logger.error("Server error (" + status + ") pushing " + recSummary +
|
|
98
|
+
" after " + maxServerRetries + " retries. Giving up.");
|
|
99
|
+
throw e;
|
|
100
|
+
}
|
|
101
|
+
var cappedBackoff = Math.min(backoffMs, 8000);
|
|
102
|
+
Logger_1.logger.warn("Server error (" + status + ") pushing " + recSummary +
|
|
103
|
+
". Retrying in " + (cappedBackoff / 1000) + "s (" +
|
|
104
|
+
(maxServerRetries - attempt) + " retries left).");
|
|
105
|
+
await (0, genericUtils_1.wait)(cappedBackoff);
|
|
106
|
+
backoffMs = backoffMs * 2;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// Unknown status or non-HTTP error — retry once, then fail
|
|
110
|
+
if (attempt > 1) {
|
|
111
|
+
var errDetail = status ? "HTTP " + status : "unknown error";
|
|
112
|
+
Logger_1.logger.error("Push failed for " + recSummary + " (" + errDetail +
|
|
113
|
+
") after 1 retry. Giving up.");
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
var retryDetail = status ? "HTTP " + status : "unknown error";
|
|
117
|
+
Logger_1.logger.warn("Unexpected error (" + retryDetail + ") pushing " + recSummary +
|
|
118
|
+
". Retrying once.");
|
|
119
|
+
await (0, genericUtils_1.wait)(1000);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
exports.retryOnHttpErr = retryOnHttpErr;
|
|
44
124
|
const processPushResponse = (response, recSummary) => {
|
|
45
125
|
const { status } = response;
|
|
46
126
|
if (status === 404) {
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for US-006: Surface warning when no update set is configured
|
|
4
|
+
*
|
|
5
|
+
* Validates:
|
|
6
|
+
* - Prominent warning when readActiveTask() returns null and no update set configured
|
|
7
|
+
* - Warning message includes specific text about Default and remediation steps
|
|
8
|
+
* - Invalid scope name produces a clear error (not a global-scope update set)
|
|
9
|
+
* - scopeSysId validated before creating update set; null/undefined skips with error
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
// --- Mock setup ---
|
|
13
|
+
const 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
|
+
client: {
|
|
23
|
+
get: jest.fn(),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
jest.mock("../snClient", () => ({
|
|
27
|
+
defaultClient: jest.fn(() => mockSNClient),
|
|
28
|
+
unwrapSNResponse: jest.fn((val) => val),
|
|
29
|
+
}));
|
|
30
|
+
jest.mock("../FileUtils", () => ({
|
|
31
|
+
getFileContextFromPath: jest.fn(),
|
|
32
|
+
getFileContextWithSkipReason: jest.fn(),
|
|
33
|
+
}));
|
|
34
|
+
jest.mock("../appUtils", () => ({
|
|
35
|
+
groupAppFiles: jest.fn(),
|
|
36
|
+
pushFiles: jest.fn(),
|
|
37
|
+
}));
|
|
38
|
+
jest.mock("../logMessages", () => ({
|
|
39
|
+
logFilePush: jest.fn(),
|
|
40
|
+
}));
|
|
41
|
+
jest.mock("../recentEdits", () => ({
|
|
42
|
+
writeRecentEdit: jest.fn(),
|
|
43
|
+
}));
|
|
44
|
+
jest.mock("../Logger", () => ({
|
|
45
|
+
logger: {
|
|
46
|
+
info: jest.fn(),
|
|
47
|
+
error: jest.fn(),
|
|
48
|
+
warn: jest.fn(),
|
|
49
|
+
debug: jest.fn(),
|
|
50
|
+
success: jest.fn(),
|
|
51
|
+
getLogLevel: jest.fn().mockReturnValue("info"),
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
jest.mock("../config", () => ({
|
|
55
|
+
loadConfigs: jest.fn().mockResolvedValue(undefined),
|
|
56
|
+
getConfig: jest.fn(),
|
|
57
|
+
getRootDir: jest.fn().mockReturnValue("/project"),
|
|
58
|
+
updateManifest: jest.fn(),
|
|
59
|
+
getManifest: jest.fn(),
|
|
60
|
+
getSourcePath: jest.fn().mockReturnValue("/project/src"),
|
|
61
|
+
getScopeManifestPath: jest.fn((scope) => `/project/sinc.manifest.${scope}.json`),
|
|
62
|
+
getManifestPath: jest.fn().mockReturnValue("/project/sinc.manifest.json"),
|
|
63
|
+
}));
|
|
64
|
+
// Filesystem mock store
|
|
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-006: Surface warning when no update set is configured", () => {
|
|
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("warns with specific message when no active task and no update set configured", async () => {
|
|
132
|
+
// No .sinc-update-sets.json and no .sinc-active-task.json
|
|
133
|
+
await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_test_core");
|
|
134
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("No update set configured for scope x_test_core"));
|
|
135
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("Changes will go to Default"));
|
|
136
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("sinc createUpdateSet"));
|
|
137
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("activate a task in the dashboard"));
|
|
138
|
+
});
|
|
139
|
+
it("does not warn when update set already configured for scope", async () => {
|
|
140
|
+
// Pre-populate the update set config
|
|
141
|
+
var configPath = require("path").resolve(process.cwd(), ".sinc-update-sets.json");
|
|
142
|
+
mockFsStore[configPath] = JSON.stringify({
|
|
143
|
+
x_test_core: { sys_id: "us_123", name: "My Update Set" }
|
|
144
|
+
});
|
|
145
|
+
await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_test_core");
|
|
146
|
+
// Should return early — no warning
|
|
147
|
+
expect(Logger_1.logger.warn).not.toHaveBeenCalled();
|
|
148
|
+
expect(Logger_1.logger.error).not.toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
it("logs error and skips creation when scope not found on instance (scopeSysId is undefined)", async () => {
|
|
151
|
+
// Active task exists so we get past the no-task check
|
|
152
|
+
var taskPath = require("path").resolve(process.cwd(), ".sinc-active-task.json");
|
|
153
|
+
mockFsStore[taskPath] = JSON.stringify({
|
|
154
|
+
taskId: "abc123",
|
|
155
|
+
taskName: "Test Task",
|
|
156
|
+
taskDescription: "Test",
|
|
157
|
+
updateSetName: "CU-abc123 Test Task",
|
|
158
|
+
description: "Test update set",
|
|
159
|
+
taskUrl: "https://example.com",
|
|
160
|
+
scopes: {},
|
|
161
|
+
});
|
|
162
|
+
// getScopeId returns empty — scope doesn't exist
|
|
163
|
+
mockSNClient.getScopeId.mockResolvedValue([]);
|
|
164
|
+
await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_invalid_scope");
|
|
165
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("x_invalid_scope"));
|
|
166
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("not found on the instance"));
|
|
167
|
+
// Should NOT attempt to create an update set
|
|
168
|
+
expect(mockSNClient.createUpdateSet).not.toHaveBeenCalled();
|
|
169
|
+
expect(mockSNClient.client.get).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
it("logs error and skips creation when getScopeId returns null", async () => {
|
|
172
|
+
var taskPath = require("path").resolve(process.cwd(), ".sinc-active-task.json");
|
|
173
|
+
mockFsStore[taskPath] = JSON.stringify({
|
|
174
|
+
taskId: "abc123",
|
|
175
|
+
taskName: "Test Task",
|
|
176
|
+
taskDescription: "Test",
|
|
177
|
+
updateSetName: "CU-abc123 Test Task",
|
|
178
|
+
description: "Test update set",
|
|
179
|
+
taskUrl: "https://example.com",
|
|
180
|
+
scopes: {},
|
|
181
|
+
});
|
|
182
|
+
// getScopeId returns null
|
|
183
|
+
mockSNClient.getScopeId.mockResolvedValue(null);
|
|
184
|
+
await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_bad_scope");
|
|
185
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("x_bad_scope"));
|
|
186
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("not found on the instance"));
|
|
187
|
+
expect(mockSNClient.createUpdateSet).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
it("proceeds to create update set when scope is valid", async () => {
|
|
190
|
+
var taskPath = require("path").resolve(process.cwd(), ".sinc-active-task.json");
|
|
191
|
+
mockFsStore[taskPath] = JSON.stringify({
|
|
192
|
+
taskId: "abc123",
|
|
193
|
+
taskName: "Test Task",
|
|
194
|
+
taskDescription: "Test",
|
|
195
|
+
updateSetName: "CU-abc123 Test Task",
|
|
196
|
+
description: "Test update set",
|
|
197
|
+
taskUrl: "https://example.com",
|
|
198
|
+
scopes: {},
|
|
199
|
+
});
|
|
200
|
+
// Valid scope
|
|
201
|
+
mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "valid_scope_sys_id" }]);
|
|
202
|
+
// Search returns existing update set
|
|
203
|
+
mockSNClient.client.get.mockResolvedValue({
|
|
204
|
+
data: { result: [{ sys_id: "us_existing", name: "CU-abc123 Test Task", state: "in progress" }] }
|
|
205
|
+
});
|
|
206
|
+
// changeUpdateSet succeeds
|
|
207
|
+
mockSNClient.changeUpdateSet.mockResolvedValue(undefined);
|
|
208
|
+
await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_valid_scope");
|
|
209
|
+
// Should have searched for existing update sets (scope was valid)
|
|
210
|
+
expect(mockSNClient.client.get).toHaveBeenCalled();
|
|
211
|
+
// No "not found on the instance" error
|
|
212
|
+
var errorCalls = Logger_1.logger.error.mock.calls.map(function (c) { return c[0]; });
|
|
213
|
+
var hasInvalidScopeError = errorCalls.some(function (msg) {
|
|
214
|
+
return msg.indexOf("not found on the instance") !== -1;
|
|
215
|
+
});
|
|
216
|
+
expect(hasInvalidScopeError).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
});
|