@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
package/dist/snClient.js
CHANGED
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.unwrapSNResponse = exports.defaultClient = exports.snClient = exports.processPushResponse = exports.retryOnHttpErr = exports.retryOnErr = void 0;
|
|
7
|
+
exports.setBenchmarkSink = setBenchmarkSink;
|
|
7
8
|
exports.unwrapTableAPIFirstItem = unwrapTableAPIFirstItem;
|
|
8
9
|
const axios_1 = __importDefault(require("axios"));
|
|
9
10
|
const axios_cookiejar_support_1 = require("axios-cookiejar-support");
|
|
@@ -12,6 +13,11 @@ const tough_cookie_1 = require("tough-cookie");
|
|
|
12
13
|
const genericUtils_1 = require("./genericUtils");
|
|
13
14
|
const Logger_1 = require("./Logger");
|
|
14
15
|
const FileLogger_1 = require("./FileLogger");
|
|
16
|
+
// Benchmark sink. Null when --benchmark is off; zero overhead in that case.
|
|
17
|
+
let _benchmarkSink = null;
|
|
18
|
+
function setBenchmarkSink(sink) {
|
|
19
|
+
_benchmarkSink = sink;
|
|
20
|
+
}
|
|
15
21
|
// Local helper to strip _ directive keys before sending to ServiceNow API.
|
|
16
22
|
// Defined here (not imported from config.ts) to avoid circular dependencies.
|
|
17
23
|
function _stripUnderscoreKeys(obj) {
|
|
@@ -143,7 +149,7 @@ const processPushResponse = (response, recSummary) => {
|
|
|
143
149
|
exports.processPushResponse = processPushResponse;
|
|
144
150
|
const snClient = (baseURL, username, password) => {
|
|
145
151
|
const jar = new tough_cookie_1.CookieJar();
|
|
146
|
-
const
|
|
152
|
+
const rawAxios = (0, axios_cookiejar_support_1.wrapper)(axios_1.default.create({
|
|
147
153
|
withCredentials: true,
|
|
148
154
|
auth: {
|
|
149
155
|
username,
|
|
@@ -154,7 +160,55 @@ const snClient = (baseURL, username, password) => {
|
|
|
154
160
|
},
|
|
155
161
|
baseURL,
|
|
156
162
|
jar,
|
|
157
|
-
}))
|
|
163
|
+
}));
|
|
164
|
+
// Request interceptor: stamp startedAt so the response interceptor can
|
|
165
|
+
// compute durationMs. Only when a benchmark sink is attached — when it
|
|
166
|
+
// isn't, this is a single property assignment and does not change shape.
|
|
167
|
+
rawAxios.interceptors.request.use(function (cfg) {
|
|
168
|
+
if (_benchmarkSink) {
|
|
169
|
+
cfg._benchStartedAt = Date.now();
|
|
170
|
+
}
|
|
171
|
+
return cfg;
|
|
172
|
+
});
|
|
173
|
+
rawAxios.interceptors.response.use(function (response) {
|
|
174
|
+
if (!_benchmarkSink)
|
|
175
|
+
return response;
|
|
176
|
+
var startedAt = response.config && response.config._benchStartedAt;
|
|
177
|
+
if (!startedAt)
|
|
178
|
+
return response;
|
|
179
|
+
var url = (response.config && response.config.url) || "";
|
|
180
|
+
var payload = response.data;
|
|
181
|
+
var responseBytes = 0;
|
|
182
|
+
try {
|
|
183
|
+
responseBytes = payload ? JSON.stringify(payload).length : 0;
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
responseBytes = 0;
|
|
187
|
+
}
|
|
188
|
+
var tableCount = 0;
|
|
189
|
+
var result = payload && payload.result;
|
|
190
|
+
if (result && typeof result === "object") {
|
|
191
|
+
if (url.indexOf("bulkDownload") !== -1) {
|
|
192
|
+
tableCount = Object.keys(result).length;
|
|
193
|
+
}
|
|
194
|
+
else if (url.indexOf("getManifest") !== -1 && result.tables) {
|
|
195
|
+
tableCount = Object.keys(result.tables).length;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
_benchmarkSink.recordHttp({
|
|
199
|
+
path: url,
|
|
200
|
+
tableCount: tableCount,
|
|
201
|
+
durationMs: Date.now() - startedAt,
|
|
202
|
+
statusCode: response.status,
|
|
203
|
+
responseBytes: responseBytes,
|
|
204
|
+
});
|
|
205
|
+
return response;
|
|
206
|
+
}, function (error) {
|
|
207
|
+
// Preserve original rejection; don't record errored requests (they skew
|
|
208
|
+
// latency tails without adding signal for the "scales the same" question).
|
|
209
|
+
return Promise.reject(error);
|
|
210
|
+
});
|
|
211
|
+
const client = (0, axios_rate_limit_1.default)(rawAxios, { maxRPS: 20 });
|
|
158
212
|
const getAppList = () => {
|
|
159
213
|
const endpoint = "api/sinc/sincronia/getAppList";
|
|
160
214
|
return client.get(endpoint);
|
|
@@ -369,13 +423,56 @@ const unwrapSNResponse = async (clientPromise) => {
|
|
|
369
423
|
return resp.data.result;
|
|
370
424
|
}
|
|
371
425
|
catch (e) {
|
|
372
|
-
let message;
|
|
373
|
-
if (e instanceof Error)
|
|
374
|
-
message = e.message;
|
|
375
|
-
else
|
|
376
|
-
message = String(e);
|
|
377
426
|
const instance = process.env.SN_INSTANCE || "unknown";
|
|
378
|
-
|
|
427
|
+
if (axios_1.default.isAxiosError(e) && e.response) {
|
|
428
|
+
const status = e.response.status;
|
|
429
|
+
const statusText = e.response.statusText || "";
|
|
430
|
+
const method = ((e.config && e.config.method) || "").toUpperCase();
|
|
431
|
+
const url = (e.config && e.config.url) || "";
|
|
432
|
+
const data = e.response.data;
|
|
433
|
+
// Extract ServiceNow-shaped error message if present (`{error: {message, detail}, status}`)
|
|
434
|
+
let snMessage = "";
|
|
435
|
+
if (data && typeof data === "object") {
|
|
436
|
+
if (data.error && typeof data.error === "object" && data.error.message) {
|
|
437
|
+
snMessage = " — " + String(data.error.message);
|
|
438
|
+
}
|
|
439
|
+
else if (typeof data.message === "string") {
|
|
440
|
+
snMessage = " — " + data.message;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Pull scope out of /getManifest/:scope URLs for easy grepping
|
|
444
|
+
let scope;
|
|
445
|
+
const manifestMatch = url.match(/\/getManifest\/([^/?]+)/);
|
|
446
|
+
if (manifestMatch)
|
|
447
|
+
scope = manifestMatch[1];
|
|
448
|
+
Logger_1.logger.error("Error from " + instance + ": HTTP " + status + " " +
|
|
449
|
+
method + " " + url + snMessage);
|
|
450
|
+
FileLogger_1.fileLogger.debug("REST error detail", {
|
|
451
|
+
instance: instance,
|
|
452
|
+
scope: scope,
|
|
453
|
+
method: method,
|
|
454
|
+
url: url,
|
|
455
|
+
status: status,
|
|
456
|
+
statusText: statusText,
|
|
457
|
+
responseData: data,
|
|
458
|
+
responseHeaders: e.response.headers,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
// Non-Axios error: preserve today's behaviour, add debug detail
|
|
463
|
+
let message;
|
|
464
|
+
if (e instanceof Error)
|
|
465
|
+
message = e.message;
|
|
466
|
+
else
|
|
467
|
+
message = String(e);
|
|
468
|
+
Logger_1.logger.error("Error from " + instance + ": " + message);
|
|
469
|
+
FileLogger_1.fileLogger.debug("Non-Axios error detail", {
|
|
470
|
+
instance: instance,
|
|
471
|
+
errorName: e instanceof Error ? e.name : undefined,
|
|
472
|
+
errorStack: e instanceof Error ? e.stack : undefined,
|
|
473
|
+
message: message,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
379
476
|
throw e;
|
|
380
477
|
}
|
|
381
478
|
};
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the --benchmark flag on `sinc refresh`.
|
|
4
|
+
*
|
|
5
|
+
* Two layers:
|
|
6
|
+
* 1. BenchmarkCollector in isolation — covers p50/p95/max math and the
|
|
7
|
+
* formatSummary string the CLI prints.
|
|
8
|
+
* 2. refreshAllFiles wired to a collector — asserts startScope/endScope
|
|
9
|
+
* bookends the scope and that filesWritten/filesUnchanged match what
|
|
10
|
+
* refresh actually did.
|
|
11
|
+
*
|
|
12
|
+
* The axios interceptor hook (snClient.setBenchmarkSink) is exercised by the
|
|
13
|
+
* real-run integration against workstudio — not here. This suite covers the
|
|
14
|
+
* collector contract and the appUtils lifecycle.
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const os = __importStar(require("os"));
|
|
52
|
+
const path = __importStar(require("path"));
|
|
53
|
+
const benchmark_1 = require("../benchmark");
|
|
54
|
+
// ---------- mocks for refreshAllFiles test (must come before importing) ----
|
|
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
|
+
setBenchmarkSink: jest.fn(),
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
var mockConfig = {
|
|
87
|
+
getConfig: jest.fn().mockReturnValue({ scopes: { x_cadso_core: {} }, tableOptions: {} }),
|
|
88
|
+
getManifest: jest.fn().mockResolvedValue({
|
|
89
|
+
x_cadso_core: { scope: "x_cadso_core", tables: {} },
|
|
90
|
+
}),
|
|
91
|
+
getSourcePathForScope: jest.fn(),
|
|
92
|
+
getSourcePath: jest.fn(),
|
|
93
|
+
getManifestPath: jest.fn().mockReturnValue("/tmp/sinc.manifest.json"),
|
|
94
|
+
resolveConfigForScope: jest.fn().mockImplementation(function () {
|
|
95
|
+
return { tables: ["sys_script_include"], fieldOverrides: {}, apiIncludes: {}, apiExcludes: {} };
|
|
96
|
+
}),
|
|
97
|
+
isMultiScopeManifest: jest.fn().mockReturnValue(true),
|
|
98
|
+
updateManifest: jest.fn(),
|
|
99
|
+
};
|
|
100
|
+
jest.mock("../config", function () { return mockConfig; });
|
|
101
|
+
jest.mock("progress", function () {
|
|
102
|
+
return jest.fn().mockImplementation(function () { return { tick: jest.fn() }; });
|
|
103
|
+
});
|
|
104
|
+
jest.mock("../FileUtils", function () {
|
|
105
|
+
var actual = jest.requireActual("../FileUtils");
|
|
106
|
+
return Object.assign({}, actual, {
|
|
107
|
+
writeScopeManifest: jest.fn().mockResolvedValue(undefined),
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
const AppUtils = __importStar(require("../appUtils"));
|
|
111
|
+
// ---------- collector unit tests ----------
|
|
112
|
+
describe("BenchmarkCollector", function () {
|
|
113
|
+
test("formatSummary reports empty state when nothing recorded", function () {
|
|
114
|
+
var collector = new benchmark_1.BenchmarkCollector();
|
|
115
|
+
var summary = collector.formatSummary();
|
|
116
|
+
expect(summary).toContain("Refresh Benchmark");
|
|
117
|
+
expect(summary).toContain("(no samples recorded)");
|
|
118
|
+
});
|
|
119
|
+
test("percentiles reflect the recorded latencies", function () {
|
|
120
|
+
var collector = new benchmark_1.BenchmarkCollector();
|
|
121
|
+
// 10, 20, 30, ..., 100 — p50 at index 5 = 60, p95 at index 9 = 100.
|
|
122
|
+
for (var i = 1; i <= 10; i++) {
|
|
123
|
+
collector.recordHttp({
|
|
124
|
+
path: "/api/test",
|
|
125
|
+
tableCount: 1,
|
|
126
|
+
durationMs: i * 10,
|
|
127
|
+
statusCode: 200,
|
|
128
|
+
responseBytes: 100,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
var summary = collector.formatSummary();
|
|
132
|
+
expect(summary).toContain("p50 60ms");
|
|
133
|
+
expect(summary).toContain("p95 100ms");
|
|
134
|
+
expect(summary).toContain("max 100ms");
|
|
135
|
+
expect(summary).toContain("10 HTTP requests");
|
|
136
|
+
});
|
|
137
|
+
test("scope samples record wall time, request counts, and file counts", function () {
|
|
138
|
+
var collector = new benchmark_1.BenchmarkCollector();
|
|
139
|
+
collector.startScope("x_cadso_core");
|
|
140
|
+
collector.recordHttp({
|
|
141
|
+
path: "/api/bulkDownload",
|
|
142
|
+
tableCount: 3,
|
|
143
|
+
durationMs: 50,
|
|
144
|
+
statusCode: 200,
|
|
145
|
+
responseBytes: 2048,
|
|
146
|
+
});
|
|
147
|
+
collector.recordHttp({
|
|
148
|
+
path: "/api/bulkDownload",
|
|
149
|
+
tableCount: 3,
|
|
150
|
+
durationMs: 80,
|
|
151
|
+
statusCode: 200,
|
|
152
|
+
responseBytes: 4096,
|
|
153
|
+
});
|
|
154
|
+
collector.endScope(7, 42);
|
|
155
|
+
var scopes = collector.getScopeSamples();
|
|
156
|
+
expect(scopes).toHaveLength(1);
|
|
157
|
+
expect(scopes[0].scopeName).toBe("x_cadso_core");
|
|
158
|
+
expect(scopes[0].httpRequests).toBe(2);
|
|
159
|
+
expect(scopes[0].totalResponseBytes).toBe(2048 + 4096);
|
|
160
|
+
expect(scopes[0].filesWritten).toBe(7);
|
|
161
|
+
expect(scopes[0].filesUnchanged).toBe(42);
|
|
162
|
+
expect(scopes[0].wallTimeMs).toBeGreaterThanOrEqual(0);
|
|
163
|
+
var summary = collector.formatSummary();
|
|
164
|
+
expect(summary).toContain("x_cadso_core:");
|
|
165
|
+
expect(summary).toContain("7 written / 42 unchanged");
|
|
166
|
+
});
|
|
167
|
+
test("formatBytes switches units at KB and MB thresholds", function () {
|
|
168
|
+
var collector = new benchmark_1.BenchmarkCollector();
|
|
169
|
+
collector.recordHttp({ path: "/a", tableCount: 1, durationMs: 1, statusCode: 200, responseBytes: 500 });
|
|
170
|
+
expect(collector.formatSummary()).toContain("500B received");
|
|
171
|
+
var collector2 = new benchmark_1.BenchmarkCollector();
|
|
172
|
+
collector2.recordHttp({ path: "/a", tableCount: 1, durationMs: 1, statusCode: 200, responseBytes: 2048 });
|
|
173
|
+
expect(collector2.formatSummary()).toContain("2.0KB received");
|
|
174
|
+
var collector3 = new benchmark_1.BenchmarkCollector();
|
|
175
|
+
collector3.recordHttp({ path: "/a", tableCount: 1, durationMs: 1, statusCode: 200, responseBytes: 2 * 1024 * 1024 });
|
|
176
|
+
expect(collector3.formatSummary()).toContain("2.00MB received");
|
|
177
|
+
});
|
|
178
|
+
test("endScope is a no-op when no scope is active", function () {
|
|
179
|
+
var collector = new benchmark_1.BenchmarkCollector();
|
|
180
|
+
collector.endScope(5, 5);
|
|
181
|
+
expect(collector.getScopeSamples()).toHaveLength(0);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
// ---------- refreshAllFiles integration with collector ----------
|
|
185
|
+
describe("refreshAllFiles — benchmarkCollector lifecycle", function () {
|
|
186
|
+
var tmpRoot;
|
|
187
|
+
beforeEach(function () {
|
|
188
|
+
jest.clearAllMocks();
|
|
189
|
+
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sinc-bench-test-"));
|
|
190
|
+
mockConfig.getSourcePathForScope.mockReturnValue(tmpRoot);
|
|
191
|
+
mockConfig.getSourcePath.mockReturnValue(tmpRoot);
|
|
192
|
+
});
|
|
193
|
+
afterEach(function () {
|
|
194
|
+
try {
|
|
195
|
+
fs.rmSync(tmpRoot, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
catch (e) { }
|
|
198
|
+
});
|
|
199
|
+
test("endScope captures filesWritten and filesUnchanged for a refresh pass", async function () {
|
|
200
|
+
// "Stale" file — content diverges from what the mock returns → will be written.
|
|
201
|
+
fs.mkdirSync(path.join(tmpRoot, "sys_script_include", "StaleRec"), { recursive: true });
|
|
202
|
+
fs.writeFileSync(path.join(tmpRoot, "sys_script_include", "StaleRec", "script.js"), "var x = 1;");
|
|
203
|
+
// "Matching" file — content matches → counted as unchanged.
|
|
204
|
+
fs.mkdirSync(path.join(tmpRoot, "sys_script_include", "SameRec"), { recursive: true });
|
|
205
|
+
var matching = "var same = true;";
|
|
206
|
+
fs.writeFileSync(path.join(tmpRoot, "sys_script_include", "SameRec", "script.js"), matching);
|
|
207
|
+
var manifest = {
|
|
208
|
+
scope: "x_cadso_core",
|
|
209
|
+
tables: {
|
|
210
|
+
sys_script_include: {
|
|
211
|
+
records: {
|
|
212
|
+
StaleRec: { name: "StaleRec", sys_id: "sysid_Stale", files: [{ name: "script", type: "js" }] },
|
|
213
|
+
SameRec: { name: "SameRec", sys_id: "sysid_Same", files: [{ name: "script", type: "js" }] },
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
mockClient.getManifest.mockResolvedValue(manifest);
|
|
219
|
+
mockClient.getMissingFiles.mockResolvedValue({
|
|
220
|
+
sys_script_include: {
|
|
221
|
+
records: {
|
|
222
|
+
StaleRec: {
|
|
223
|
+
name: "StaleRec", sys_id: "sysid_Stale",
|
|
224
|
+
files: [{ name: "script", type: "js", content: "var x = 99; // new from instance" }],
|
|
225
|
+
},
|
|
226
|
+
SameRec: {
|
|
227
|
+
name: "SameRec", sys_id: "sysid_Same",
|
|
228
|
+
files: [{ name: "script", type: "js", content: matching }],
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
var collector = new benchmark_1.BenchmarkCollector();
|
|
234
|
+
collector.startScope("x_cadso_core");
|
|
235
|
+
await AppUtils.refreshAllFiles(manifest, tmpRoot, {
|
|
236
|
+
benchmarkCollector: collector,
|
|
237
|
+
});
|
|
238
|
+
var scopes = collector.getScopeSamples();
|
|
239
|
+
expect(scopes).toHaveLength(1);
|
|
240
|
+
expect(scopes[0].scopeName).toBe("x_cadso_core");
|
|
241
|
+
// One file had divergent content → written. One matched → unchanged.
|
|
242
|
+
expect(scopes[0].filesWritten).toBe(1);
|
|
243
|
+
expect(scopes[0].filesUnchanged).toBe(1);
|
|
244
|
+
});
|
|
245
|
+
test("endScope(0, 0) on error path so the collector still closes cleanly", async function () {
|
|
246
|
+
mockClient.getMissingFiles.mockRejectedValue(new Error("boom"));
|
|
247
|
+
var manifest = {
|
|
248
|
+
scope: "x_cadso_core",
|
|
249
|
+
tables: {
|
|
250
|
+
sys_script_include: {
|
|
251
|
+
records: {
|
|
252
|
+
Rec: { name: "Rec", sys_id: "sysid_Rec", files: [{ name: "script", type: "js" }] },
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
var collector = new benchmark_1.BenchmarkCollector();
|
|
258
|
+
collector.startScope("x_cadso_core");
|
|
259
|
+
await expect(AppUtils.refreshAllFiles(manifest, tmpRoot, { benchmarkCollector: collector })).rejects.toThrow("boom");
|
|
260
|
+
var scopes = collector.getScopeSamples();
|
|
261
|
+
expect(scopes).toHaveLength(1);
|
|
262
|
+
expect(scopes[0].filesWritten).toBe(0);
|
|
263
|
+
expect(scopes[0].filesUnchanged).toBe(0);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -86,6 +86,7 @@ var mockSNClient = {
|
|
|
86
86
|
updateCurrentAppUserPref: jest.fn(),
|
|
87
87
|
createCurrentAppUserPref: jest.fn(),
|
|
88
88
|
getCurrentUpdateSetUserPref: jest.fn(),
|
|
89
|
+
changeScope: jest.fn().mockResolvedValue(undefined),
|
|
89
90
|
};
|
|
90
91
|
jest.mock("../snClient", function () {
|
|
91
92
|
return {
|
|
@@ -267,9 +268,9 @@ describe("US-014: Global debounce for serialized scope processing", function ()
|
|
|
267
268
|
watcherB.pushQueue.push(ctxB.filePath);
|
|
268
269
|
// Track the order of scope switches
|
|
269
270
|
var switchOrder = [];
|
|
270
|
-
mockSNClient.
|
|
271
|
+
mockSNClient.changeScope.mockImplementation(function (scope) {
|
|
271
272
|
switchOrder.push(scope);
|
|
272
|
-
return Promise.resolve(
|
|
273
|
+
return Promise.resolve(undefined);
|
|
273
274
|
});
|
|
274
275
|
// Fire the global debounce
|
|
275
276
|
await capturedDebounceFns[0]();
|
|
@@ -83,6 +83,7 @@ const mockSNClient = {
|
|
|
83
83
|
updateCurrentAppUserPref: jest.fn(),
|
|
84
84
|
createCurrentAppUserPref: jest.fn(),
|
|
85
85
|
getCurrentUpdateSetUserPref: jest.fn(),
|
|
86
|
+
changeScope: jest.fn().mockResolvedValue(undefined),
|
|
86
87
|
};
|
|
87
88
|
jest.mock("../snClient", () => ({
|
|
88
89
|
defaultClient: jest.fn(() => mockSNClient),
|
|
@@ -415,28 +416,17 @@ describe("MultiScopeWatcherManager", () => {
|
|
|
415
416
|
});
|
|
416
417
|
});
|
|
417
418
|
describe("switchToScope", () => {
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
419
|
+
// switchToScope now uses the Claude REST API changeScope endpoint
|
|
420
|
+
// (gs.setCurrentApplicationId) to switch the active REST session scope.
|
|
421
|
+
// The previous user-preference approach only wrote a DB record and did
|
|
422
|
+
// not affect the session, causing updateRecord() to operate in the wrong scope.
|
|
423
|
+
it("invokes client.changeScope with the target scope", async () => {
|
|
421
424
|
await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_test_core");
|
|
422
|
-
expect(mockSNClient.
|
|
423
|
-
expect(mockSNClient.getUserSysId).toHaveBeenCalled();
|
|
424
|
-
expect(mockSNClient.updateCurrentAppUserPref).toHaveBeenCalledWith("scope_sys_id", "existing_pref");
|
|
425
|
-
expect(mockSNClient.createCurrentAppUserPref).not.toHaveBeenCalled();
|
|
425
|
+
expect(mockSNClient.changeScope).toHaveBeenCalledWith("x_test_core");
|
|
426
426
|
});
|
|
427
|
-
it("
|
|
428
|
-
mockSNClient.
|
|
429
|
-
await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_test_core");
|
|
430
|
-
expect(mockSNClient.createCurrentAppUserPref).toHaveBeenCalledWith("scope_sys_id", "user_sys_id");
|
|
431
|
-
expect(mockSNClient.updateCurrentAppUserPref).not.toHaveBeenCalled();
|
|
432
|
-
});
|
|
433
|
-
it("throws when scope not found", async () => {
|
|
434
|
-
mockSNClient.getScopeId.mockResolvedValue([]);
|
|
435
|
-
await expect(MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_missing")).rejects.toThrow("Scope x_missing not found");
|
|
436
|
-
});
|
|
437
|
-
it("throws when user sys_id cannot be retrieved", async () => {
|
|
438
|
-
mockSNClient.getUserSysId.mockResolvedValue([]);
|
|
439
|
-
await expect(MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_test_core")).rejects.toThrow("Could not get user sys_id");
|
|
427
|
+
it("propagates errors from changeScope and invalidates cached scope", async () => {
|
|
428
|
+
mockSNClient.changeScope.mockRejectedValueOnce(new Error("changeScope failed"));
|
|
429
|
+
await expect(MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_test_core")).rejects.toThrow("changeScope failed");
|
|
440
430
|
});
|
|
441
431
|
});
|
|
442
432
|
describe("loadScopeManifest", () => {
|
|
@@ -26,6 +26,10 @@ jest.mock("../snClient", function () {
|
|
|
26
26
|
createCurrentAppUserPref: function (scopeSysId, userSysId) {
|
|
27
27
|
apiCalls.push("createCurrentAppUserPref");
|
|
28
28
|
return Promise.resolve({});
|
|
29
|
+
},
|
|
30
|
+
changeScope: function (scope) {
|
|
31
|
+
apiCalls.push("changeScope:" + scope);
|
|
32
|
+
return Promise.resolve(undefined);
|
|
29
33
|
}
|
|
30
34
|
};
|
|
31
35
|
},
|
|
@@ -56,16 +60,13 @@ jest.mock("../config", function () {
|
|
|
56
60
|
describe("Scope Caching (US-013)", function () {
|
|
57
61
|
beforeEach(function () {
|
|
58
62
|
apiCalls = [];
|
|
59
|
-
// Reset cached state
|
|
63
|
+
// Reset cached state. switchToScope now uses the Claude REST API
|
|
64
|
+
// changeScope endpoint; the cache avoids redundant session switches.
|
|
60
65
|
MultiScopeWatcher_1.multiScopeWatcher.cachedScope = null;
|
|
61
|
-
MultiScopeWatcher_1.multiScopeWatcher.cachedUserSysId = null;
|
|
62
66
|
});
|
|
63
|
-
it("should
|
|
67
|
+
it("should call changeScope on first switch to a scope", async function () {
|
|
64
68
|
await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
|
|
65
|
-
expect(apiCalls).toContain("
|
|
66
|
-
expect(apiCalls).toContain("getUserSysId");
|
|
67
|
-
expect(apiCalls).toContain("getCurrentAppUserPrefSysId");
|
|
68
|
-
expect(apiCalls).toContain("updateCurrentAppUserPref");
|
|
69
|
+
expect(apiCalls).toContain("changeScope:x_cadso_core");
|
|
69
70
|
});
|
|
70
71
|
it("should skip API calls when switching to the same scope (cache hit)", async function () {
|
|
71
72
|
await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
|
|
@@ -73,21 +74,11 @@ describe("Scope Caching (US-013)", function () {
|
|
|
73
74
|
await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
|
|
74
75
|
expect(apiCalls).toEqual([]);
|
|
75
76
|
});
|
|
76
|
-
it("should
|
|
77
|
-
await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
|
|
78
|
-
apiCalls = [];
|
|
79
|
-
await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_automate");
|
|
80
|
-
expect(apiCalls).toContain("getScopeId:x_cadso_automate");
|
|
81
|
-
expect(apiCalls).toContain("getCurrentAppUserPrefSysId");
|
|
82
|
-
expect(apiCalls).toContain("updateCurrentAppUserPref");
|
|
83
|
-
});
|
|
84
|
-
it("should cache getUserSysId and call it at most once across multiple scope switches", async function () {
|
|
77
|
+
it("should call changeScope when switching to a different scope (cache miss)", async function () {
|
|
85
78
|
await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
|
|
86
79
|
apiCalls = [];
|
|
87
80
|
await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_automate");
|
|
88
|
-
|
|
89
|
-
expect(userCalls).toHaveLength(0);
|
|
90
|
-
expect(MultiScopeWatcher_1.multiScopeWatcher.cachedUserSysId).toBe("user_abc123");
|
|
81
|
+
expect(apiCalls).toContain("changeScope:x_cadso_automate");
|
|
91
82
|
});
|
|
92
83
|
it("should update cachedScope after a successful switch", async function () {
|
|
93
84
|
expect(MultiScopeWatcher_1.multiScopeWatcher.cachedScope).toBeNull();
|
|
@@ -100,14 +91,14 @@ describe("Scope Caching (US-013)", function () {
|
|
|
100
91
|
// First successful switch
|
|
101
92
|
await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
|
|
102
93
|
expect(MultiScopeWatcher_1.multiScopeWatcher.cachedScope).toBe("x_cadso_core");
|
|
103
|
-
//
|
|
94
|
+
// Swap in a client whose changeScope rejects
|
|
104
95
|
var snClient = require("../snClient");
|
|
105
96
|
var origDefault = snClient.defaultClient;
|
|
106
97
|
snClient.defaultClient = function () {
|
|
107
98
|
var client = origDefault();
|
|
108
|
-
client.
|
|
109
|
-
apiCalls.push("
|
|
110
|
-
return Promise.
|
|
99
|
+
client.changeScope = function (scope) {
|
|
100
|
+
apiCalls.push("changeScope:" + scope + ":failed");
|
|
101
|
+
return Promise.reject(new Error("changeScope failed"));
|
|
111
102
|
};
|
|
112
103
|
return client;
|
|
113
104
|
};
|