@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.
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ function createMockWatcher() {
40
+ var handlers = {};
41
+ var mock = {
42
+ _handlers: handlers,
43
+ on: jest.fn(function (event, handler) {
44
+ if (!handlers[event])
45
+ handlers[event] = [];
46
+ handlers[event].push(handler);
47
+ return mock;
48
+ }),
49
+ close: jest.fn(),
50
+ _emit: function (event) {
51
+ var args = Array.prototype.slice.call(arguments, 1);
52
+ (handlers[event] || []).forEach(function (h) { h.apply(null, args); });
53
+ },
54
+ };
55
+ return mock;
56
+ }
57
+ var mockWatchers = [];
58
+ jest.mock("chokidar", function () {
59
+ return {
60
+ watch: jest.fn(function () {
61
+ var w = createMockWatcher();
62
+ mockWatchers.push(w);
63
+ return w;
64
+ }),
65
+ };
66
+ });
67
+ jest.mock("lodash", function () {
68
+ var actual = jest.requireActual("lodash");
69
+ return {
70
+ ...actual,
71
+ debounce: jest.fn(function (fn) {
72
+ var wrapper = jest.fn();
73
+ wrapper.cancel = jest.fn();
74
+ wrapper.flush = jest.fn(function () { return fn(); });
75
+ return wrapper;
76
+ }),
77
+ };
78
+ });
79
+ var mockSNClient = {
80
+ getScopeId: jest.fn(),
81
+ getUserSysId: jest.fn(),
82
+ getCurrentAppUserPrefSysId: jest.fn(),
83
+ updateCurrentAppUserPref: jest.fn(),
84
+ createCurrentAppUserPref: jest.fn(),
85
+ getCurrentUpdateSetUserPref: jest.fn(),
86
+ };
87
+ jest.mock("../snClient", function () {
88
+ return {
89
+ defaultClient: jest.fn(function () { return mockSNClient; }),
90
+ unwrapSNResponse: jest.fn(function (val) { return val; }),
91
+ };
92
+ });
93
+ jest.mock("../FileUtils", function () {
94
+ return {
95
+ getFileContextFromPath: jest.fn(),
96
+ getFileContextWithSkipReason: jest.fn(),
97
+ };
98
+ });
99
+ jest.mock("../appUtils", function () {
100
+ return {
101
+ groupAppFiles: jest.fn(),
102
+ pushFiles: jest.fn(),
103
+ };
104
+ });
105
+ jest.mock("../logMessages", function () {
106
+ return { logFilePush: jest.fn() };
107
+ });
108
+ jest.mock("../recentEdits", function () {
109
+ return { writeRecentEdit: jest.fn() };
110
+ });
111
+ jest.mock("../Logger", function () {
112
+ return {
113
+ logger: {
114
+ info: jest.fn(),
115
+ error: jest.fn(),
116
+ warn: jest.fn(),
117
+ debug: jest.fn(),
118
+ success: jest.fn(),
119
+ getLogLevel: jest.fn().mockReturnValue("info"),
120
+ },
121
+ };
122
+ });
123
+ jest.mock("../config", function () {
124
+ return {
125
+ loadConfigs: jest.fn().mockResolvedValue(undefined),
126
+ getConfig: jest.fn(),
127
+ getRootDir: jest.fn().mockReturnValue("/project"),
128
+ updateManifest: jest.fn(),
129
+ getManifest: jest.fn(),
130
+ getSourcePath: jest.fn().mockReturnValue("/project/src"),
131
+ getScopeManifestPath: jest.fn(function (scope) { return "/project/sinc.manifest." + scope + ".json"; }),
132
+ getManifestPath: jest.fn().mockReturnValue("/project/sinc.manifest.json"),
133
+ };
134
+ });
135
+ jest.mock("fs", function () {
136
+ return {
137
+ existsSync: jest.fn(),
138
+ readFileSync: jest.fn(),
139
+ writeFileSync: jest.fn(),
140
+ promises: {
141
+ readFile: jest.fn(),
142
+ writeFile: jest.fn(),
143
+ readdir: jest.fn(),
144
+ mkdir: jest.fn(),
145
+ access: jest.fn(),
146
+ stat: jest.fn(),
147
+ },
148
+ };
149
+ });
150
+ jest.mock("axios", function () {
151
+ return {
152
+ default: {
153
+ create: jest.fn(function () {
154
+ return { get: jest.fn().mockResolvedValue({ data: { result: null } }) };
155
+ }),
156
+ },
157
+ };
158
+ });
159
+ // --- Imports ---
160
+ const ConfigManager = __importStar(require("../config"));
161
+ const Logger_1 = require("../Logger");
162
+ const MultiScopeWatcher_1 = require("../MultiScopeWatcher");
163
+ const fs_1 = __importDefault(require("fs"));
164
+ var MOCK_CONFIG = {
165
+ sourceDirectory: "src",
166
+ buildDirectory: "build",
167
+ rules: [],
168
+ includes: {},
169
+ excludes: {},
170
+ tableOptions: {},
171
+ refreshInterval: 30,
172
+ scopes: {
173
+ x_test_core: { sourceDirectory: "src/x_test_core" },
174
+ x_test_work: {},
175
+ },
176
+ };
177
+ describe("US-009: Rate limit coordination", function () {
178
+ beforeEach(function () {
179
+ jest.clearAllMocks();
180
+ mockWatchers.length = 0;
181
+ (0, MultiScopeWatcher_1.stopMultiScopeWatching)();
182
+ mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
183
+ mockSNClient.getUserSysId.mockResolvedValue([{ sys_id: "user_sys_id" }]);
184
+ mockSNClient.getCurrentAppUserPrefSysId.mockResolvedValue([{ sys_id: "pref_sys_id" }]);
185
+ mockSNClient.updateCurrentAppUserPref.mockResolvedValue({});
186
+ mockSNClient.getCurrentUpdateSetUserPref.mockResolvedValue([]);
187
+ });
188
+ afterEach(function () {
189
+ (0, MultiScopeWatcher_1.stopMultiScopeWatching)();
190
+ });
191
+ describe("configurable monitoring interval", function () {
192
+ it("uses the provided monitorIntervalMs", async function () {
193
+ var setIntervalSpy = jest.spyOn(global, "setInterval");
194
+ ConfigManager.getConfig.mockReturnValue({
195
+ ...MOCK_CONFIG,
196
+ scopes: { x_test: {} },
197
+ });
198
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)({ monitorIntervalMs: 60000 });
199
+ await new Promise(function (r) { setTimeout(r, 50); });
200
+ expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 60000);
201
+ setIntervalSpy.mockRestore();
202
+ });
203
+ it("defaults to 120s when no options provided", async function () {
204
+ var setIntervalSpy = jest.spyOn(global, "setInterval");
205
+ ConfigManager.getConfig.mockReturnValue({
206
+ ...MOCK_CONFIG,
207
+ scopes: { x_test: {} },
208
+ });
209
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
210
+ await new Promise(function (r) { setTimeout(r, 50); });
211
+ expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 120000);
212
+ setIntervalSpy.mockRestore();
213
+ });
214
+ it("disables monitoring when monitorIntervalMs is 0", async function () {
215
+ var setIntervalSpy = jest.spyOn(global, "setInterval");
216
+ ConfigManager.getConfig.mockReturnValue({
217
+ ...MOCK_CONFIG,
218
+ scopes: { x_test: {} },
219
+ });
220
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)({ monitorIntervalMs: 0 });
221
+ await new Promise(function (r) { setTimeout(r, 50); });
222
+ expect(setIntervalSpy).not.toHaveBeenCalled();
223
+ expect(Logger_1.logger.info).toHaveBeenCalledWith(expect.stringContaining("monitoring disabled"));
224
+ setIntervalSpy.mockRestore();
225
+ });
226
+ });
227
+ describe("monitoring uses local config (no API calls)", function () {
228
+ it("reads update set config from local file instead of making API calls", async function () {
229
+ ConfigManager.getConfig.mockReturnValue({
230
+ ...MOCK_CONFIG,
231
+ scopes: { x_scope_a: {}, x_scope_b: {} },
232
+ });
233
+ // Simulate update set config on disk
234
+ fs_1.default.existsSync.mockReturnValue(true);
235
+ fs_1.default.readFileSync.mockReturnValue(JSON.stringify({
236
+ x_scope_a: { sys_id: "us_a", name: "Task A Update Set" },
237
+ x_scope_b: { sys_id: "us_b", name: "Task B Update Set" },
238
+ }));
239
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)({ monitorIntervalMs: 60000 });
240
+ await new Promise(function (r) { setTimeout(r, 50); });
241
+ // Monitoring should NOT call any SN API (getScopeId is only from startWatching scope switching)
242
+ // The checkAllUpdateSets method uses local config file — no getUserSysId, no getCurrentUpdateSetUserPref
243
+ expect(Logger_1.logger.info).toHaveBeenCalledWith(expect.stringContaining("Task A Update Set"));
244
+ expect(Logger_1.logger.info).toHaveBeenCalledWith(expect.stringContaining("Task B Update Set"));
245
+ });
246
+ it("warns when scope has no update set configured", async function () {
247
+ ConfigManager.getConfig.mockReturnValue({
248
+ ...MOCK_CONFIG,
249
+ scopes: { x_scope_a: {} },
250
+ });
251
+ fs_1.default.existsSync.mockReturnValue(true);
252
+ fs_1.default.readFileSync.mockReturnValue(JSON.stringify({}));
253
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)({ monitorIntervalMs: 60000 });
254
+ await new Promise(function (r) { setTimeout(r, 50); });
255
+ expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("No update set configured"));
256
+ });
257
+ it("warns when scope is on Default update set", async function () {
258
+ ConfigManager.getConfig.mockReturnValue({
259
+ ...MOCK_CONFIG,
260
+ scopes: { x_scope_a: {} },
261
+ });
262
+ fs_1.default.existsSync.mockReturnValue(true);
263
+ fs_1.default.readFileSync.mockReturnValue(JSON.stringify({
264
+ x_scope_a: { sys_id: "us_default", name: "Default" },
265
+ }));
266
+ await (0, MultiScopeWatcher_1.startMultiScopeWatching)({ monitorIntervalMs: 60000 });
267
+ await new Promise(function (r) { setTimeout(r, 50); });
268
+ expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("DEFAULT update set"));
269
+ });
270
+ });
271
+ });
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ var mockLogger = {
4
+ info: jest.fn(),
5
+ debug: jest.fn(),
6
+ warn: jest.fn(),
7
+ error: jest.fn(),
8
+ getLogLevel: function () { return "debug"; },
9
+ };
10
+ jest.mock("../Logger", function () {
11
+ return { logger: mockLogger };
12
+ });
13
+ jest.mock("../FileLogger", function () {
14
+ return { fileLogger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } };
15
+ });
16
+ // Minimal mock for genericUtils — just need wait()
17
+ jest.mock("../genericUtils", function () {
18
+ return {
19
+ wait: jest.fn().mockResolvedValue(undefined),
20
+ };
21
+ });
22
+ const snClient_1 = require("../snClient");
23
+ const genericUtils_1 = require("../genericUtils");
24
+ function makeAxiosError(status, headers) {
25
+ var error = new Error("Request failed with status " + status);
26
+ error.isAxiosError = true;
27
+ error.response = {
28
+ status: status,
29
+ headers: headers || {},
30
+ data: {},
31
+ };
32
+ return error;
33
+ }
34
+ describe("retryOnHttpErr", function () {
35
+ beforeEach(function () {
36
+ jest.clearAllMocks();
37
+ });
38
+ it("returns successfully on first try when no error", async function () {
39
+ var fn = jest.fn().mockResolvedValue({ status: 200, data: {} });
40
+ var result = await (0, snClient_1.retryOnHttpErr)(fn, "test > rec1");
41
+ expect(result).toEqual({ status: 200, data: {} });
42
+ expect(fn).toHaveBeenCalledTimes(1);
43
+ });
44
+ // 401/403: Fail immediately
45
+ it("fails immediately on 401 with credential message", async function () {
46
+ var fn = jest.fn().mockRejectedValue(makeAxiosError(401));
47
+ await expect((0, snClient_1.retryOnHttpErr)(fn, "test > rec1")).rejects.toThrow();
48
+ expect(fn).toHaveBeenCalledTimes(1);
49
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Unauthorized (401)"));
50
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("credentials"));
51
+ });
52
+ it("fails immediately on 403 with credential message", async function () {
53
+ var fn = jest.fn().mockRejectedValue(makeAxiosError(403));
54
+ await expect((0, snClient_1.retryOnHttpErr)(fn, "test > rec1")).rejects.toThrow();
55
+ expect(fn).toHaveBeenCalledTimes(1);
56
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Forbidden (403)"));
57
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("credentials"));
58
+ });
59
+ // 404: Fail immediately
60
+ it("fails immediately on 404 with record not found message", async function () {
61
+ var fn = jest.fn().mockRejectedValue(makeAxiosError(404));
62
+ await expect((0, snClient_1.retryOnHttpErr)(fn, "test > rec1")).rejects.toThrow();
63
+ expect(fn).toHaveBeenCalledTimes(1);
64
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("Record not found (404)"));
65
+ });
66
+ // 429: Rate limited — honor Retry-After
67
+ it("retries on 429 and honors Retry-After header", async function () {
68
+ var fn = jest.fn()
69
+ .mockRejectedValueOnce(makeAxiosError(429, { "retry-after": "5" }))
70
+ .mockResolvedValue({ status: 200, data: {} });
71
+ var result = await (0, snClient_1.retryOnHttpErr)(fn, "test > rec1");
72
+ expect(result).toEqual({ status: 200, data: {} });
73
+ expect(fn).toHaveBeenCalledTimes(2);
74
+ expect(genericUtils_1.wait).toHaveBeenCalledWith(5000); // 5 seconds from Retry-After
75
+ expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining("Rate limited (429)"));
76
+ });
77
+ it("uses default 10s wait on 429 without Retry-After header", async function () {
78
+ var fn = jest.fn()
79
+ .mockRejectedValueOnce(makeAxiosError(429))
80
+ .mockResolvedValue({ status: 200, data: {} });
81
+ await (0, snClient_1.retryOnHttpErr)(fn, "test > rec1");
82
+ expect(genericUtils_1.wait).toHaveBeenCalledWith(10000);
83
+ });
84
+ // 500/502/503: Exponential backoff
85
+ it("retries 500 with exponential backoff (1s, 2s, 4s)", async function () {
86
+ var fn = jest.fn()
87
+ .mockRejectedValueOnce(makeAxiosError(500))
88
+ .mockRejectedValueOnce(makeAxiosError(500))
89
+ .mockRejectedValueOnce(makeAxiosError(500))
90
+ .mockResolvedValue({ status: 200, data: {} });
91
+ var result = await (0, snClient_1.retryOnHttpErr)(fn, "test > rec1");
92
+ expect(result).toEqual({ status: 200, data: {} });
93
+ expect(fn).toHaveBeenCalledTimes(4); // 3 failures + 1 success
94
+ expect(genericUtils_1.wait).toHaveBeenNthCalledWith(1, 1000); // 1s
95
+ expect(genericUtils_1.wait).toHaveBeenNthCalledWith(2, 2000); // 2s
96
+ expect(genericUtils_1.wait).toHaveBeenNthCalledWith(3, 4000); // 4s
97
+ });
98
+ it("gives up after 3 retries on 500", async function () {
99
+ var fn = jest.fn().mockRejectedValue(makeAxiosError(500));
100
+ await expect((0, snClient_1.retryOnHttpErr)(fn, "test > rec1")).rejects.toThrow();
101
+ expect(fn).toHaveBeenCalledTimes(4); // 1 initial + 3 retries
102
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("after 3 retries"));
103
+ });
104
+ it("retries 502 with exponential backoff", async function () {
105
+ var fn = jest.fn()
106
+ .mockRejectedValueOnce(makeAxiosError(502))
107
+ .mockResolvedValue({ status: 200, data: {} });
108
+ await (0, snClient_1.retryOnHttpErr)(fn, "test > rec1");
109
+ expect(fn).toHaveBeenCalledTimes(2);
110
+ expect(genericUtils_1.wait).toHaveBeenCalledWith(1000);
111
+ });
112
+ it("retries 503 with exponential backoff", async function () {
113
+ var fn = jest.fn()
114
+ .mockRejectedValueOnce(makeAxiosError(503))
115
+ .mockResolvedValue({ status: 200, data: {} });
116
+ await (0, snClient_1.retryOnHttpErr)(fn, "test > rec1");
117
+ expect(fn).toHaveBeenCalledTimes(2);
118
+ expect(genericUtils_1.wait).toHaveBeenCalledWith(1000);
119
+ });
120
+ it("caps backoff at 8s for server errors", async function () {
121
+ // After 3 retries with backoff doubling: 1s, 2s, 4s
122
+ // If we could do a 4th it would be 8s (capped), but we stop at 3
123
+ var fn = jest.fn()
124
+ .mockRejectedValueOnce(makeAxiosError(500))
125
+ .mockRejectedValueOnce(makeAxiosError(500))
126
+ .mockRejectedValueOnce(makeAxiosError(500))
127
+ .mockResolvedValue({ status: 200, data: {} });
128
+ await (0, snClient_1.retryOnHttpErr)(fn, "test > rec1");
129
+ // Verify the 3rd wait is 4s (next would be 8s = cap)
130
+ expect(genericUtils_1.wait).toHaveBeenNthCalledWith(3, 4000);
131
+ });
132
+ // Unknown errors: retry once
133
+ it("retries unknown status code once then fails", async function () {
134
+ var fn = jest.fn().mockRejectedValue(makeAxiosError(418));
135
+ await expect((0, snClient_1.retryOnHttpErr)(fn, "test > rec1")).rejects.toThrow();
136
+ expect(fn).toHaveBeenCalledTimes(2); // 1 initial + 1 retry
137
+ expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining("HTTP 418"));
138
+ expect(mockLogger.error).toHaveBeenCalledWith(expect.stringContaining("after 1 retry"));
139
+ });
140
+ it("retries non-HTTP error once then fails", async function () {
141
+ var fn = jest.fn().mockRejectedValue(new Error("ECONNRESET"));
142
+ await expect((0, snClient_1.retryOnHttpErr)(fn, "test > rec1")).rejects.toThrow("ECONNRESET");
143
+ expect(fn).toHaveBeenCalledTimes(2); // 1 initial + 1 retry
144
+ expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining("unknown error"));
145
+ });
146
+ it("succeeds on retry for unknown error", async function () {
147
+ var fn = jest.fn()
148
+ .mockRejectedValueOnce(makeAxiosError(418))
149
+ .mockResolvedValue({ status: 200, data: {} });
150
+ var result = await (0, snClient_1.retryOnHttpErr)(fn, "test > rec1");
151
+ expect(result).toEqual({ status: 200, data: {} });
152
+ expect(fn).toHaveBeenCalledTimes(2);
153
+ });
154
+ });
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const MultiScopeWatcher_1 = require("../MultiScopeWatcher");
4
+ // Track API calls
5
+ var apiCalls = [];
6
+ jest.mock("../snClient", function () {
7
+ return {
8
+ defaultClient: function () {
9
+ return {
10
+ getScopeId: function (scope) {
11
+ apiCalls.push("getScopeId:" + scope);
12
+ return Promise.resolve({ data: { result: [{ sys_id: "scope_" + scope }] } });
13
+ },
14
+ getUserSysId: function () {
15
+ apiCalls.push("getUserSysId");
16
+ return Promise.resolve({ data: { result: [{ sys_id: "user_abc123" }] } });
17
+ },
18
+ getCurrentAppUserPrefSysId: function (userSysId) {
19
+ apiCalls.push("getCurrentAppUserPrefSysId");
20
+ return Promise.resolve({ data: { result: [{ sys_id: "pref_123" }] } });
21
+ },
22
+ updateCurrentAppUserPref: function (scopeSysId, prefSysId) {
23
+ apiCalls.push("updateCurrentAppUserPref");
24
+ return Promise.resolve({});
25
+ },
26
+ createCurrentAppUserPref: function (scopeSysId, userSysId) {
27
+ apiCalls.push("createCurrentAppUserPref");
28
+ return Promise.resolve({});
29
+ }
30
+ };
31
+ },
32
+ unwrapSNResponse: function (resp) {
33
+ return resp.then(function (r) { return r.data.result; });
34
+ }
35
+ };
36
+ });
37
+ jest.mock("../Logger", function () {
38
+ return {
39
+ logger: {
40
+ info: jest.fn(),
41
+ debug: jest.fn(),
42
+ warn: jest.fn(),
43
+ error: jest.fn(),
44
+ success: jest.fn()
45
+ }
46
+ };
47
+ });
48
+ jest.mock("../config", function () {
49
+ return {
50
+ loadConfigs: jest.fn().mockResolvedValue(undefined),
51
+ getConfig: jest.fn().mockReturnValue({ scopes: {} }),
52
+ getRootDir: jest.fn().mockReturnValue("/tmp"),
53
+ updateManifest: jest.fn()
54
+ };
55
+ });
56
+ describe("Scope Caching (US-013)", function () {
57
+ beforeEach(function () {
58
+ apiCalls = [];
59
+ // Reset cached state
60
+ MultiScopeWatcher_1.multiScopeWatcher.cachedScope = null;
61
+ MultiScopeWatcher_1.multiScopeWatcher.cachedUserSysId = null;
62
+ });
63
+ it("should make API calls on first switch to a scope", async function () {
64
+ await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
65
+ expect(apiCalls).toContain("getScopeId:x_cadso_core");
66
+ expect(apiCalls).toContain("getUserSysId");
67
+ expect(apiCalls).toContain("getCurrentAppUserPrefSysId");
68
+ expect(apiCalls).toContain("updateCurrentAppUserPref");
69
+ });
70
+ it("should skip API calls when switching to the same scope (cache hit)", async function () {
71
+ await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
72
+ apiCalls = [];
73
+ await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
74
+ expect(apiCalls).toEqual([]);
75
+ });
76
+ it("should make API calls when switching to a different scope (cache miss)", async function () {
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 () {
85
+ await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
86
+ apiCalls = [];
87
+ await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_automate");
88
+ var userCalls = apiCalls.filter(function (c) { return c === "getUserSysId"; });
89
+ expect(userCalls).toHaveLength(0);
90
+ expect(MultiScopeWatcher_1.multiScopeWatcher.cachedUserSysId).toBe("user_abc123");
91
+ });
92
+ it("should update cachedScope after a successful switch", async function () {
93
+ expect(MultiScopeWatcher_1.multiScopeWatcher.cachedScope).toBeNull();
94
+ await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
95
+ expect(MultiScopeWatcher_1.multiScopeWatcher.cachedScope).toBe("x_cadso_core");
96
+ await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_automate");
97
+ expect(MultiScopeWatcher_1.multiScopeWatcher.cachedScope).toBe("x_cadso_automate");
98
+ });
99
+ it("should invalidate cachedScope on failure", async function () {
100
+ // First successful switch
101
+ await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_cadso_core");
102
+ expect(MultiScopeWatcher_1.multiScopeWatcher.cachedScope).toBe("x_cadso_core");
103
+ // Mock getScopeId to fail for a bad scope
104
+ var snClient = require("../snClient");
105
+ var origDefault = snClient.defaultClient;
106
+ snClient.defaultClient = function () {
107
+ var client = origDefault();
108
+ client.getScopeId = function () {
109
+ apiCalls.push("getScopeId:bad_scope");
110
+ return Promise.resolve({ data: { result: [] } });
111
+ };
112
+ return client;
113
+ };
114
+ try {
115
+ await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("bad_scope");
116
+ }
117
+ catch (e) {
118
+ // Expected
119
+ }
120
+ expect(MultiScopeWatcher_1.multiScopeWatcher.cachedScope).toBeNull();
121
+ // Restore
122
+ snClient.defaultClient = origDefault;
123
+ });
124
+ });