@tenonhq/sincronia-core 0.0.80 → 0.0.82
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/dist/FileUtils.js +40 -1
- package/dist/MultiScopeWatcher.js +6 -3
- package/dist/appUtils.js +283 -15
- package/dist/benchmark.js +104 -0
- package/dist/commander.js +25 -1
- package/dist/commands.js +5 -2
- package/dist/snClient.js +105 -8
- package/dist/tests/benchmarkRefresh.test.js +265 -0
- package/dist/tests/globalDebounce.test.js +3 -2
- package/dist/tests/multi-scope-watcher.test.js +10 -20
- package/dist/tests/scopeCaching.test.js +14 -23
- package/dist/tests/syncManifestRefresh.test.js +287 -0
- package/dist/tests/syncManifestWhitelist.test.js +192 -0
- package/dist/tests/unwrapSNResponseError.test.js +145 -0
- package/package.json +1 -1
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the refresh content-compare path.
|
|
4
|
+
*
|
|
5
|
+
* Regression target: `npx sinc refresh` previously only downloaded files that
|
|
6
|
+
* were absent from disk. Files edited on the ServiceNow instance would never
|
|
7
|
+
* propagate to a developer's local copy — the refresh command silently
|
|
8
|
+
* skipped existing files without checking content.
|
|
9
|
+
*
|
|
10
|
+
* These tests assert:
|
|
11
|
+
* 1. refresh fetches content for EVERY file in the manifest (not just missing)
|
|
12
|
+
* 2. Files whose local content differs are overwritten
|
|
13
|
+
* 3. Files whose local content matches are left alone (no rewrite)
|
|
14
|
+
* 4. Files missing locally are created
|
|
15
|
+
* 5. --force bypasses the content check and writes all files unconditionally
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
// ---------- mocks (must come before importing the module under test) ----------
|
|
55
|
+
var mockLogger = {
|
|
56
|
+
info: jest.fn(),
|
|
57
|
+
debug: jest.fn(),
|
|
58
|
+
warn: jest.fn(),
|
|
59
|
+
error: jest.fn(),
|
|
60
|
+
success: jest.fn(),
|
|
61
|
+
getLogLevel: function () { return "warn"; },
|
|
62
|
+
};
|
|
63
|
+
var mockFileLogger = {
|
|
64
|
+
debug: jest.fn(),
|
|
65
|
+
info: jest.fn(),
|
|
66
|
+
warn: jest.fn(),
|
|
67
|
+
error: jest.fn(),
|
|
68
|
+
};
|
|
69
|
+
var mockClient = {
|
|
70
|
+
getManifest: jest.fn(),
|
|
71
|
+
getMissingFiles: jest.fn(),
|
|
72
|
+
};
|
|
73
|
+
jest.mock("../Logger", function () { return { logger: mockLogger }; });
|
|
74
|
+
jest.mock("../FileLogger", function () { return { fileLogger: mockFileLogger }; });
|
|
75
|
+
jest.mock("../snClient", function () {
|
|
76
|
+
return {
|
|
77
|
+
defaultClient: function () { return mockClient; },
|
|
78
|
+
unwrapSNResponse: function (p) { return Promise.resolve(p).then(function (r) { return r; }); },
|
|
79
|
+
processPushResponse: jest.fn(),
|
|
80
|
+
retryOnErr: jest.fn(),
|
|
81
|
+
retryOnHttpErr: jest.fn(),
|
|
82
|
+
unwrapTableAPIFirstItem: jest.fn(),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
var mockConfig = {
|
|
86
|
+
getConfig: jest.fn().mockReturnValue({
|
|
87
|
+
scopes: { x_cadso_core: {} },
|
|
88
|
+
tableOptions: {},
|
|
89
|
+
}),
|
|
90
|
+
getManifest: jest.fn().mockResolvedValue({
|
|
91
|
+
x_cadso_core: { scope: "x_cadso_core", tables: {} },
|
|
92
|
+
}),
|
|
93
|
+
getSourcePathForScope: jest.fn(),
|
|
94
|
+
getSourcePath: jest.fn(),
|
|
95
|
+
getManifestPath: jest.fn().mockReturnValue("/tmp/sinc.manifest.json"),
|
|
96
|
+
resolveConfigForScope: jest.fn().mockImplementation(function () {
|
|
97
|
+
return {
|
|
98
|
+
tables: ["sys_script_include"],
|
|
99
|
+
fieldOverrides: {},
|
|
100
|
+
apiIncludes: {},
|
|
101
|
+
apiExcludes: {},
|
|
102
|
+
};
|
|
103
|
+
}),
|
|
104
|
+
isMultiScopeManifest: jest.fn().mockReturnValue(true),
|
|
105
|
+
updateManifest: jest.fn(),
|
|
106
|
+
};
|
|
107
|
+
jest.mock("../config", function () { return mockConfig; });
|
|
108
|
+
// Silence ProgressBar's stdout writes.
|
|
109
|
+
jest.mock("progress", function () {
|
|
110
|
+
return jest.fn().mockImplementation(function () {
|
|
111
|
+
return { tick: jest.fn() };
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
// Stub writeScopeManifest so we don't actually write a manifest file. Leave
|
|
115
|
+
// the other FileUtils exports (writeSNFileCurry, writeSNFileIfDifferent,
|
|
116
|
+
// createDirRecursively, etc.) as real implementations so we can assert
|
|
117
|
+
// against the file system.
|
|
118
|
+
jest.mock("../FileUtils", function () {
|
|
119
|
+
var actual = jest.requireActual("../FileUtils");
|
|
120
|
+
return Object.assign({}, actual, {
|
|
121
|
+
writeScopeManifest: jest.fn().mockResolvedValue(undefined),
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
const AppUtils = __importStar(require("../appUtils"));
|
|
125
|
+
// ---------- tmp dir scaffolding ----------
|
|
126
|
+
var tmpRoot;
|
|
127
|
+
function setupScope(files) {
|
|
128
|
+
// Build a manifest whose single table (sys_script_include) contains the
|
|
129
|
+
// supplied records/files. Content is stripped (manifests do not carry it).
|
|
130
|
+
var records = {};
|
|
131
|
+
files.forEach(function (f) {
|
|
132
|
+
if (!records[f.record]) {
|
|
133
|
+
records[f.record] = { name: f.record, sys_id: "sysid_" + f.record, files: [] };
|
|
134
|
+
}
|
|
135
|
+
records[f.record].files.push({ name: f.name, type: f.type });
|
|
136
|
+
});
|
|
137
|
+
return {
|
|
138
|
+
scope: "x_cadso_core",
|
|
139
|
+
tables: {
|
|
140
|
+
sys_script_include: { records: records },
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function writeLocal(record, name, type, content) {
|
|
145
|
+
var recDir = path.join(tmpRoot, "sys_script_include", record);
|
|
146
|
+
fs.mkdirSync(recDir, { recursive: true });
|
|
147
|
+
fs.writeFileSync(path.join(recDir, name + "." + type), content);
|
|
148
|
+
}
|
|
149
|
+
function readLocal(record, name, type) {
|
|
150
|
+
var p = path.join(tmpRoot, "sys_script_include", record, name + "." + type);
|
|
151
|
+
try {
|
|
152
|
+
return fs.readFileSync(p, "utf8");
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function statMtime(record, name, type) {
|
|
159
|
+
var p = path.join(tmpRoot, "sys_script_include", record, name + "." + type);
|
|
160
|
+
return fs.statSync(p).mtimeMs;
|
|
161
|
+
}
|
|
162
|
+
describe("syncManifest — refresh pulls instance edits down", function () {
|
|
163
|
+
beforeEach(function () {
|
|
164
|
+
jest.clearAllMocks();
|
|
165
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sinc-refresh-test-"));
|
|
166
|
+
mockConfig.getSourcePathForScope.mockReturnValue(tmpRoot);
|
|
167
|
+
mockConfig.getSourcePath.mockReturnValue(tmpRoot);
|
|
168
|
+
});
|
|
169
|
+
afterEach(function () {
|
|
170
|
+
try {
|
|
171
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
catch (e) { }
|
|
174
|
+
});
|
|
175
|
+
test("overwrites existing file when instance content differs", async function () {
|
|
176
|
+
writeLocal("Foo", "script", "js", "var x = 1;"); // stale local
|
|
177
|
+
var manifest = setupScope([{ record: "Foo", name: "script", type: "js", content: "" }]);
|
|
178
|
+
mockClient.getManifest.mockResolvedValue(manifest);
|
|
179
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
180
|
+
sys_script_include: {
|
|
181
|
+
records: {
|
|
182
|
+
Foo: {
|
|
183
|
+
name: "Foo",
|
|
184
|
+
sys_id: "sysid_Foo",
|
|
185
|
+
files: [{ name: "script", type: "js", content: "var x = 2; // from instance" }],
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
await AppUtils.syncManifest("x_cadso_core");
|
|
191
|
+
expect(mockClient.getMissingFiles).toHaveBeenCalledTimes(1);
|
|
192
|
+
expect(readLocal("Foo", "script", "js")).toBe("var x = 2; // from instance");
|
|
193
|
+
});
|
|
194
|
+
test("does not rewrite a file when content already matches", async function () {
|
|
195
|
+
var identical = "var answer = 42;";
|
|
196
|
+
writeLocal("Bar", "script", "js", identical);
|
|
197
|
+
var beforeMtime = statMtime("Bar", "script", "js");
|
|
198
|
+
var manifest = setupScope([{ record: "Bar", name: "script", type: "js", content: "" }]);
|
|
199
|
+
mockClient.getManifest.mockResolvedValue(manifest);
|
|
200
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
201
|
+
sys_script_include: {
|
|
202
|
+
records: {
|
|
203
|
+
Bar: {
|
|
204
|
+
name: "Bar",
|
|
205
|
+
sys_id: "sysid_Bar",
|
|
206
|
+
files: [{ name: "script", type: "js", content: identical }],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
// Give the fs a moment so any write would register a different mtime.
|
|
212
|
+
await new Promise(function (r) { setTimeout(r, 10); });
|
|
213
|
+
await AppUtils.syncManifest("x_cadso_core");
|
|
214
|
+
expect(readLocal("Bar", "script", "js")).toBe(identical);
|
|
215
|
+
expect(statMtime("Bar", "script", "js")).toBe(beforeMtime);
|
|
216
|
+
// metaData should NOT be written when nothing changed in the record
|
|
217
|
+
expect(readLocal("Bar", "metaData", "json")).toBeNull();
|
|
218
|
+
});
|
|
219
|
+
test("creates the file when it is missing locally", async function () {
|
|
220
|
+
var manifest = setupScope([{ record: "Baz", name: "script", type: "js", content: "" }]);
|
|
221
|
+
mockClient.getManifest.mockResolvedValue(manifest);
|
|
222
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
223
|
+
sys_script_include: {
|
|
224
|
+
records: {
|
|
225
|
+
Baz: {
|
|
226
|
+
name: "Baz",
|
|
227
|
+
sys_id: "sysid_Baz",
|
|
228
|
+
files: [{ name: "script", type: "js", content: "// fresh file" }],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
await AppUtils.syncManifest("x_cadso_core");
|
|
234
|
+
expect(readLocal("Baz", "script", "js")).toBe("// fresh file");
|
|
235
|
+
expect(readLocal("Baz", "metaData", "json")).not.toBeNull();
|
|
236
|
+
});
|
|
237
|
+
test("--force overwrites even when local content already matches", async function () {
|
|
238
|
+
var identical = "var same = true;";
|
|
239
|
+
writeLocal("Qux", "script", "js", identical);
|
|
240
|
+
var manifest = setupScope([{ record: "Qux", name: "script", type: "js", content: "" }]);
|
|
241
|
+
mockClient.getManifest.mockResolvedValue(manifest);
|
|
242
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
243
|
+
sys_script_include: {
|
|
244
|
+
records: {
|
|
245
|
+
Qux: {
|
|
246
|
+
name: "Qux",
|
|
247
|
+
sys_id: "sysid_Qux",
|
|
248
|
+
files: [{ name: "script", type: "js", content: identical }],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
var beforeMtime = statMtime("Qux", "script", "js");
|
|
254
|
+
await new Promise(function (r) { setTimeout(r, 10); });
|
|
255
|
+
await AppUtils.syncManifest("x_cadso_core", { force: true });
|
|
256
|
+
// Force path rewrites regardless — mtime should advance.
|
|
257
|
+
expect(statMtime("Qux", "script", "js")).toBeGreaterThan(beforeMtime);
|
|
258
|
+
expect(readLocal("Qux", "metaData", "json")).not.toBeNull();
|
|
259
|
+
});
|
|
260
|
+
test("bulkDownload is requested for EVERY file in the manifest, not just missing ones", async function () {
|
|
261
|
+
writeLocal("RecA", "script", "js", "local A");
|
|
262
|
+
writeLocal("RecB", "script", "js", "local B");
|
|
263
|
+
var manifest = setupScope([
|
|
264
|
+
{ record: "RecA", name: "script", type: "js", content: "" },
|
|
265
|
+
{ record: "RecB", name: "script", type: "js", content: "" },
|
|
266
|
+
]);
|
|
267
|
+
mockClient.getManifest.mockResolvedValue(manifest);
|
|
268
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
269
|
+
sys_script_include: {
|
|
270
|
+
records: {
|
|
271
|
+
RecA: {
|
|
272
|
+
name: "RecA", sys_id: "sysid_RecA",
|
|
273
|
+
files: [{ name: "script", type: "js", content: "local A" }],
|
|
274
|
+
},
|
|
275
|
+
RecB: {
|
|
276
|
+
name: "RecB", sys_id: "sysid_RecB",
|
|
277
|
+
files: [{ name: "script", type: "js", content: "local B" }],
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
await AppUtils.syncManifest("x_cadso_core");
|
|
283
|
+
expect(mockClient.getMissingFiles).toHaveBeenCalledTimes(1);
|
|
284
|
+
var missingArg = mockClient.getMissingFiles.mock.calls[0][0];
|
|
285
|
+
expect(Object.keys(missingArg.sys_script_include).sort()).toEqual(["sysid_RecA", "sysid_RecB"]);
|
|
286
|
+
});
|
|
287
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the scope + table whitelist gates in syncManifest() and
|
|
4
|
+
* processTablesInManifest().
|
|
5
|
+
*
|
|
6
|
+
* Regression target: 2026-04-14 sys_alias debris incident.
|
|
7
|
+
* - `npx sinc refresh` iterated every scope in the persisted multi-scope
|
|
8
|
+
* manifest, including scopes not declared in sinc.config.js.
|
|
9
|
+
* - For each of those scopes, the server-side getManifest response returned
|
|
10
|
+
* hundreds of tables (up to 129 for x_cadso_work) that the config _tables
|
|
11
|
+
* whitelist never authorised, and the client wrote all of them to disk —
|
|
12
|
+
* including sys_alias/sys_alias_templates folders with 314 field files per
|
|
13
|
+
* record (fan-out of every script/html/css/xml field config across tables).
|
|
14
|
+
*
|
|
15
|
+
* These tests assert the two defensive filters:
|
|
16
|
+
* 1. Undeclared scopes are skipped before any REST call.
|
|
17
|
+
* 2. Tables not in the _tables whitelist are filtered from the manifest
|
|
18
|
+
* before writeScopeManifest / processMissingFiles run.
|
|
19
|
+
*/
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
var mockLogger = {
|
|
55
|
+
info: jest.fn(),
|
|
56
|
+
debug: jest.fn(),
|
|
57
|
+
warn: jest.fn(),
|
|
58
|
+
error: jest.fn(),
|
|
59
|
+
success: jest.fn(),
|
|
60
|
+
getLogLevel: function () { return "info"; },
|
|
61
|
+
};
|
|
62
|
+
var mockFileLogger = {
|
|
63
|
+
debug: jest.fn(),
|
|
64
|
+
info: jest.fn(),
|
|
65
|
+
warn: jest.fn(),
|
|
66
|
+
error: jest.fn(),
|
|
67
|
+
};
|
|
68
|
+
var mockClient = {
|
|
69
|
+
getManifest: jest.fn(),
|
|
70
|
+
};
|
|
71
|
+
var mockFUtils = {
|
|
72
|
+
writeScopeManifest: jest.fn().mockResolvedValue(undefined),
|
|
73
|
+
writeFileForce: jest.fn().mockResolvedValue(undefined),
|
|
74
|
+
writeSNFileCurry: jest.fn(() => jest.fn().mockResolvedValue(undefined)),
|
|
75
|
+
createDirRecursively: jest.fn().mockResolvedValue(undefined),
|
|
76
|
+
};
|
|
77
|
+
jest.mock("../Logger", function () { return { logger: mockLogger }; });
|
|
78
|
+
jest.mock("../FileLogger", function () { return { fileLogger: mockFileLogger }; });
|
|
79
|
+
jest.mock("../FileUtils", function () { return mockFUtils; });
|
|
80
|
+
jest.mock("../snClient", function () {
|
|
81
|
+
return {
|
|
82
|
+
defaultClient: function () { return mockClient; },
|
|
83
|
+
unwrapSNResponse: function (p) { return Promise.resolve(p).then(function (r) { return r; }); },
|
|
84
|
+
processPushResponse: jest.fn(),
|
|
85
|
+
retryOnErr: jest.fn(),
|
|
86
|
+
retryOnHttpErr: jest.fn(),
|
|
87
|
+
unwrapTableAPIFirstItem: jest.fn(),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
// Mock config module. Individual tests override getConfig/getManifest/etc.
|
|
91
|
+
var mockConfig = {
|
|
92
|
+
getConfig: jest.fn(),
|
|
93
|
+
getManifest: jest.fn(),
|
|
94
|
+
getSourcePathForScope: jest.fn().mockReturnValue("/tmp/src"),
|
|
95
|
+
getSourcePath: jest.fn().mockReturnValue("/tmp/src"),
|
|
96
|
+
getManifestPath: jest.fn().mockReturnValue("/tmp/sinc.manifest.json"),
|
|
97
|
+
resolveConfigForScope: jest.fn(),
|
|
98
|
+
isMultiScopeManifest: jest.fn().mockReturnValue(true),
|
|
99
|
+
updateManifest: jest.fn(),
|
|
100
|
+
};
|
|
101
|
+
jest.mock("../config", function () { return mockConfig; });
|
|
102
|
+
// Prevent processMissingFiles' progress bar from touching stdout in tests.
|
|
103
|
+
jest.mock("progress", function () {
|
|
104
|
+
return jest.fn().mockImplementation(function () {
|
|
105
|
+
return { tick: jest.fn() };
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
// Keep the per-scope progress shim simple.
|
|
109
|
+
jest.mock("../genericUtils", function () {
|
|
110
|
+
var actual = jest.requireActual("../genericUtils");
|
|
111
|
+
return actual;
|
|
112
|
+
});
|
|
113
|
+
const AppUtils = __importStar(require("../appUtils"));
|
|
114
|
+
describe("syncManifest — scope + table whitelist gates", function () {
|
|
115
|
+
beforeEach(function () {
|
|
116
|
+
jest.clearAllMocks();
|
|
117
|
+
mockConfig.isMultiScopeManifest.mockReturnValue(true);
|
|
118
|
+
mockConfig.resolveConfigForScope.mockImplementation(function (_scope) {
|
|
119
|
+
return {
|
|
120
|
+
tables: ["sys_script_include", "sys_script", "sys_ux_macroponent"],
|
|
121
|
+
fieldOverrides: {},
|
|
122
|
+
apiIncludes: {},
|
|
123
|
+
apiExcludes: {},
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
test("skips undeclared scope — no REST call, warn logged", async function () {
|
|
128
|
+
mockConfig.getConfig.mockReturnValue({
|
|
129
|
+
scopes: { x_cadso_core: {}, x_cadso_work: {} },
|
|
130
|
+
});
|
|
131
|
+
mockConfig.getManifest.mockResolvedValue({
|
|
132
|
+
x_cadso_core: { scope: "x_cadso_core", tables: {} },
|
|
133
|
+
x_cadso_click: { scope: "x_cadso_click", tables: {} }, // stale undeclared
|
|
134
|
+
});
|
|
135
|
+
await AppUtils.syncManifest("x_cadso_click");
|
|
136
|
+
expect(mockClient.getManifest).not.toHaveBeenCalled();
|
|
137
|
+
expect(mockFUtils.writeScopeManifest).not.toHaveBeenCalled();
|
|
138
|
+
var warnedAboutScope = mockLogger.warn.mock.calls.some(function (args) {
|
|
139
|
+
return typeof args[0] === "string" && args[0].indexOf("x_cadso_click") !== -1;
|
|
140
|
+
});
|
|
141
|
+
expect(warnedAboutScope).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
test("declared scope — filters non-whitelisted tables before write", async function () {
|
|
144
|
+
mockConfig.getConfig.mockReturnValue({
|
|
145
|
+
scopes: { x_cadso_core: {} },
|
|
146
|
+
});
|
|
147
|
+
mockConfig.getManifest.mockResolvedValue({
|
|
148
|
+
x_cadso_core: { scope: "x_cadso_core", tables: {} },
|
|
149
|
+
});
|
|
150
|
+
// Server returns two whitelisted tables + sys_alias (not whitelisted).
|
|
151
|
+
mockClient.getManifest.mockResolvedValue({
|
|
152
|
+
scope: "x_cadso_core",
|
|
153
|
+
tables: {
|
|
154
|
+
sys_script_include: { records: { FooInclude: { name: "FooInclude", sys_id: "a1", files: [] } } },
|
|
155
|
+
sys_script: { records: { BarBR: { name: "BarBR", sys_id: "a2", files: [] } } },
|
|
156
|
+
sys_alias: { records: { DebrisRec: { name: "DebrisRec", sys_id: "a3", files: [] } } },
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
await AppUtils.syncManifest("x_cadso_core");
|
|
160
|
+
expect(mockClient.getManifest).toHaveBeenCalledWith("x_cadso_core", expect.any(Object));
|
|
161
|
+
expect(mockFUtils.writeScopeManifest).toHaveBeenCalledTimes(1);
|
|
162
|
+
var writtenScope = mockFUtils.writeScopeManifest.mock.calls[0][0];
|
|
163
|
+
var writtenManifest = mockFUtils.writeScopeManifest.mock.calls[0][1];
|
|
164
|
+
expect(writtenScope).toBe("x_cadso_core");
|
|
165
|
+
var writtenTables = Object.keys(writtenManifest.tables);
|
|
166
|
+
expect(writtenTables).toContain("sys_script_include");
|
|
167
|
+
expect(writtenTables).toContain("sys_script");
|
|
168
|
+
expect(writtenTables).not.toContain("sys_alias");
|
|
169
|
+
});
|
|
170
|
+
test("no-scope call iterates only declared scopes, even when manifest has stale ones", async function () {
|
|
171
|
+
mockConfig.getConfig.mockReturnValue({
|
|
172
|
+
scopes: { x_cadso_core: {}, x_cadso_work: {} },
|
|
173
|
+
});
|
|
174
|
+
// Persisted multi-scope manifest still carries stale undeclared scopes.
|
|
175
|
+
mockConfig.getManifest.mockResolvedValue({
|
|
176
|
+
x_cadso_core: { scope: "x_cadso_core", tables: {} },
|
|
177
|
+
x_cadso_work: { scope: "x_cadso_work", tables: {} },
|
|
178
|
+
x_cadso_click: { scope: "x_cadso_click", tables: {} },
|
|
179
|
+
x_nuvo_sinc: { scope: "x_nuvo_sinc", tables: {} },
|
|
180
|
+
x_cadso_ti_agile: { scope: "x_cadso_ti_agile", tables: {} },
|
|
181
|
+
});
|
|
182
|
+
mockClient.getManifest.mockImplementation(function (scope) {
|
|
183
|
+
return Promise.resolve({ scope: scope, tables: {} });
|
|
184
|
+
});
|
|
185
|
+
await AppUtils.syncManifest();
|
|
186
|
+
var refreshedScopes = mockClient.getManifest.mock.calls.map(function (c) { return c[0]; });
|
|
187
|
+
expect(refreshedScopes.sort()).toEqual(["x_cadso_core", "x_cadso_work"]);
|
|
188
|
+
expect(refreshedScopes).not.toContain("x_cadso_click");
|
|
189
|
+
expect(refreshedScopes).not.toContain("x_nuvo_sinc");
|
|
190
|
+
expect(refreshedScopes).not.toContain("x_cadso_ti_agile");
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the enhanced error path in unwrapSNResponse.
|
|
4
|
+
*
|
|
5
|
+
* When a ServiceNow REST call throws, the catch block must:
|
|
6
|
+
* - Log a structured one-liner via logger.error including HTTP status, method, URL
|
|
7
|
+
* and (when present) the ServiceNow-shaped error message from the response body.
|
|
8
|
+
* - Dump the full error surface (status, statusText, responseData, responseHeaders,
|
|
9
|
+
* scope extracted from /getManifest/:scope URLs) via fileLogger.debug.
|
|
10
|
+
* - Re-throw the original error so upstream callers see identical behaviour.
|
|
11
|
+
*
|
|
12
|
+
* Non-Axios errors must preserve the original log shape ("Error from <instance>: <msg>")
|
|
13
|
+
* and also produce a debug dump for future triage.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
var mockLogger = {
|
|
17
|
+
info: jest.fn(),
|
|
18
|
+
debug: jest.fn(),
|
|
19
|
+
warn: jest.fn(),
|
|
20
|
+
error: jest.fn(),
|
|
21
|
+
getLogLevel: function () { return "debug"; },
|
|
22
|
+
};
|
|
23
|
+
var mockFileLogger = {
|
|
24
|
+
debug: jest.fn(),
|
|
25
|
+
info: jest.fn(),
|
|
26
|
+
warn: jest.fn(),
|
|
27
|
+
error: jest.fn(),
|
|
28
|
+
};
|
|
29
|
+
jest.mock("../Logger", function () {
|
|
30
|
+
return { logger: mockLogger };
|
|
31
|
+
});
|
|
32
|
+
jest.mock("../FileLogger", function () {
|
|
33
|
+
return { fileLogger: mockFileLogger };
|
|
34
|
+
});
|
|
35
|
+
const snClient_1 = require("../snClient");
|
|
36
|
+
function makeAxiosError(overrides) {
|
|
37
|
+
var error = new Error("Request failed with status code " + overrides.status);
|
|
38
|
+
error.isAxiosError = true;
|
|
39
|
+
error.config = {
|
|
40
|
+
method: overrides.method || "post",
|
|
41
|
+
url: overrides.url || "api/sinc/sincronia/getManifest/x_cadso_example",
|
|
42
|
+
};
|
|
43
|
+
error.response = {
|
|
44
|
+
status: overrides.status,
|
|
45
|
+
statusText: overrides.statusText || "",
|
|
46
|
+
headers: overrides.headers || { "x-test": "1" },
|
|
47
|
+
data: overrides.data,
|
|
48
|
+
};
|
|
49
|
+
return error;
|
|
50
|
+
}
|
|
51
|
+
describe("unwrapSNResponse — error handling", function () {
|
|
52
|
+
var origInstance;
|
|
53
|
+
beforeAll(function () {
|
|
54
|
+
origInstance = process.env.SN_INSTANCE;
|
|
55
|
+
process.env.SN_INSTANCE = "tenontest.service-now.com";
|
|
56
|
+
});
|
|
57
|
+
afterAll(function () {
|
|
58
|
+
if (origInstance === undefined)
|
|
59
|
+
delete process.env.SN_INSTANCE;
|
|
60
|
+
else
|
|
61
|
+
process.env.SN_INSTANCE = origInstance;
|
|
62
|
+
});
|
|
63
|
+
beforeEach(function () {
|
|
64
|
+
jest.clearAllMocks();
|
|
65
|
+
});
|
|
66
|
+
test("Axios 500 with ServiceNow-shaped body: one-liner includes status + URL + SN message, debug dump carries full detail", async function () {
|
|
67
|
+
var snBody = {
|
|
68
|
+
error: {
|
|
69
|
+
message: "org.mozilla.javascript.EcmaError: TypeError",
|
|
70
|
+
detail: "Cannot read property 'name' of null",
|
|
71
|
+
},
|
|
72
|
+
status: "failure",
|
|
73
|
+
};
|
|
74
|
+
var axErr = makeAxiosError({
|
|
75
|
+
status: 500,
|
|
76
|
+
method: "post",
|
|
77
|
+
url: "api/sinc/sincronia/getManifest/x_cadso_automate",
|
|
78
|
+
data: snBody,
|
|
79
|
+
statusText: "Internal Server Error",
|
|
80
|
+
});
|
|
81
|
+
var rejected = Promise.reject(axErr);
|
|
82
|
+
// Silence the unhandled-rejection warning before jest inspects it
|
|
83
|
+
rejected.catch(function () { });
|
|
84
|
+
await expect((0, snClient_1.unwrapSNResponse)(rejected)).rejects.toBe(axErr);
|
|
85
|
+
expect(mockLogger.error).toHaveBeenCalledTimes(1);
|
|
86
|
+
var userLine = mockLogger.error.mock.calls[0][0];
|
|
87
|
+
expect(userLine).toContain("tenontest.service-now.com");
|
|
88
|
+
expect(userLine).toContain("HTTP 500");
|
|
89
|
+
expect(userLine).toContain("POST");
|
|
90
|
+
expect(userLine).toContain("getManifest/x_cadso_automate");
|
|
91
|
+
expect(userLine).toContain("org.mozilla.javascript.EcmaError");
|
|
92
|
+
expect(mockFileLogger.debug).toHaveBeenCalledTimes(1);
|
|
93
|
+
var debugLabel = mockFileLogger.debug.mock.calls[0][0];
|
|
94
|
+
var debugPayload = mockFileLogger.debug.mock.calls[0][1];
|
|
95
|
+
expect(debugLabel).toBe("REST error detail");
|
|
96
|
+
expect(debugPayload).toMatchObject({
|
|
97
|
+
instance: "tenontest.service-now.com",
|
|
98
|
+
scope: "x_cadso_automate",
|
|
99
|
+
method: "POST",
|
|
100
|
+
status: 500,
|
|
101
|
+
statusText: "Internal Server Error",
|
|
102
|
+
responseData: snBody,
|
|
103
|
+
responseHeaders: { "x-test": "1" },
|
|
104
|
+
});
|
|
105
|
+
expect(debugPayload.url).toContain("getManifest/x_cadso_automate");
|
|
106
|
+
});
|
|
107
|
+
test("Axios 500 with non-SN body: one-liner falls back cleanly, no crash", async function () {
|
|
108
|
+
var axErr = makeAxiosError({
|
|
109
|
+
status: 500,
|
|
110
|
+
method: "get",
|
|
111
|
+
url: "api/now/table/sys_script_include",
|
|
112
|
+
data: "<html><body>Gateway error</body></html>",
|
|
113
|
+
});
|
|
114
|
+
var rejected = Promise.reject(axErr);
|
|
115
|
+
rejected.catch(function () { });
|
|
116
|
+
await expect((0, snClient_1.unwrapSNResponse)(rejected)).rejects.toBe(axErr);
|
|
117
|
+
var userLine = mockLogger.error.mock.calls[0][0];
|
|
118
|
+
expect(userLine).toContain("HTTP 500");
|
|
119
|
+
expect(userLine).toContain("GET");
|
|
120
|
+
expect(userLine).toContain("table/sys_script_include");
|
|
121
|
+
// No SN message to append; no em dash
|
|
122
|
+
expect(userLine).not.toContain(" — ");
|
|
123
|
+
expect(mockFileLogger.debug).toHaveBeenCalledTimes(1);
|
|
124
|
+
var debugPayload = mockFileLogger.debug.mock.calls[0][1];
|
|
125
|
+
// Non-manifest URL — scope should be undefined
|
|
126
|
+
expect(debugPayload.scope).toBeUndefined();
|
|
127
|
+
expect(debugPayload.responseData).toBe("<html><body>Gateway error</body></html>");
|
|
128
|
+
});
|
|
129
|
+
test("Non-Axios error: preserves legacy log shape and re-throws", async function () {
|
|
130
|
+
var nonAxios = new Error("socket hang up");
|
|
131
|
+
var rejected = Promise.reject(nonAxios);
|
|
132
|
+
rejected.catch(function () { });
|
|
133
|
+
await expect((0, snClient_1.unwrapSNResponse)(rejected)).rejects.toBe(nonAxios);
|
|
134
|
+
expect(mockLogger.error).toHaveBeenCalledTimes(1);
|
|
135
|
+
var userLine = mockLogger.error.mock.calls[0][0];
|
|
136
|
+
expect(userLine).toBe("Error from tenontest.service-now.com: socket hang up");
|
|
137
|
+
expect(mockFileLogger.debug).toHaveBeenCalledTimes(1);
|
|
138
|
+
var debugLabel = mockFileLogger.debug.mock.calls[0][0];
|
|
139
|
+
expect(debugLabel).toBe("Non-Axios error detail");
|
|
140
|
+
var debugPayload = mockFileLogger.debug.mock.calls[0][1];
|
|
141
|
+
expect(debugPayload.message).toBe("socket hang up");
|
|
142
|
+
expect(debugPayload.errorName).toBe("Error");
|
|
143
|
+
expect(typeof debugPayload.errorStack).toBe("string");
|
|
144
|
+
});
|
|
145
|
+
});
|