@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,558 @@
|
|
|
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
|
+
const handlers = {};
|
|
41
|
+
const mock = {
|
|
42
|
+
_handlers: handlers,
|
|
43
|
+
on: jest.fn((event, handler) => {
|
|
44
|
+
if (!handlers[event])
|
|
45
|
+
handlers[event] = [];
|
|
46
|
+
handlers[event].push(handler);
|
|
47
|
+
return mock;
|
|
48
|
+
}),
|
|
49
|
+
close: jest.fn(),
|
|
50
|
+
_emit: (event, ...args) => {
|
|
51
|
+
(handlers[event] || []).forEach((h) => h(...args));
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
return mock;
|
|
55
|
+
}
|
|
56
|
+
const mockWatchers = [];
|
|
57
|
+
jest.mock("chokidar", () => ({
|
|
58
|
+
watch: jest.fn(() => {
|
|
59
|
+
const w = createMockWatcher();
|
|
60
|
+
mockWatchers.push(w);
|
|
61
|
+
return w;
|
|
62
|
+
}),
|
|
63
|
+
}));
|
|
64
|
+
// Debounce: capture the global processor, tests trigger manually
|
|
65
|
+
const capturedDebounceFns = [];
|
|
66
|
+
jest.mock("lodash", () => {
|
|
67
|
+
const actual = jest.requireActual("lodash");
|
|
68
|
+
return {
|
|
69
|
+
...actual,
|
|
70
|
+
debounce: jest.fn((fn) => {
|
|
71
|
+
capturedDebounceFns.push(fn);
|
|
72
|
+
const wrapper = jest.fn();
|
|
73
|
+
wrapper.cancel = jest.fn();
|
|
74
|
+
wrapper.flush = jest.fn(() => fn());
|
|
75
|
+
return wrapper;
|
|
76
|
+
}),
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
const 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
|
+
changeScope: jest.fn().mockResolvedValue(undefined),
|
|
87
|
+
};
|
|
88
|
+
jest.mock("../snClient", () => ({
|
|
89
|
+
defaultClient: jest.fn(() => mockSNClient),
|
|
90
|
+
unwrapSNResponse: jest.fn((val) => val),
|
|
91
|
+
}));
|
|
92
|
+
jest.mock("../FileUtils", () => ({
|
|
93
|
+
getFileContextFromPath: jest.fn(),
|
|
94
|
+
getFileContextWithSkipReason: jest.fn(),
|
|
95
|
+
}));
|
|
96
|
+
jest.mock("../appUtils", () => ({
|
|
97
|
+
groupAppFiles: jest.fn(),
|
|
98
|
+
pushFiles: jest.fn(),
|
|
99
|
+
}));
|
|
100
|
+
jest.mock("../logMessages", () => ({
|
|
101
|
+
logFilePush: jest.fn(),
|
|
102
|
+
}));
|
|
103
|
+
jest.mock("../recentEdits", () => ({
|
|
104
|
+
writeRecentEdit: jest.fn(),
|
|
105
|
+
}));
|
|
106
|
+
jest.mock("../Logger", () => ({
|
|
107
|
+
logger: {
|
|
108
|
+
info: jest.fn(),
|
|
109
|
+
error: jest.fn(),
|
|
110
|
+
warn: jest.fn(),
|
|
111
|
+
debug: jest.fn(),
|
|
112
|
+
success: jest.fn(),
|
|
113
|
+
getLogLevel: jest.fn().mockReturnValue("info"),
|
|
114
|
+
},
|
|
115
|
+
}));
|
|
116
|
+
jest.mock("../config", () => ({
|
|
117
|
+
loadConfigs: jest.fn().mockResolvedValue(undefined),
|
|
118
|
+
getConfig: jest.fn(),
|
|
119
|
+
getRootDir: jest.fn().mockReturnValue("/project"),
|
|
120
|
+
updateManifest: jest.fn(),
|
|
121
|
+
getManifest: jest.fn(),
|
|
122
|
+
getSourcePath: jest.fn().mockReturnValue("/project/src"),
|
|
123
|
+
getScopeManifestPath: jest.fn((scope) => `/project/dove.manifest.${scope}.json`),
|
|
124
|
+
getManifestPath: jest.fn().mockReturnValue("/project/dove.manifest.json"),
|
|
125
|
+
}));
|
|
126
|
+
// Mock fs for manifest loading and config file access
|
|
127
|
+
jest.mock("fs", () => ({
|
|
128
|
+
existsSync: jest.fn(),
|
|
129
|
+
readFileSync: jest.fn(),
|
|
130
|
+
writeFileSync: jest.fn(),
|
|
131
|
+
promises: {
|
|
132
|
+
readFile: jest.fn(),
|
|
133
|
+
writeFile: jest.fn(),
|
|
134
|
+
readdir: jest.fn(),
|
|
135
|
+
mkdir: jest.fn(),
|
|
136
|
+
access: jest.fn(),
|
|
137
|
+
stat: jest.fn(),
|
|
138
|
+
},
|
|
139
|
+
}));
|
|
140
|
+
// Also mock axios for getUpdateSetDetails
|
|
141
|
+
jest.mock("axios", () => ({
|
|
142
|
+
default: {
|
|
143
|
+
create: jest.fn(() => ({
|
|
144
|
+
get: jest.fn().mockResolvedValue({ data: { result: null } }),
|
|
145
|
+
})),
|
|
146
|
+
},
|
|
147
|
+
}));
|
|
148
|
+
// --- Imports ---
|
|
149
|
+
const chokidar_1 = __importDefault(require("chokidar"));
|
|
150
|
+
const fs_1 = __importDefault(require("fs"));
|
|
151
|
+
const ConfigManager = __importStar(require("../config"));
|
|
152
|
+
const FileUtils_1 = require("../FileUtils");
|
|
153
|
+
const appUtils_1 = require("../appUtils");
|
|
154
|
+
const Logger_1 = require("../Logger");
|
|
155
|
+
const MultiScopeWatcher_1 = require("../MultiScopeWatcher");
|
|
156
|
+
// Helper to flush microtask queue (for async code in setInterval callbacks)
|
|
157
|
+
const flushPromises = () => new Promise((resolve) => setImmediate(resolve));
|
|
158
|
+
// --- Fixtures ---
|
|
159
|
+
const makeFileContext = (overrides = {}) => ({
|
|
160
|
+
filePath: "/project/src/x_test_core/sys_script_include/TestScript/script.js",
|
|
161
|
+
ext: ".js",
|
|
162
|
+
sys_id: "abc123",
|
|
163
|
+
name: "TestScript",
|
|
164
|
+
scope: "x_test_core",
|
|
165
|
+
tableName: "sys_script_include",
|
|
166
|
+
targetField: "script",
|
|
167
|
+
...overrides,
|
|
168
|
+
});
|
|
169
|
+
const MOCK_CONFIG_TWO_SCOPES = {
|
|
170
|
+
sourceDirectory: "src",
|
|
171
|
+
buildDirectory: "build",
|
|
172
|
+
rules: [],
|
|
173
|
+
includes: {},
|
|
174
|
+
excludes: {},
|
|
175
|
+
tableOptions: {},
|
|
176
|
+
refreshInterval: 30,
|
|
177
|
+
scopes: {
|
|
178
|
+
x_test_core: { sourceDirectory: "src/x_test_core" },
|
|
179
|
+
x_test_work: {},
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
const MOCK_CONFIG_CUSTOM_DIR = {
|
|
183
|
+
...MOCK_CONFIG_TWO_SCOPES,
|
|
184
|
+
scopes: {
|
|
185
|
+
x_custom: { sourceDirectory: "custom/path" },
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
const MOCK_CONFIG_NO_SCOPES = {
|
|
189
|
+
sourceDirectory: "src",
|
|
190
|
+
};
|
|
191
|
+
// --- Tests ---
|
|
192
|
+
describe("MultiScopeWatcherManager", () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
jest.clearAllMocks();
|
|
195
|
+
mockWatchers.length = 0;
|
|
196
|
+
capturedDebounceFns.length = 0;
|
|
197
|
+
// Reset the internal state by stopping any previous watchers
|
|
198
|
+
(0, MultiScopeWatcher_1.stopMultiScopeWatching)();
|
|
199
|
+
// Reset scope and user caches (US-013)
|
|
200
|
+
MultiScopeWatcher_1.multiScopeWatcher.cachedScope = null;
|
|
201
|
+
MultiScopeWatcher_1.multiScopeWatcher.cachedUserSysId = null;
|
|
202
|
+
// Reset global debounce state (US-014)
|
|
203
|
+
MultiScopeWatcher_1.multiScopeWatcher.pendingScopes = new Map();
|
|
204
|
+
MultiScopeWatcher_1.multiScopeWatcher.globalProcessQueue = null;
|
|
205
|
+
// Default: scope switching succeeds
|
|
206
|
+
mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
|
|
207
|
+
mockSNClient.getUserSysId.mockResolvedValue([{ sys_id: "user_sys_id" }]);
|
|
208
|
+
mockSNClient.getCurrentAppUserPrefSysId.mockResolvedValue([{ sys_id: "pref_sys_id" }]);
|
|
209
|
+
mockSNClient.updateCurrentAppUserPref.mockResolvedValue({});
|
|
210
|
+
mockSNClient.createCurrentAppUserPref.mockResolvedValue({});
|
|
211
|
+
mockSNClient.getCurrentUpdateSetUserPref.mockResolvedValue([]);
|
|
212
|
+
});
|
|
213
|
+
afterEach(() => {
|
|
214
|
+
(0, MultiScopeWatcher_1.stopMultiScopeWatching)();
|
|
215
|
+
jest.useRealTimers();
|
|
216
|
+
});
|
|
217
|
+
describe("startWatchingAllScopes", () => {
|
|
218
|
+
it("loads config and creates a watcher per scope", async () => {
|
|
219
|
+
ConfigManager.getConfig.mockReturnValue(MOCK_CONFIG_TWO_SCOPES);
|
|
220
|
+
await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
|
|
221
|
+
expect(ConfigManager.loadConfigs).toHaveBeenCalled();
|
|
222
|
+
expect(chokidar_1.default.watch).toHaveBeenCalledTimes(2);
|
|
223
|
+
});
|
|
224
|
+
it("throws when config has no scopes", async () => {
|
|
225
|
+
ConfigManager.getConfig.mockReturnValue(MOCK_CONFIG_NO_SCOPES);
|
|
226
|
+
await expect((0, MultiScopeWatcher_1.startMultiScopeWatching)()).rejects.toThrow("No scopes defined in configuration");
|
|
227
|
+
});
|
|
228
|
+
it("uses custom sourceDirectory from scope config", async () => {
|
|
229
|
+
ConfigManager.getConfig.mockReturnValue(MOCK_CONFIG_CUSTOM_DIR);
|
|
230
|
+
await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
|
|
231
|
+
expect(chokidar_1.default.watch).toHaveBeenCalledWith(expect.stringContaining("custom/path"), expect.any(Object));
|
|
232
|
+
});
|
|
233
|
+
it("uses default src/{scopeName} when no sourceDirectory", async () => {
|
|
234
|
+
ConfigManager.getConfig.mockReturnValue({
|
|
235
|
+
...MOCK_CONFIG_TWO_SCOPES,
|
|
236
|
+
scopes: { x_default_scope: {} },
|
|
237
|
+
});
|
|
238
|
+
await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
|
|
239
|
+
expect(chokidar_1.default.watch).toHaveBeenCalledWith(expect.stringContaining("/src/x_default_scope"), expect.any(Object));
|
|
240
|
+
});
|
|
241
|
+
it("configures chokidar with correct options", async () => {
|
|
242
|
+
ConfigManager.getConfig.mockReturnValue({
|
|
243
|
+
...MOCK_CONFIG_TWO_SCOPES,
|
|
244
|
+
scopes: { x_test: {} },
|
|
245
|
+
});
|
|
246
|
+
await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
|
|
247
|
+
expect(chokidar_1.default.watch).toHaveBeenCalledWith(expect.any(String), {
|
|
248
|
+
persistent: true,
|
|
249
|
+
ignoreInitial: true,
|
|
250
|
+
awaitWriteFinish: {
|
|
251
|
+
stabilityThreshold: 300,
|
|
252
|
+
pollInterval: 100,
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
it("registers change, add, and error handlers on each watcher", async () => {
|
|
257
|
+
ConfigManager.getConfig.mockReturnValue({
|
|
258
|
+
...MOCK_CONFIG_TWO_SCOPES,
|
|
259
|
+
scopes: { x_test: {} },
|
|
260
|
+
});
|
|
261
|
+
await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
|
|
262
|
+
const watcher = mockWatchers[0];
|
|
263
|
+
const registeredEvents = watcher.on.mock.calls.map((c) => c[0]);
|
|
264
|
+
expect(registeredEvents).toContain("change");
|
|
265
|
+
expect(registeredEvents).toContain("add");
|
|
266
|
+
expect(registeredEvents).toContain("error");
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
describe("file change in scope", () => {
|
|
270
|
+
beforeEach(async () => {
|
|
271
|
+
ConfigManager.getConfig.mockReturnValue({
|
|
272
|
+
...MOCK_CONFIG_TWO_SCOPES,
|
|
273
|
+
scopes: { x_test_core: { sourceDirectory: "src/x_test_core" } },
|
|
274
|
+
});
|
|
275
|
+
// fs mock for loadScopeManifest
|
|
276
|
+
fs_1.default.existsSync.mockReturnValue(false);
|
|
277
|
+
await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
|
|
278
|
+
});
|
|
279
|
+
it("processes file change through scope switch and push pipeline", async () => {
|
|
280
|
+
const ctx = makeFileContext();
|
|
281
|
+
FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ context: ctx });
|
|
282
|
+
appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
|
|
283
|
+
appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
|
|
284
|
+
// Emit change on the scope's watcher
|
|
285
|
+
mockWatchers[0]._emit("change", ctx.filePath);
|
|
286
|
+
// Trigger the debounced processor for this scope
|
|
287
|
+
await capturedDebounceFns[0]();
|
|
288
|
+
expect(FileUtils_1.getFileContextWithSkipReason).toHaveBeenCalled();
|
|
289
|
+
expect(appUtils_1.groupAppFiles).toHaveBeenCalled();
|
|
290
|
+
expect(appUtils_1.pushFiles).toHaveBeenCalled();
|
|
291
|
+
});
|
|
292
|
+
it("processes file add through the same pipeline", async () => {
|
|
293
|
+
const ctx = makeFileContext();
|
|
294
|
+
FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ context: ctx });
|
|
295
|
+
appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
|
|
296
|
+
appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
|
|
297
|
+
mockWatchers[0]._emit("add", ctx.filePath);
|
|
298
|
+
await capturedDebounceFns[0]();
|
|
299
|
+
expect(appUtils_1.pushFiles).toHaveBeenCalled();
|
|
300
|
+
});
|
|
301
|
+
it("warns when no valid file contexts found", async () => {
|
|
302
|
+
FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ skipReason: "not in manifest" });
|
|
303
|
+
mockWatchers[0]._emit("change", "/project/src/x_test_core/unknown/file.txt");
|
|
304
|
+
await capturedDebounceFns[0]();
|
|
305
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("No valid file contexts found"));
|
|
306
|
+
expect(appUtils_1.pushFiles).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
it("logs warning for each skipped file with reason", async () => {
|
|
309
|
+
FileUtils_1.getFileContextWithSkipReason
|
|
310
|
+
.mockReturnValueOnce({ skipReason: "not in manifest" })
|
|
311
|
+
.mockReturnValueOnce({ context: makeFileContext() });
|
|
312
|
+
appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
|
|
313
|
+
appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
|
|
314
|
+
mockWatchers[0]._emit("change", "/project/src/x_test_core/unknown/file.txt");
|
|
315
|
+
mockWatchers[0]._emit("change", makeFileContext().filePath);
|
|
316
|
+
await capturedDebounceFns[0]();
|
|
317
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("Skipped: /project/src/x_test_core/unknown/file.txt (not in manifest)"));
|
|
318
|
+
});
|
|
319
|
+
it("logs warning with scope not found reason", async () => {
|
|
320
|
+
FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ skipReason: "scope not found" });
|
|
321
|
+
mockWatchers[0]._emit("change", "/project/src/x_unknown/sys_script/file.js");
|
|
322
|
+
await capturedDebounceFns[0]();
|
|
323
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("Skipped: /project/src/x_unknown/sys_script/file.js (scope not found)"));
|
|
324
|
+
});
|
|
325
|
+
it("logs push summary with skipped file count", async () => {
|
|
326
|
+
var ctx = makeFileContext();
|
|
327
|
+
FileUtils_1.getFileContextWithSkipReason
|
|
328
|
+
.mockReturnValueOnce({ context: ctx })
|
|
329
|
+
.mockReturnValueOnce({ skipReason: "not in manifest" });
|
|
330
|
+
appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
|
|
331
|
+
appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
|
|
332
|
+
mockWatchers[0]._emit("change", ctx.filePath);
|
|
333
|
+
mockWatchers[0]._emit("change", "/project/src/x_test_core/unknown/other.txt");
|
|
334
|
+
await capturedDebounceFns[0]();
|
|
335
|
+
// Should log a summary with pushed/total counts and skipped files
|
|
336
|
+
var allCalls = [
|
|
337
|
+
...Logger_1.logger.info.mock.calls,
|
|
338
|
+
...Logger_1.logger.success.mock.calls,
|
|
339
|
+
].map(function (c) { return c[0]; });
|
|
340
|
+
var summaryCall = allCalls.find(function (msg) {
|
|
341
|
+
return typeof msg === "string" && msg.indexOf("Pushed") !== -1 && msg.indexOf("files to") !== -1;
|
|
342
|
+
});
|
|
343
|
+
expect(summaryCall).toBeDefined();
|
|
344
|
+
expect(summaryCall).toContain("1/2");
|
|
345
|
+
expect(summaryCall).toContain("skipped");
|
|
346
|
+
});
|
|
347
|
+
it("allows file through when scope matches watcher scope", async () => {
|
|
348
|
+
const ctx = makeFileContext({ scope: "x_test_core" });
|
|
349
|
+
FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ context: ctx });
|
|
350
|
+
appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
|
|
351
|
+
appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
|
|
352
|
+
mockWatchers[0]._emit("change", ctx.filePath);
|
|
353
|
+
await capturedDebounceFns[0]();
|
|
354
|
+
expect(appUtils_1.pushFiles).toHaveBeenCalled();
|
|
355
|
+
expect(Logger_1.logger.error).not.toHaveBeenCalledWith(expect.stringContaining("Scope mismatch"));
|
|
356
|
+
});
|
|
357
|
+
it("skips file with error when scope mismatches watcher scope", async () => {
|
|
358
|
+
const ctx = makeFileContext({ scope: "x_other_scope" });
|
|
359
|
+
FileUtils_1.getFileContextWithSkipReason.mockReturnValue({ context: ctx });
|
|
360
|
+
mockWatchers[0]._emit("change", ctx.filePath);
|
|
361
|
+
await capturedDebounceFns[0]();
|
|
362
|
+
// Should log an error about scope mismatch
|
|
363
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("Scope mismatch"));
|
|
364
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("x_other_scope"));
|
|
365
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("x_test_core"));
|
|
366
|
+
// Should not push the file
|
|
367
|
+
expect(appUtils_1.pushFiles).not.toHaveBeenCalled();
|
|
368
|
+
// Should appear in the summary as skipped
|
|
369
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("No valid file contexts found"));
|
|
370
|
+
});
|
|
371
|
+
it("includes scope-mismatched file in push summary alongside valid files", async () => {
|
|
372
|
+
const validCtx = makeFileContext({ scope: "x_test_core", filePath: "/project/src/x_test_core/sys_script_include/Valid/script.js" });
|
|
373
|
+
const mismatchCtx = makeFileContext({ scope: "x_other_scope", filePath: "/project/src/x_test_core/sys_script_include/Wrong/script.js" });
|
|
374
|
+
FileUtils_1.getFileContextWithSkipReason
|
|
375
|
+
.mockReturnValueOnce({ context: validCtx })
|
|
376
|
+
.mockReturnValueOnce({ context: mismatchCtx });
|
|
377
|
+
appUtils_1.groupAppFiles.mockReturnValue([{ table: "sys_script_include", sysId: "abc123", fields: {} }]);
|
|
378
|
+
appUtils_1.pushFiles.mockResolvedValue([{ success: true, message: "ok" }]);
|
|
379
|
+
mockWatchers[0]._emit("change", validCtx.filePath);
|
|
380
|
+
mockWatchers[0]._emit("change", mismatchCtx.filePath);
|
|
381
|
+
await capturedDebounceFns[0]();
|
|
382
|
+
// Valid file should be pushed
|
|
383
|
+
expect(appUtils_1.pushFiles).toHaveBeenCalled();
|
|
384
|
+
// Mismatch should be logged as error
|
|
385
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("Scope mismatch"));
|
|
386
|
+
// Push summary should include the skipped file
|
|
387
|
+
var allCalls = [
|
|
388
|
+
...Logger_1.logger.info.mock.calls,
|
|
389
|
+
...Logger_1.logger.success.mock.calls,
|
|
390
|
+
].map(function (c) { return c[0]; });
|
|
391
|
+
var summaryCall = allCalls.find(function (msg) {
|
|
392
|
+
return typeof msg === "string" && msg.indexOf("Pushed") !== -1 && msg.indexOf("files to") !== -1;
|
|
393
|
+
});
|
|
394
|
+
expect(summaryCall).toBeDefined();
|
|
395
|
+
expect(summaryCall).toContain("1/2");
|
|
396
|
+
expect(summaryCall).toContain("skipped");
|
|
397
|
+
});
|
|
398
|
+
it("logs error on watcher error event", () => {
|
|
399
|
+
mockWatchers[0]._emit("error", new Error("FS error"));
|
|
400
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("Watcher error: FS error"));
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
describe("processScopeQueue - empty queue", () => {
|
|
404
|
+
it("returns early when queue is empty", async () => {
|
|
405
|
+
ConfigManager.getConfig.mockReturnValue({
|
|
406
|
+
...MOCK_CONFIG_TWO_SCOPES,
|
|
407
|
+
scopes: { x_test_core: {} },
|
|
408
|
+
});
|
|
409
|
+
fs_1.default.existsSync.mockReturnValue(false);
|
|
410
|
+
await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
|
|
411
|
+
// Don't emit any events, just trigger processQueue
|
|
412
|
+
await capturedDebounceFns[0]();
|
|
413
|
+
// switchToScope should NOT have been called
|
|
414
|
+
expect(mockSNClient.getScopeId).not.toHaveBeenCalled();
|
|
415
|
+
expect(FileUtils_1.getFileContextFromPath).not.toHaveBeenCalled();
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
describe("switchToScope", () => {
|
|
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 () => {
|
|
424
|
+
await MultiScopeWatcher_1.multiScopeWatcher.switchToScope("x_test_core");
|
|
425
|
+
expect(mockSNClient.changeScope).toHaveBeenCalledWith("x_test_core");
|
|
426
|
+
});
|
|
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");
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
describe("loadScopeManifest", () => {
|
|
433
|
+
it("loads scope-specific manifest when it exists", async () => {
|
|
434
|
+
const manifest = { tables: {}, scope: "x_test_core" };
|
|
435
|
+
fs_1.default.existsSync.mockImplementation((p) => p.includes("dove.manifest.x_test_core.json"));
|
|
436
|
+
fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(manifest));
|
|
437
|
+
await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
|
|
438
|
+
expect(ConfigManager.updateManifest).toHaveBeenCalledWith(manifest);
|
|
439
|
+
});
|
|
440
|
+
it("sets scope field on manifest if missing", async () => {
|
|
441
|
+
const manifest = { tables: {} };
|
|
442
|
+
fs_1.default.existsSync.mockImplementation((p) => p.includes("dove.manifest.x_test_core.json"));
|
|
443
|
+
fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(manifest));
|
|
444
|
+
await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
|
|
445
|
+
expect(ConfigManager.updateManifest).toHaveBeenCalledWith(expect.objectContaining({ scope: "x_test_core" }));
|
|
446
|
+
});
|
|
447
|
+
it("falls back to legacy multi-scope manifest", async () => {
|
|
448
|
+
const legacyManifest = {
|
|
449
|
+
x_test_core: { tables: { sys_script_include: {} } },
|
|
450
|
+
x_test_work: { tables: {} },
|
|
451
|
+
};
|
|
452
|
+
// Scope-specific file doesn't exist, legacy does
|
|
453
|
+
fs_1.default.existsSync.mockImplementation((p) => {
|
|
454
|
+
if (p.includes("dove.manifest.x_test_core.json"))
|
|
455
|
+
return false;
|
|
456
|
+
if (p.includes("dove.manifest.json"))
|
|
457
|
+
return true;
|
|
458
|
+
return false;
|
|
459
|
+
});
|
|
460
|
+
fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(legacyManifest));
|
|
461
|
+
await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
|
|
462
|
+
expect(ConfigManager.updateManifest).toHaveBeenCalledWith(expect.objectContaining({ scope: "x_test_core", tables: { sys_script_include: {} } }));
|
|
463
|
+
});
|
|
464
|
+
it("loads single-scope manifest matching the scope", async () => {
|
|
465
|
+
const manifest = { tables: {}, scope: "x_test_core" };
|
|
466
|
+
fs_1.default.existsSync.mockImplementation((p) => {
|
|
467
|
+
if (p.includes("dove.manifest.x_test_core.json"))
|
|
468
|
+
return false;
|
|
469
|
+
if (p.includes("dove.manifest.json"))
|
|
470
|
+
return true;
|
|
471
|
+
return false;
|
|
472
|
+
});
|
|
473
|
+
fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(manifest));
|
|
474
|
+
await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
|
|
475
|
+
expect(ConfigManager.updateManifest).toHaveBeenCalledWith(manifest);
|
|
476
|
+
});
|
|
477
|
+
it("handles legacy format with tables but no scope field", async () => {
|
|
478
|
+
const manifest = { tables: { sys_script_include: {} } };
|
|
479
|
+
fs_1.default.existsSync.mockImplementation((p) => {
|
|
480
|
+
if (p.includes("dove.manifest.x_test_core.json"))
|
|
481
|
+
return false;
|
|
482
|
+
if (p.includes("dove.manifest.json"))
|
|
483
|
+
return true;
|
|
484
|
+
return false;
|
|
485
|
+
});
|
|
486
|
+
fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(manifest));
|
|
487
|
+
await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
|
|
488
|
+
expect(ConfigManager.updateManifest).toHaveBeenCalledWith(expect.objectContaining({ scope: "x_test_core", tables: { sys_script_include: {} } }));
|
|
489
|
+
});
|
|
490
|
+
it("warns when scope not found in legacy manifest", async () => {
|
|
491
|
+
const manifest = { other_scope: { tables: {} } };
|
|
492
|
+
fs_1.default.existsSync.mockImplementation((p) => {
|
|
493
|
+
if (p.includes("dove.manifest.x_test_core.json"))
|
|
494
|
+
return false;
|
|
495
|
+
if (p.includes("dove.manifest.json"))
|
|
496
|
+
return true;
|
|
497
|
+
return false;
|
|
498
|
+
});
|
|
499
|
+
fs_1.default.promises.readFile.mockResolvedValue(JSON.stringify(manifest));
|
|
500
|
+
await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
|
|
501
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("Scope not found in manifest"));
|
|
502
|
+
});
|
|
503
|
+
it("warns when no manifest files exist", async () => {
|
|
504
|
+
fs_1.default.existsSync.mockReturnValue(false);
|
|
505
|
+
await MultiScopeWatcher_1.multiScopeWatcher.loadScopeManifest("x_test_core", "/project/src/x_test_core");
|
|
506
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("No manifest found"));
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
describe("stopWatching", () => {
|
|
510
|
+
it("closes all scope watchers", async () => {
|
|
511
|
+
ConfigManager.getConfig.mockReturnValue(MOCK_CONFIG_TWO_SCOPES);
|
|
512
|
+
await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
|
|
513
|
+
expect(mockWatchers.length).toBe(2);
|
|
514
|
+
(0, MultiScopeWatcher_1.stopMultiScopeWatching)();
|
|
515
|
+
mockWatchers.forEach((w) => {
|
|
516
|
+
expect(w.close).toHaveBeenCalled();
|
|
517
|
+
});
|
|
518
|
+
});
|
|
519
|
+
it("logs stop message", async () => {
|
|
520
|
+
ConfigManager.getConfig.mockReturnValue({
|
|
521
|
+
...MOCK_CONFIG_TWO_SCOPES,
|
|
522
|
+
scopes: { x_test: {} },
|
|
523
|
+
});
|
|
524
|
+
await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
|
|
525
|
+
jest.clearAllMocks();
|
|
526
|
+
(0, MultiScopeWatcher_1.stopMultiScopeWatching)();
|
|
527
|
+
expect(Logger_1.logger.info).toHaveBeenCalledWith(expect.stringContaining("Stopping watcher"));
|
|
528
|
+
expect(Logger_1.logger.info).toHaveBeenCalledWith("All watchers stopped");
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
describe("update set monitoring", () => {
|
|
532
|
+
it("sets up interval with default 120s when no options provided", async () => {
|
|
533
|
+
const setIntervalSpy = jest.spyOn(global, "setInterval");
|
|
534
|
+
ConfigManager.getConfig.mockReturnValue({
|
|
535
|
+
...MOCK_CONFIG_TWO_SCOPES,
|
|
536
|
+
scopes: { x_test: {} },
|
|
537
|
+
});
|
|
538
|
+
await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
|
|
539
|
+
// startUpdateSetMonitoring runs async — wait for it to set the interval
|
|
540
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
541
|
+
expect(setIntervalSpy).toHaveBeenCalledWith(expect.any(Function), 120000);
|
|
542
|
+
setIntervalSpy.mockRestore();
|
|
543
|
+
});
|
|
544
|
+
it("clears interval on stopWatching", async () => {
|
|
545
|
+
const clearIntervalSpy = jest.spyOn(global, "clearInterval");
|
|
546
|
+
ConfigManager.getConfig.mockReturnValue({
|
|
547
|
+
...MOCK_CONFIG_TWO_SCOPES,
|
|
548
|
+
scopes: { x_test: {} },
|
|
549
|
+
});
|
|
550
|
+
await (0, MultiScopeWatcher_1.startMultiScopeWatching)();
|
|
551
|
+
// Wait for startUpdateSetMonitoring to complete and set the interval
|
|
552
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
553
|
+
(0, MultiScopeWatcher_1.stopMultiScopeWatching)();
|
|
554
|
+
expect(clearIntervalSpy).toHaveBeenCalled();
|
|
555
|
+
clearIntervalSpy.mockRestore();
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const corePlugin_1 = require("../initSystem/corePlugin");
|
|
4
|
+
describe("normalizeInstance", function () {
|
|
5
|
+
it("strips https:// prefix", function () {
|
|
6
|
+
expect((0, corePlugin_1.normalizeInstance)("https://mycompany.service-now.com")).toBe("mycompany.service-now.com");
|
|
7
|
+
});
|
|
8
|
+
it("strips http:// prefix", function () {
|
|
9
|
+
expect((0, corePlugin_1.normalizeInstance)("http://mycompany.service-now.com")).toBe("mycompany.service-now.com");
|
|
10
|
+
});
|
|
11
|
+
it("returns bare hostname without trailing slash", function () {
|
|
12
|
+
expect((0, corePlugin_1.normalizeInstance)("mycompany.service-now.com")).toBe("mycompany.service-now.com");
|
|
13
|
+
});
|
|
14
|
+
it("strips trailing slash if present", function () {
|
|
15
|
+
expect((0, corePlugin_1.normalizeInstance)("mycompany.service-now.com/")).toBe("mycompany.service-now.com");
|
|
16
|
+
});
|
|
17
|
+
it("trims whitespace", function () {
|
|
18
|
+
expect((0, corePlugin_1.normalizeInstance)(" mycompany.service-now.com ")).toBe("mycompany.service-now.com");
|
|
19
|
+
});
|
|
20
|
+
it("handles full URL with protocol and trailing slash", function () {
|
|
21
|
+
expect((0, corePlugin_1.normalizeInstance)("https://mycompany.service-now.com/")).toBe("mycompany.service-now.com");
|
|
22
|
+
});
|
|
23
|
+
});
|