@tenonhq/dovetail-core 0.0.85
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 +6 -0
- package/dist/FileLogger.js +207 -0
- package/dist/FileUtils.js +387 -0
- package/dist/Logger.js +88 -0
- package/dist/MultiScopeWatcher.js +615 -0
- package/dist/PluginManager.js +125 -0
- package/dist/_defaultOptionsOLD.js +44 -0
- package/dist/allScopesCommands.js +418 -0
- package/dist/appUtils.js +929 -0
- package/dist/benchmark.js +104 -0
- package/dist/bootstrap.js +62 -0
- package/dist/claudeCommand.js +60 -0
- package/dist/clickupCommands.js +472 -0
- package/dist/clickupPushHelper.js +33 -0
- package/dist/commander.js +466 -0
- package/dist/commands.js +378 -0
- package/dist/config.js +697 -0
- package/dist/constants.js +15 -0
- package/dist/createRecordCommand.js +314 -0
- package/dist/dashboardCommand.js +38 -0
- package/dist/defaultOptions.js +14 -0
- package/dist/deleteRecordCommand.js +282 -0
- package/dist/flowDesigner/values.js +55 -0
- package/dist/genericUtils.js +189 -0
- package/dist/gitUtils.js +99 -0
- package/dist/index.js +35 -0
- package/dist/initSystem/corePlugin.js +344 -0
- package/dist/initSystem/discovery.js +107 -0
- package/dist/initSystem/orchestrator.js +408 -0
- package/dist/logMessages.js +82 -0
- package/dist/loginCommand.js +20 -0
- package/dist/migrateCommand.js +123 -0
- package/dist/projectFiles.js +32 -0
- package/dist/recentEdits.js +66 -0
- package/dist/schemaCommand.js +85 -0
- package/dist/snClient.js +519 -0
- package/dist/tests/benchmarkRefresh.test.js +265 -0
- package/dist/tests/clickupCommands.test.js +384 -0
- package/dist/tests/clickupPushHelper.test.js +124 -0
- package/dist/tests/configFallback.test.js +68 -0
- package/dist/tests/cred.test.js +59 -0
- package/dist/tests/discovery.test.js +67 -0
- package/dist/tests/ensureUpdateSetWarnings.test.js +218 -0
- package/dist/tests/errorLogLevels.test.js +273 -0
- package/dist/tests/example.test.js +6 -0
- package/dist/tests/fileContextSkipReason.test.js +116 -0
- package/dist/tests/globalDebounce.test.js +308 -0
- package/dist/tests/migrateCommand.test.js +113 -0
- package/dist/tests/multi-scope-watcher.test.js +558 -0
- package/dist/tests/orchestrator.test.js +23 -0
- package/dist/tests/pushFiles.test.js +248 -0
- package/dist/tests/rateLimitCoordination.test.js +271 -0
- package/dist/tests/resolveConfigReadOnly.test.js +67 -0
- package/dist/tests/retryOnHttpErr.test.js +154 -0
- package/dist/tests/scopeCaching.test.js +115 -0
- package/dist/tests/serializeUpdateSetConfig.test.js +325 -0
- package/dist/tests/snClient.fallback.test.js +120 -0
- package/dist/tests/syncManifestRefresh.test.js +287 -0
- package/dist/tests/syncManifestWhitelist.test.js +192 -0
- package/dist/tests/taskClear.test.js +170 -0
- package/dist/tests/taskStaleness.test.js +220 -0
- package/dist/tests/unwrapSNResponseError.test.js +145 -0
- package/dist/tests/v2Values.test.js +79 -0
- package/dist/tests/validateTaskId.test.js +304 -0
- package/dist/tests/verifyUpdateSetSwitch.test.js +277 -0
- package/dist/tests/writeEnvVar.test.js +119 -0
- package/dist/updateSetCommands.js +702 -0
- package/dist/wizard.js +40 -0
- package/package.json +69 -0
- package/skills/dove-configure-pipeline.md +158 -0
- package/skills/dove-create-plugin.md +174 -0
- package/skills/dove-create-record.md +135 -0
- package/skills/dove-debug-build.md +117 -0
- package/skills/dove-delete-record.md +104 -0
- package/skills/dove-manage-tables.md +174 -0
- package/skills/dove-manage-update-sets.md +88 -0
- package/skills/dove-setup-project.md +154 -0
- package/skills/dove-troubleshoot-sync.md +113 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Track how many times the update set config file is read
|
|
4
|
+
var configReadCount = 0;
|
|
5
|
+
var mockConfigData = {};
|
|
6
|
+
// Per-scope read-only table sets, consumed by the mocked ../config module.
|
|
7
|
+
// Mutate this object between tests to drive the push-gate behavior.
|
|
8
|
+
var mockReadOnlyByScope = {};
|
|
9
|
+
jest.mock("fs", function () {
|
|
10
|
+
var actualFs = jest.requireActual("fs");
|
|
11
|
+
return {
|
|
12
|
+
...actualFs,
|
|
13
|
+
existsSync: function (p) {
|
|
14
|
+
if (typeof p === "string" && p.endsWith(".dove-update-sets.json")) {
|
|
15
|
+
configReadCount++;
|
|
16
|
+
return Object.keys(mockConfigData).length > 0;
|
|
17
|
+
}
|
|
18
|
+
return actualFs.existsSync(p);
|
|
19
|
+
},
|
|
20
|
+
readFileSync: function (p, encoding) {
|
|
21
|
+
if (typeof p === "string" && p.endsWith(".dove-update-sets.json")) {
|
|
22
|
+
return JSON.stringify(mockConfigData);
|
|
23
|
+
}
|
|
24
|
+
return actualFs.readFileSync(p, encoding);
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
// Mock only the config surface pushFiles touches. getReadOnlyTablesForScope
|
|
29
|
+
// is the one new call site; the rest of pushFiles reads no ConfigManager
|
|
30
|
+
// exports, so a minimal mock keeps the test focused on the push gate.
|
|
31
|
+
jest.mock("../config", function () {
|
|
32
|
+
return {
|
|
33
|
+
getReadOnlyTablesForScope: function (scope) {
|
|
34
|
+
return mockReadOnlyByScope[scope] || new Set();
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
// Mock dependencies that pushFiles needs
|
|
39
|
+
var mockPushWithUpdateSet = jest.fn().mockResolvedValue({
|
|
40
|
+
data: { result: { status: "success" } },
|
|
41
|
+
});
|
|
42
|
+
var mockUpdateRecord = jest.fn().mockResolvedValue({
|
|
43
|
+
data: { result: { status: "success" } },
|
|
44
|
+
});
|
|
45
|
+
jest.mock("../snClient", function () {
|
|
46
|
+
return {
|
|
47
|
+
defaultClient: function () {
|
|
48
|
+
return {
|
|
49
|
+
pushWithUpdateSet: mockPushWithUpdateSet,
|
|
50
|
+
updateRecord: mockUpdateRecord,
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
processPushResponse: function (_res, summary) {
|
|
54
|
+
return { success: true, message: summary };
|
|
55
|
+
},
|
|
56
|
+
retryOnErr: function (fn) {
|
|
57
|
+
return fn();
|
|
58
|
+
},
|
|
59
|
+
retryOnHttpErr: function (fn) {
|
|
60
|
+
return fn();
|
|
61
|
+
},
|
|
62
|
+
unwrapSNResponse: jest.fn(),
|
|
63
|
+
unwrapTableAPIFirstItem: jest.fn(),
|
|
64
|
+
SNClient: jest.fn(),
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
jest.mock("../PluginManager", function () {
|
|
68
|
+
return {
|
|
69
|
+
__esModule: true,
|
|
70
|
+
default: {
|
|
71
|
+
getFinalFileContents: function (ctx) {
|
|
72
|
+
return Promise.resolve("built-content");
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
jest.mock("../Logger", function () {
|
|
78
|
+
return {
|
|
79
|
+
logger: {
|
|
80
|
+
info: jest.fn(),
|
|
81
|
+
debug: jest.fn(),
|
|
82
|
+
warn: jest.fn(),
|
|
83
|
+
error: jest.fn(),
|
|
84
|
+
getLogLevel: function () { return "debug"; },
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
jest.mock("progress", function () {
|
|
89
|
+
return jest.fn().mockImplementation(function () {
|
|
90
|
+
return { tick: jest.fn() };
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
function makeRecord(table, sysId, scope) {
|
|
94
|
+
return {
|
|
95
|
+
table: table,
|
|
96
|
+
sysId: sysId,
|
|
97
|
+
fields: {
|
|
98
|
+
script: {
|
|
99
|
+
name: "TestRecord",
|
|
100
|
+
tableName: table,
|
|
101
|
+
targetField: "script",
|
|
102
|
+
ext: "js",
|
|
103
|
+
sys_id: sysId,
|
|
104
|
+
filePath: "/fake/path/script.js",
|
|
105
|
+
scope: scope,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
describe("pushFiles", function () {
|
|
111
|
+
beforeEach(function () {
|
|
112
|
+
configReadCount = 0;
|
|
113
|
+
mockConfigData = {};
|
|
114
|
+
mockReadOnlyByScope = {};
|
|
115
|
+
mockPushWithUpdateSet.mockClear();
|
|
116
|
+
mockUpdateRecord.mockClear();
|
|
117
|
+
});
|
|
118
|
+
it("reads update set config once at batch start, not per record", async function () {
|
|
119
|
+
// Set up config for two scopes
|
|
120
|
+
mockConfigData = {
|
|
121
|
+
"x_cadso_core": { sys_id: "us-core-123", name: "Core US" },
|
|
122
|
+
"x_cadso_work": { sys_id: "us-work-456", name: "Work US" },
|
|
123
|
+
};
|
|
124
|
+
var { pushFiles } = require("../appUtils");
|
|
125
|
+
var records = [
|
|
126
|
+
makeRecord("sys_script_include", "rec1", "x_cadso_core"),
|
|
127
|
+
makeRecord("sys_script_include", "rec2", "x_cadso_work"),
|
|
128
|
+
makeRecord("sys_script_include", "rec3", "x_cadso_core"),
|
|
129
|
+
];
|
|
130
|
+
await pushFiles(records);
|
|
131
|
+
// Config existsSync is hit twice at batch start: once by resolveDoveDotfile
|
|
132
|
+
// (preferring .dove-update-sets.json over the legacy .sinc-* fallback) and
|
|
133
|
+
// once by getUpdateSetConfig() to gate the JSON read. The point of this
|
|
134
|
+
// test is that the count is bounded by the resolution + read pattern, not
|
|
135
|
+
// by record count — 3 records still produces 2 checks, not 6.
|
|
136
|
+
expect(configReadCount).toBe(2);
|
|
137
|
+
});
|
|
138
|
+
it("does not re-read config mid-batch even with multiple scopes", async function () {
|
|
139
|
+
mockConfigData = {
|
|
140
|
+
"x_cadso_core": { sys_id: "us-core-123", name: "Core US" },
|
|
141
|
+
};
|
|
142
|
+
var { pushFiles } = require("../appUtils");
|
|
143
|
+
var records = [
|
|
144
|
+
makeRecord("sys_script_include", "rec1", "x_cadso_core"),
|
|
145
|
+
makeRecord("sys_script_include", "rec2", "x_cadso_core"),
|
|
146
|
+
];
|
|
147
|
+
await pushFiles(records);
|
|
148
|
+
// Bounded by resolver + read-gate (see note above), not by record count.
|
|
149
|
+
expect(configReadCount).toBe(2);
|
|
150
|
+
});
|
|
151
|
+
it("routes records to correct update sets from cached config", async function () {
|
|
152
|
+
mockConfigData = {
|
|
153
|
+
"x_cadso_core": { sys_id: "us-core-123", name: "Core US" },
|
|
154
|
+
"x_cadso_work": { sys_id: "us-work-456", name: "Work US" },
|
|
155
|
+
};
|
|
156
|
+
var { pushFiles } = require("../appUtils");
|
|
157
|
+
var records = [
|
|
158
|
+
makeRecord("sys_script_include", "rec1", "x_cadso_core"),
|
|
159
|
+
makeRecord("sys_script_include", "rec2", "x_cadso_work"),
|
|
160
|
+
];
|
|
161
|
+
await pushFiles(records);
|
|
162
|
+
// Both should use pushWithUpdateSet with their respective update set IDs
|
|
163
|
+
expect(mockPushWithUpdateSet).toHaveBeenCalledTimes(2);
|
|
164
|
+
var calls = mockPushWithUpdateSet.mock.calls;
|
|
165
|
+
var updateSetIds = calls.map(function (call) { return call[0]; });
|
|
166
|
+
expect(updateSetIds).toContain("us-core-123");
|
|
167
|
+
expect(updateSetIds).toContain("us-work-456");
|
|
168
|
+
});
|
|
169
|
+
it("uses updateRecord when no update set config exists", async function () {
|
|
170
|
+
mockConfigData = {};
|
|
171
|
+
var { pushFiles } = require("../appUtils");
|
|
172
|
+
var records = [
|
|
173
|
+
makeRecord("sys_script_include", "rec1", "x_cadso_core"),
|
|
174
|
+
];
|
|
175
|
+
await pushFiles(records);
|
|
176
|
+
expect(mockUpdateRecord).toHaveBeenCalledTimes(1);
|
|
177
|
+
expect(mockPushWithUpdateSet).not.toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
it("skips push for tables listed in _readOnlyTables for the scope", async function () {
|
|
180
|
+
mockConfigData = {};
|
|
181
|
+
mockReadOnlyByScope = {
|
|
182
|
+
x_cadso_core: new Set(["sys_hub_flow"]),
|
|
183
|
+
};
|
|
184
|
+
var { pushFiles } = require("../appUtils");
|
|
185
|
+
var records = [
|
|
186
|
+
makeRecord("sys_hub_flow", "flow1", "x_cadso_core"),
|
|
187
|
+
];
|
|
188
|
+
var results = await pushFiles(records);
|
|
189
|
+
expect(mockPushWithUpdateSet).not.toHaveBeenCalled();
|
|
190
|
+
expect(mockUpdateRecord).not.toHaveBeenCalled();
|
|
191
|
+
expect(results).toHaveLength(1);
|
|
192
|
+
expect(results[0].success).toBe(true);
|
|
193
|
+
expect(results[0].message).toContain("skipped (read-only table)");
|
|
194
|
+
});
|
|
195
|
+
it("pushes normally when the scope has no read-only tables", async function () {
|
|
196
|
+
mockConfigData = {};
|
|
197
|
+
mockReadOnlyByScope = {};
|
|
198
|
+
var { pushFiles } = require("../appUtils");
|
|
199
|
+
var records = [
|
|
200
|
+
makeRecord("sys_script_include", "rec1", "x_cadso_core"),
|
|
201
|
+
];
|
|
202
|
+
await pushFiles(records);
|
|
203
|
+
expect(mockUpdateRecord).toHaveBeenCalledTimes(1);
|
|
204
|
+
expect(mockPushWithUpdateSet).not.toHaveBeenCalled();
|
|
205
|
+
});
|
|
206
|
+
it("in a mixed batch, only non-read-only tables are pushed", async function () {
|
|
207
|
+
mockConfigData = {};
|
|
208
|
+
mockReadOnlyByScope = {
|
|
209
|
+
x_cadso_core: new Set(["sys_hub_flow"]),
|
|
210
|
+
};
|
|
211
|
+
var { pushFiles } = require("../appUtils");
|
|
212
|
+
var records = [
|
|
213
|
+
makeRecord("sys_hub_flow", "flow1", "x_cadso_core"),
|
|
214
|
+
makeRecord("sys_script_include", "rec1", "x_cadso_core"),
|
|
215
|
+
makeRecord("sys_hub_action_instance", "ai1", "x_cadso_core"),
|
|
216
|
+
];
|
|
217
|
+
var results = await pushFiles(records);
|
|
218
|
+
// Only the read-only flow row is skipped; the other two hit updateRecord.
|
|
219
|
+
expect(mockUpdateRecord).toHaveBeenCalledTimes(2);
|
|
220
|
+
expect(mockPushWithUpdateSet).not.toHaveBeenCalled();
|
|
221
|
+
expect(results).toHaveLength(3);
|
|
222
|
+
var skipped = results.filter(function (r) {
|
|
223
|
+
return r.message.indexOf("skipped (read-only table)") !== -1;
|
|
224
|
+
});
|
|
225
|
+
expect(skipped).toHaveLength(1);
|
|
226
|
+
});
|
|
227
|
+
it("applies scope-specific read-only tables independently across scopes", async function () {
|
|
228
|
+
mockConfigData = {};
|
|
229
|
+
mockReadOnlyByScope = {
|
|
230
|
+
x_cadso_core: new Set(["sys_hub_flow"]),
|
|
231
|
+
x_cadso_work: new Set(),
|
|
232
|
+
};
|
|
233
|
+
var { pushFiles } = require("../appUtils");
|
|
234
|
+
var records = [
|
|
235
|
+
makeRecord("sys_hub_flow", "flow1", "x_cadso_core"),
|
|
236
|
+
makeRecord("sys_hub_flow", "flow2", "x_cadso_work"),
|
|
237
|
+
];
|
|
238
|
+
var results = await pushFiles(records);
|
|
239
|
+
// x_cadso_core's flow is blocked; x_cadso_work's flow pushes because it
|
|
240
|
+
// isn't in that scope's read-only set.
|
|
241
|
+
expect(mockUpdateRecord).toHaveBeenCalledTimes(1);
|
|
242
|
+
expect(results).toHaveLength(2);
|
|
243
|
+
var skipped = results.filter(function (r) {
|
|
244
|
+
return r.message.indexOf("skipped (read-only table)") !== -1;
|
|
245
|
+
});
|
|
246
|
+
expect(skipped).toHaveLength(1);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -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/dove.manifest." + scope + ".json"; }),
|
|
132
|
+
getManifestPath: jest.fn().mockReturnValue("/project/dove.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,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const config_1 = require("../config");
|
|
4
|
+
const baseConfig = {
|
|
5
|
+
sourceDirectory: "src",
|
|
6
|
+
buildDirectory: "build",
|
|
7
|
+
includes: {},
|
|
8
|
+
excludes: {},
|
|
9
|
+
tableOptions: {},
|
|
10
|
+
refreshInterval: 30,
|
|
11
|
+
scopes: {},
|
|
12
|
+
};
|
|
13
|
+
function withIncludes(overrides) {
|
|
14
|
+
return {
|
|
15
|
+
...baseConfig,
|
|
16
|
+
includes: overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
describe("resolveConfigForScope - _readOnlyTables", () => {
|
|
20
|
+
it("returns an empty list when _readOnlyTables is absent", () => {
|
|
21
|
+
const cfg = withIncludes({ _tables: ["incident", "sys_hub_flow"] });
|
|
22
|
+
const resolved = (0, config_1.resolveConfigForScope)("any_scope", cfg);
|
|
23
|
+
expect(resolved.readOnlyTables).toEqual([]);
|
|
24
|
+
});
|
|
25
|
+
it("returns the global _readOnlyTables list when no scope override", () => {
|
|
26
|
+
const cfg = withIncludes({
|
|
27
|
+
_tables: ["incident", "sys_hub_flow"],
|
|
28
|
+
_readOnlyTables: ["sys_hub_flow"],
|
|
29
|
+
});
|
|
30
|
+
const resolved = (0, config_1.resolveConfigForScope)("x_cadso_core", cfg);
|
|
31
|
+
expect(resolved.readOnlyTables).toEqual(["sys_hub_flow"]);
|
|
32
|
+
});
|
|
33
|
+
it("unions global and scope _readOnlyTables without duplicates", () => {
|
|
34
|
+
const cfg = withIncludes({
|
|
35
|
+
_tables: ["incident", "sys_hub_flow", "sys_hub_action_instance"],
|
|
36
|
+
_readOnlyTables: ["sys_hub_flow"],
|
|
37
|
+
_scopes: {
|
|
38
|
+
x_cadso_core: {
|
|
39
|
+
_readOnlyTables: ["sys_hub_flow", "sys_hub_action_instance"],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const resolved = (0, config_1.resolveConfigForScope)("x_cadso_core", cfg);
|
|
44
|
+
expect(resolved.readOnlyTables.sort()).toEqual(["sys_hub_action_instance", "sys_hub_flow"].sort());
|
|
45
|
+
});
|
|
46
|
+
it("falls back to the global list for scopes without an override", () => {
|
|
47
|
+
const cfg = withIncludes({
|
|
48
|
+
_tables: ["incident", "sys_hub_flow"],
|
|
49
|
+
_readOnlyTables: ["sys_hub_flow"],
|
|
50
|
+
_scopes: {
|
|
51
|
+
x_cadso_core: {
|
|
52
|
+
_readOnlyTables: ["sys_hub_action_instance"],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
const resolved = (0, config_1.resolveConfigForScope)("x_cadso_other", cfg);
|
|
57
|
+
expect(resolved.readOnlyTables).toEqual(["sys_hub_flow"]);
|
|
58
|
+
});
|
|
59
|
+
it("ignores non-array _readOnlyTables values safely", () => {
|
|
60
|
+
const cfg = withIncludes({
|
|
61
|
+
_tables: ["incident"],
|
|
62
|
+
_readOnlyTables: "oops-not-an-array",
|
|
63
|
+
});
|
|
64
|
+
const resolved = (0, config_1.resolveConfigForScope)("any_scope", cfg);
|
|
65
|
+
expect(resolved.readOnlyTables).toEqual([]);
|
|
66
|
+
});
|
|
67
|
+
});
|