@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.
Files changed (78) hide show
  1. package/README.md +6 -0
  2. package/dist/FileLogger.js +207 -0
  3. package/dist/FileUtils.js +387 -0
  4. package/dist/Logger.js +88 -0
  5. package/dist/MultiScopeWatcher.js +615 -0
  6. package/dist/PluginManager.js +125 -0
  7. package/dist/_defaultOptionsOLD.js +44 -0
  8. package/dist/allScopesCommands.js +418 -0
  9. package/dist/appUtils.js +929 -0
  10. package/dist/benchmark.js +104 -0
  11. package/dist/bootstrap.js +62 -0
  12. package/dist/claudeCommand.js +60 -0
  13. package/dist/clickupCommands.js +472 -0
  14. package/dist/clickupPushHelper.js +33 -0
  15. package/dist/commander.js +466 -0
  16. package/dist/commands.js +378 -0
  17. package/dist/config.js +697 -0
  18. package/dist/constants.js +15 -0
  19. package/dist/createRecordCommand.js +314 -0
  20. package/dist/dashboardCommand.js +38 -0
  21. package/dist/defaultOptions.js +14 -0
  22. package/dist/deleteRecordCommand.js +282 -0
  23. package/dist/flowDesigner/values.js +55 -0
  24. package/dist/genericUtils.js +189 -0
  25. package/dist/gitUtils.js +99 -0
  26. package/dist/index.js +35 -0
  27. package/dist/initSystem/corePlugin.js +344 -0
  28. package/dist/initSystem/discovery.js +107 -0
  29. package/dist/initSystem/orchestrator.js +408 -0
  30. package/dist/logMessages.js +82 -0
  31. package/dist/loginCommand.js +20 -0
  32. package/dist/migrateCommand.js +123 -0
  33. package/dist/projectFiles.js +32 -0
  34. package/dist/recentEdits.js +66 -0
  35. package/dist/schemaCommand.js +85 -0
  36. package/dist/snClient.js +519 -0
  37. package/dist/tests/benchmarkRefresh.test.js +265 -0
  38. package/dist/tests/clickupCommands.test.js +384 -0
  39. package/dist/tests/clickupPushHelper.test.js +124 -0
  40. package/dist/tests/configFallback.test.js +68 -0
  41. package/dist/tests/cred.test.js +59 -0
  42. package/dist/tests/discovery.test.js +67 -0
  43. package/dist/tests/ensureUpdateSetWarnings.test.js +218 -0
  44. package/dist/tests/errorLogLevels.test.js +273 -0
  45. package/dist/tests/example.test.js +6 -0
  46. package/dist/tests/fileContextSkipReason.test.js +116 -0
  47. package/dist/tests/globalDebounce.test.js +308 -0
  48. package/dist/tests/migrateCommand.test.js +113 -0
  49. package/dist/tests/multi-scope-watcher.test.js +558 -0
  50. package/dist/tests/orchestrator.test.js +23 -0
  51. package/dist/tests/pushFiles.test.js +248 -0
  52. package/dist/tests/rateLimitCoordination.test.js +271 -0
  53. package/dist/tests/resolveConfigReadOnly.test.js +67 -0
  54. package/dist/tests/retryOnHttpErr.test.js +154 -0
  55. package/dist/tests/scopeCaching.test.js +115 -0
  56. package/dist/tests/serializeUpdateSetConfig.test.js +325 -0
  57. package/dist/tests/snClient.fallback.test.js +120 -0
  58. package/dist/tests/syncManifestRefresh.test.js +287 -0
  59. package/dist/tests/syncManifestWhitelist.test.js +192 -0
  60. package/dist/tests/taskClear.test.js +170 -0
  61. package/dist/tests/taskStaleness.test.js +220 -0
  62. package/dist/tests/unwrapSNResponseError.test.js +145 -0
  63. package/dist/tests/v2Values.test.js +79 -0
  64. package/dist/tests/validateTaskId.test.js +304 -0
  65. package/dist/tests/verifyUpdateSetSwitch.test.js +277 -0
  66. package/dist/tests/writeEnvVar.test.js +119 -0
  67. package/dist/updateSetCommands.js +702 -0
  68. package/dist/wizard.js +40 -0
  69. package/package.json +69 -0
  70. package/skills/dove-configure-pipeline.md +158 -0
  71. package/skills/dove-create-plugin.md +174 -0
  72. package/skills/dove-create-record.md +135 -0
  73. package/skills/dove-debug-build.md +117 -0
  74. package/skills/dove-delete-record.md +104 -0
  75. package/skills/dove-manage-tables.md +174 -0
  76. package/skills/dove-manage-update-sets.md +88 -0
  77. package/skills/dove-setup-project.md +154 -0
  78. package/skills/dove-troubleshoot-sync.md +113 -0
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ // Tests for the /api/cadso/dovetail/* -> /api/cadso/claude/* 404 fallback
3
+ // in _callDovetailApi (snClient.ts). Covers: first call hits the dovetail
4
+ // endpoint, 404 latches the legacy path with a one-time warning, subsequent
5
+ // calls skip dovetail, and non-404 errors do NOT trigger fallback.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ var mockLogger = {
8
+ info: jest.fn(),
9
+ debug: jest.fn(),
10
+ warn: jest.fn(),
11
+ error: jest.fn(),
12
+ getLogLevel: function () { return "debug"; },
13
+ };
14
+ jest.mock("../Logger", function () {
15
+ return { logger: mockLogger };
16
+ });
17
+ jest.mock("../FileLogger", function () {
18
+ return { fileLogger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() } };
19
+ });
20
+ jest.mock("../genericUtils", function () {
21
+ return { wait: jest.fn().mockResolvedValue(undefined) };
22
+ });
23
+ const snClient_1 = require("../snClient");
24
+ function makeAxiosError(status) {
25
+ var error = new Error("Request failed with status " + status);
26
+ error.isAxiosError = true;
27
+ error.response = { status: status, headers: {}, data: {} };
28
+ return error;
29
+ }
30
+ function makeAxiosResponse(data) {
31
+ return { status: 200, data: data, headers: {}, statusText: "OK", config: {} };
32
+ }
33
+ describe("_callDovetailApi 404 fallback", function () {
34
+ beforeEach(function () {
35
+ (0, snClient_1._resetDovetailApiFallback)();
36
+ jest.clearAllMocks();
37
+ });
38
+ it("first request hits /api/cadso/dovetail/<op>", async function () {
39
+ var seen = [];
40
+ var call = jest.fn(async function (endpoint) {
41
+ seen.push(endpoint);
42
+ return makeAxiosResponse({ ok: true });
43
+ });
44
+ await (0, snClient_1._callDovetailApi)("changeScope", call);
45
+ expect(seen).toEqual(["api/cadso/dovetail/changeScope"]);
46
+ expect((0, snClient_1._isUsingLegacyClaudePath)()).toBe(false);
47
+ });
48
+ it("on 404, retries against legacy /api/cadso/claude/<op> and warns once", async function () {
49
+ var seen = [];
50
+ var call = jest.fn(async function (endpoint) {
51
+ seen.push(endpoint);
52
+ if (endpoint.indexOf("api/cadso/dovetail/") === 0) {
53
+ throw makeAxiosError(404);
54
+ }
55
+ return makeAxiosResponse({ ok: true });
56
+ });
57
+ var result = await (0, snClient_1._callDovetailApi)("changeScope", call);
58
+ expect(result.data).toEqual({ ok: true });
59
+ expect(seen).toEqual([
60
+ "api/cadso/dovetail/changeScope",
61
+ "api/cadso/claude/changeScope",
62
+ ]);
63
+ expect((0, snClient_1._isUsingLegacyClaudePath)()).toBe(true);
64
+ expect(mockLogger.warn).toHaveBeenCalledTimes(1);
65
+ expect(mockLogger.warn.mock.calls[0][0]).toContain("[deprecation]");
66
+ expect(mockLogger.warn.mock.calls[0][0]).toContain("/api/cadso/dovetail/changeScope");
67
+ expect(mockLogger.warn.mock.calls[0][0]).toContain("/api/cadso/claude/changeScope");
68
+ });
69
+ it("after latching, subsequent calls skip dovetail and warn no more", async function () {
70
+ var seen = [];
71
+ var call = jest.fn(async function (endpoint) {
72
+ seen.push(endpoint);
73
+ if (endpoint.indexOf("api/cadso/dovetail/") === 0) {
74
+ throw makeAxiosError(404);
75
+ }
76
+ return makeAxiosResponse({ ok: true });
77
+ });
78
+ await (0, snClient_1._callDovetailApi)("currentUpdateSet", call);
79
+ seen.length = 0;
80
+ mockLogger.warn.mockClear();
81
+ await (0, snClient_1._callDovetailApi)("changeScope", call);
82
+ await (0, snClient_1._callDovetailApi)("changeUpdateSet", call);
83
+ expect(seen).toEqual([
84
+ "api/cadso/claude/changeScope",
85
+ "api/cadso/claude/changeUpdateSet",
86
+ ]);
87
+ expect(mockLogger.warn).not.toHaveBeenCalled();
88
+ });
89
+ it("does NOT fall back on 401 (auth failure)", async function () {
90
+ var call = jest.fn(async function (_endpoint) {
91
+ throw makeAxiosError(401);
92
+ });
93
+ await expect((0, snClient_1._callDovetailApi)("changeScope", call)).rejects.toMatchObject({
94
+ response: { status: 401 },
95
+ });
96
+ expect(call).toHaveBeenCalledTimes(1);
97
+ expect((0, snClient_1._isUsingLegacyClaudePath)()).toBe(false);
98
+ expect(mockLogger.warn).not.toHaveBeenCalled();
99
+ });
100
+ it("does NOT fall back on 500 (server error)", async function () {
101
+ var call = jest.fn(async function (_endpoint) {
102
+ throw makeAxiosError(500);
103
+ });
104
+ await expect((0, snClient_1._callDovetailApi)("changeScope", call)).rejects.toMatchObject({
105
+ response: { status: 500 },
106
+ });
107
+ expect(call).toHaveBeenCalledTimes(1);
108
+ expect((0, snClient_1._isUsingLegacyClaudePath)()).toBe(false);
109
+ expect(mockLogger.warn).not.toHaveBeenCalled();
110
+ });
111
+ it("does NOT fall back on a network error with no response", async function () {
112
+ var call = jest.fn(async function (_endpoint) {
113
+ throw new Error("ECONNREFUSED");
114
+ });
115
+ await expect((0, snClient_1._callDovetailApi)("changeScope", call)).rejects.toThrow("ECONNREFUSED");
116
+ expect(call).toHaveBeenCalledTimes(1);
117
+ expect((0, snClient_1._isUsingLegacyClaudePath)()).toBe(false);
118
+ expect(mockLogger.warn).not.toHaveBeenCalled();
119
+ });
120
+ });
@@ -0,0 +1,287 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for the refresh content-compare path.
4
+ *
5
+ * Regression target: `npx dove refresh` previously only downloaded files that
6
+ * were absent from disk. Files edited on the ServiceNow instance would never
7
+ * propagate to a developer's local copy — the refresh command silently
8
+ * skipped existing files without checking content.
9
+ *
10
+ * These tests assert:
11
+ * 1. refresh fetches content for EVERY file in the manifest (not just missing)
12
+ * 2. Files whose local content differs are overwritten
13
+ * 3. Files whose local content matches are left alone (no rewrite)
14
+ * 4. Files missing locally are created
15
+ * 5. --force bypasses the content check and writes all files unconditionally
16
+ */
17
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ var desc = Object.getOwnPropertyDescriptor(m, k);
20
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
21
+ desc = { enumerable: true, get: function() { return m[k]; } };
22
+ }
23
+ Object.defineProperty(o, k2, desc);
24
+ }) : (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ o[k2] = m[k];
27
+ }));
28
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
29
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
30
+ }) : function(o, v) {
31
+ o["default"] = v;
32
+ });
33
+ var __importStar = (this && this.__importStar) || (function () {
34
+ var ownKeys = function(o) {
35
+ ownKeys = Object.getOwnPropertyNames || function (o) {
36
+ var ar = [];
37
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
38
+ return ar;
39
+ };
40
+ return ownKeys(o);
41
+ };
42
+ return function (mod) {
43
+ if (mod && mod.__esModule) return mod;
44
+ var result = {};
45
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
46
+ __setModuleDefault(result, mod);
47
+ return result;
48
+ };
49
+ })();
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ const fs = __importStar(require("fs"));
52
+ const os = __importStar(require("os"));
53
+ const path = __importStar(require("path"));
54
+ // ---------- mocks (must come before importing the module under test) ----------
55
+ var mockLogger = {
56
+ info: jest.fn(),
57
+ debug: jest.fn(),
58
+ warn: jest.fn(),
59
+ error: jest.fn(),
60
+ success: jest.fn(),
61
+ getLogLevel: function () { return "warn"; },
62
+ };
63
+ var mockFileLogger = {
64
+ debug: jest.fn(),
65
+ info: jest.fn(),
66
+ warn: jest.fn(),
67
+ error: jest.fn(),
68
+ };
69
+ var mockClient = {
70
+ getManifest: jest.fn(),
71
+ getMissingFiles: jest.fn(),
72
+ };
73
+ jest.mock("../Logger", function () { return { logger: mockLogger }; });
74
+ jest.mock("../FileLogger", function () { return { fileLogger: mockFileLogger }; });
75
+ jest.mock("../snClient", function () {
76
+ return {
77
+ defaultClient: function () { return mockClient; },
78
+ unwrapSNResponse: function (p) { return Promise.resolve(p).then(function (r) { return r; }); },
79
+ processPushResponse: jest.fn(),
80
+ retryOnErr: jest.fn(),
81
+ retryOnHttpErr: jest.fn(),
82
+ unwrapTableAPIFirstItem: jest.fn(),
83
+ };
84
+ });
85
+ var mockConfig = {
86
+ getConfig: jest.fn().mockReturnValue({
87
+ scopes: { x_cadso_core: {} },
88
+ tableOptions: {},
89
+ }),
90
+ getManifest: jest.fn().mockResolvedValue({
91
+ x_cadso_core: { scope: "x_cadso_core", tables: {} },
92
+ }),
93
+ getSourcePathForScope: jest.fn(),
94
+ getSourcePath: jest.fn(),
95
+ getManifestPath: jest.fn().mockReturnValue("/tmp/dove.manifest.json"),
96
+ resolveConfigForScope: jest.fn().mockImplementation(function () {
97
+ return {
98
+ tables: ["sys_script_include"],
99
+ fieldOverrides: {},
100
+ apiIncludes: {},
101
+ apiExcludes: {},
102
+ };
103
+ }),
104
+ isMultiScopeManifest: jest.fn().mockReturnValue(true),
105
+ updateManifest: jest.fn(),
106
+ };
107
+ jest.mock("../config", function () { return mockConfig; });
108
+ // Silence ProgressBar's stdout writes.
109
+ jest.mock("progress", function () {
110
+ return jest.fn().mockImplementation(function () {
111
+ return { tick: jest.fn() };
112
+ });
113
+ });
114
+ // Stub writeScopeManifest so we don't actually write a manifest file. Leave
115
+ // the other FileUtils exports (writeSNFileCurry, writeSNFileIfDifferent,
116
+ // createDirRecursively, etc.) as real implementations so we can assert
117
+ // against the file system.
118
+ jest.mock("../FileUtils", function () {
119
+ var actual = jest.requireActual("../FileUtils");
120
+ return Object.assign({}, actual, {
121
+ writeScopeManifest: jest.fn().mockResolvedValue(undefined),
122
+ });
123
+ });
124
+ const AppUtils = __importStar(require("../appUtils"));
125
+ // ---------- tmp dir scaffolding ----------
126
+ var tmpRoot;
127
+ function setupScope(files) {
128
+ // Build a manifest whose single table (sys_script_include) contains the
129
+ // supplied records/files. Content is stripped (manifests do not carry it).
130
+ var records = {};
131
+ files.forEach(function (f) {
132
+ if (!records[f.record]) {
133
+ records[f.record] = { name: f.record, sys_id: "sysid_" + f.record, files: [] };
134
+ }
135
+ records[f.record].files.push({ name: f.name, type: f.type });
136
+ });
137
+ return {
138
+ scope: "x_cadso_core",
139
+ tables: {
140
+ sys_script_include: { records: records },
141
+ },
142
+ };
143
+ }
144
+ function writeLocal(record, name, type, content) {
145
+ var recDir = path.join(tmpRoot, "sys_script_include", record);
146
+ fs.mkdirSync(recDir, { recursive: true });
147
+ fs.writeFileSync(path.join(recDir, name + "." + type), content);
148
+ }
149
+ function readLocal(record, name, type) {
150
+ var p = path.join(tmpRoot, "sys_script_include", record, name + "." + type);
151
+ try {
152
+ return fs.readFileSync(p, "utf8");
153
+ }
154
+ catch (e) {
155
+ return null;
156
+ }
157
+ }
158
+ function statMtime(record, name, type) {
159
+ var p = path.join(tmpRoot, "sys_script_include", record, name + "." + type);
160
+ return fs.statSync(p).mtimeMs;
161
+ }
162
+ describe("syncManifest — refresh pulls instance edits down", function () {
163
+ beforeEach(function () {
164
+ jest.clearAllMocks();
165
+ tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "sinc-refresh-test-"));
166
+ mockConfig.getSourcePathForScope.mockReturnValue(tmpRoot);
167
+ mockConfig.getSourcePath.mockReturnValue(tmpRoot);
168
+ });
169
+ afterEach(function () {
170
+ try {
171
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
172
+ }
173
+ catch (e) { }
174
+ });
175
+ test("overwrites existing file when instance content differs", async function () {
176
+ writeLocal("Foo", "script", "js", "var x = 1;"); // stale local
177
+ var manifest = setupScope([{ record: "Foo", name: "script", type: "js", content: "" }]);
178
+ mockClient.getManifest.mockResolvedValue(manifest);
179
+ mockClient.getMissingFiles.mockResolvedValue({
180
+ sys_script_include: {
181
+ records: {
182
+ Foo: {
183
+ name: "Foo",
184
+ sys_id: "sysid_Foo",
185
+ files: [{ name: "script", type: "js", content: "var x = 2; // from instance" }],
186
+ },
187
+ },
188
+ },
189
+ });
190
+ await AppUtils.syncManifest("x_cadso_core");
191
+ expect(mockClient.getMissingFiles).toHaveBeenCalledTimes(1);
192
+ expect(readLocal("Foo", "script", "js")).toBe("var x = 2; // from instance");
193
+ });
194
+ test("does not rewrite a file when content already matches", async function () {
195
+ var identical = "var answer = 42;";
196
+ writeLocal("Bar", "script", "js", identical);
197
+ var beforeMtime = statMtime("Bar", "script", "js");
198
+ var manifest = setupScope([{ record: "Bar", name: "script", type: "js", content: "" }]);
199
+ mockClient.getManifest.mockResolvedValue(manifest);
200
+ mockClient.getMissingFiles.mockResolvedValue({
201
+ sys_script_include: {
202
+ records: {
203
+ Bar: {
204
+ name: "Bar",
205
+ sys_id: "sysid_Bar",
206
+ files: [{ name: "script", type: "js", content: identical }],
207
+ },
208
+ },
209
+ },
210
+ });
211
+ // Give the fs a moment so any write would register a different mtime.
212
+ await new Promise(function (r) { setTimeout(r, 10); });
213
+ await AppUtils.syncManifest("x_cadso_core");
214
+ expect(readLocal("Bar", "script", "js")).toBe(identical);
215
+ expect(statMtime("Bar", "script", "js")).toBe(beforeMtime);
216
+ // metaData should NOT be written when nothing changed in the record
217
+ expect(readLocal("Bar", "metaData", "json")).toBeNull();
218
+ });
219
+ test("creates the file when it is missing locally", async function () {
220
+ var manifest = setupScope([{ record: "Baz", name: "script", type: "js", content: "" }]);
221
+ mockClient.getManifest.mockResolvedValue(manifest);
222
+ mockClient.getMissingFiles.mockResolvedValue({
223
+ sys_script_include: {
224
+ records: {
225
+ Baz: {
226
+ name: "Baz",
227
+ sys_id: "sysid_Baz",
228
+ files: [{ name: "script", type: "js", content: "// fresh file" }],
229
+ },
230
+ },
231
+ },
232
+ });
233
+ await AppUtils.syncManifest("x_cadso_core");
234
+ expect(readLocal("Baz", "script", "js")).toBe("// fresh file");
235
+ expect(readLocal("Baz", "metaData", "json")).not.toBeNull();
236
+ });
237
+ test("--force overwrites even when local content already matches", async function () {
238
+ var identical = "var same = true;";
239
+ writeLocal("Qux", "script", "js", identical);
240
+ var manifest = setupScope([{ record: "Qux", name: "script", type: "js", content: "" }]);
241
+ mockClient.getManifest.mockResolvedValue(manifest);
242
+ mockClient.getMissingFiles.mockResolvedValue({
243
+ sys_script_include: {
244
+ records: {
245
+ Qux: {
246
+ name: "Qux",
247
+ sys_id: "sysid_Qux",
248
+ files: [{ name: "script", type: "js", content: identical }],
249
+ },
250
+ },
251
+ },
252
+ });
253
+ var beforeMtime = statMtime("Qux", "script", "js");
254
+ await new Promise(function (r) { setTimeout(r, 10); });
255
+ await AppUtils.syncManifest("x_cadso_core", { force: true });
256
+ // Force path rewrites regardless — mtime should advance.
257
+ expect(statMtime("Qux", "script", "js")).toBeGreaterThan(beforeMtime);
258
+ expect(readLocal("Qux", "metaData", "json")).not.toBeNull();
259
+ });
260
+ test("bulkDownload is requested for EVERY file in the manifest, not just missing ones", async function () {
261
+ writeLocal("RecA", "script", "js", "local A");
262
+ writeLocal("RecB", "script", "js", "local B");
263
+ var manifest = setupScope([
264
+ { record: "RecA", name: "script", type: "js", content: "" },
265
+ { record: "RecB", name: "script", type: "js", content: "" },
266
+ ]);
267
+ mockClient.getManifest.mockResolvedValue(manifest);
268
+ mockClient.getMissingFiles.mockResolvedValue({
269
+ sys_script_include: {
270
+ records: {
271
+ RecA: {
272
+ name: "RecA", sys_id: "sysid_RecA",
273
+ files: [{ name: "script", type: "js", content: "local A" }],
274
+ },
275
+ RecB: {
276
+ name: "RecB", sys_id: "sysid_RecB",
277
+ files: [{ name: "script", type: "js", content: "local B" }],
278
+ },
279
+ },
280
+ },
281
+ });
282
+ await AppUtils.syncManifest("x_cadso_core");
283
+ expect(mockClient.getMissingFiles).toHaveBeenCalledTimes(1);
284
+ var missingArg = mockClient.getMissingFiles.mock.calls[0][0];
285
+ expect(Object.keys(missingArg.sys_script_include).sort()).toEqual(["sysid_RecA", "sysid_RecB"]);
286
+ });
287
+ });
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for the scope + table whitelist gates in syncManifest() and
4
+ * processTablesInManifest().
5
+ *
6
+ * Regression target: 2026-04-14 sys_alias debris incident.
7
+ * - `npx dove refresh` iterated every scope in the persisted multi-scope
8
+ * manifest, including scopes not declared in dove.config.js.
9
+ * - For each of those scopes, the server-side getManifest response returned
10
+ * hundreds of tables (up to 129 for x_cadso_work) that the config _tables
11
+ * whitelist never authorised, and the client wrote all of them to disk —
12
+ * including sys_alias/sys_alias_templates folders with 314 field files per
13
+ * record (fan-out of every script/html/css/xml field config across tables).
14
+ *
15
+ * These tests assert the two defensive filters:
16
+ * 1. Undeclared scopes are skipped before any REST call.
17
+ * 2. Tables not in the _tables whitelist are filtered from the manifest
18
+ * before writeScopeManifest / processMissingFiles run.
19
+ */
20
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ var desc = Object.getOwnPropertyDescriptor(m, k);
23
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
24
+ desc = { enumerable: true, get: function() { return m[k]; } };
25
+ }
26
+ Object.defineProperty(o, k2, desc);
27
+ }) : (function(o, m, k, k2) {
28
+ if (k2 === undefined) k2 = k;
29
+ o[k2] = m[k];
30
+ }));
31
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
32
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
33
+ }) : function(o, v) {
34
+ o["default"] = v;
35
+ });
36
+ var __importStar = (this && this.__importStar) || (function () {
37
+ var ownKeys = function(o) {
38
+ ownKeys = Object.getOwnPropertyNames || function (o) {
39
+ var ar = [];
40
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
41
+ return ar;
42
+ };
43
+ return ownKeys(o);
44
+ };
45
+ return function (mod) {
46
+ if (mod && mod.__esModule) return mod;
47
+ var result = {};
48
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
49
+ __setModuleDefault(result, mod);
50
+ return result;
51
+ };
52
+ })();
53
+ Object.defineProperty(exports, "__esModule", { value: true });
54
+ var mockLogger = {
55
+ info: jest.fn(),
56
+ debug: jest.fn(),
57
+ warn: jest.fn(),
58
+ error: jest.fn(),
59
+ success: jest.fn(),
60
+ getLogLevel: function () { return "info"; },
61
+ };
62
+ var mockFileLogger = {
63
+ debug: jest.fn(),
64
+ info: jest.fn(),
65
+ warn: jest.fn(),
66
+ error: jest.fn(),
67
+ };
68
+ var mockClient = {
69
+ getManifest: jest.fn(),
70
+ };
71
+ var mockFUtils = {
72
+ writeScopeManifest: jest.fn().mockResolvedValue(undefined),
73
+ writeFileForce: jest.fn().mockResolvedValue(undefined),
74
+ writeSNFileCurry: jest.fn(() => jest.fn().mockResolvedValue(undefined)),
75
+ createDirRecursively: jest.fn().mockResolvedValue(undefined),
76
+ };
77
+ jest.mock("../Logger", function () { return { logger: mockLogger }; });
78
+ jest.mock("../FileLogger", function () { return { fileLogger: mockFileLogger }; });
79
+ jest.mock("../FileUtils", function () { return mockFUtils; });
80
+ jest.mock("../snClient", function () {
81
+ return {
82
+ defaultClient: function () { return mockClient; },
83
+ unwrapSNResponse: function (p) { return Promise.resolve(p).then(function (r) { return r; }); },
84
+ processPushResponse: jest.fn(),
85
+ retryOnErr: jest.fn(),
86
+ retryOnHttpErr: jest.fn(),
87
+ unwrapTableAPIFirstItem: jest.fn(),
88
+ };
89
+ });
90
+ // Mock config module. Individual tests override getConfig/getManifest/etc.
91
+ var mockConfig = {
92
+ getConfig: jest.fn(),
93
+ getManifest: jest.fn(),
94
+ getSourcePathForScope: jest.fn().mockReturnValue("/tmp/src"),
95
+ getSourcePath: jest.fn().mockReturnValue("/tmp/src"),
96
+ getManifestPath: jest.fn().mockReturnValue("/tmp/dove.manifest.json"),
97
+ resolveConfigForScope: jest.fn(),
98
+ isMultiScopeManifest: jest.fn().mockReturnValue(true),
99
+ updateManifest: jest.fn(),
100
+ };
101
+ jest.mock("../config", function () { return mockConfig; });
102
+ // Prevent processMissingFiles' progress bar from touching stdout in tests.
103
+ jest.mock("progress", function () {
104
+ return jest.fn().mockImplementation(function () {
105
+ return { tick: jest.fn() };
106
+ });
107
+ });
108
+ // Keep the per-scope progress shim simple.
109
+ jest.mock("../genericUtils", function () {
110
+ var actual = jest.requireActual("../genericUtils");
111
+ return actual;
112
+ });
113
+ const AppUtils = __importStar(require("../appUtils"));
114
+ describe("syncManifest — scope + table whitelist gates", function () {
115
+ beforeEach(function () {
116
+ jest.clearAllMocks();
117
+ mockConfig.isMultiScopeManifest.mockReturnValue(true);
118
+ mockConfig.resolveConfigForScope.mockImplementation(function (_scope) {
119
+ return {
120
+ tables: ["sys_script_include", "sys_script", "sys_ux_macroponent"],
121
+ fieldOverrides: {},
122
+ apiIncludes: {},
123
+ apiExcludes: {},
124
+ };
125
+ });
126
+ });
127
+ test("skips undeclared scope — no REST call, warn logged", async function () {
128
+ mockConfig.getConfig.mockReturnValue({
129
+ scopes: { x_cadso_core: {}, x_cadso_work: {} },
130
+ });
131
+ mockConfig.getManifest.mockResolvedValue({
132
+ x_cadso_core: { scope: "x_cadso_core", tables: {} },
133
+ x_cadso_click: { scope: "x_cadso_click", tables: {} }, // stale undeclared
134
+ });
135
+ await AppUtils.syncManifest("x_cadso_click");
136
+ expect(mockClient.getManifest).not.toHaveBeenCalled();
137
+ expect(mockFUtils.writeScopeManifest).not.toHaveBeenCalled();
138
+ var warnedAboutScope = mockLogger.warn.mock.calls.some(function (args) {
139
+ return typeof args[0] === "string" && args[0].indexOf("x_cadso_click") !== -1;
140
+ });
141
+ expect(warnedAboutScope).toBe(true);
142
+ });
143
+ test("declared scope — filters non-whitelisted tables before write", async function () {
144
+ mockConfig.getConfig.mockReturnValue({
145
+ scopes: { x_cadso_core: {} },
146
+ });
147
+ mockConfig.getManifest.mockResolvedValue({
148
+ x_cadso_core: { scope: "x_cadso_core", tables: {} },
149
+ });
150
+ // Server returns two whitelisted tables + sys_alias (not whitelisted).
151
+ mockClient.getManifest.mockResolvedValue({
152
+ scope: "x_cadso_core",
153
+ tables: {
154
+ sys_script_include: { records: { FooInclude: { name: "FooInclude", sys_id: "a1", files: [] } } },
155
+ sys_script: { records: { BarBR: { name: "BarBR", sys_id: "a2", files: [] } } },
156
+ sys_alias: { records: { DebrisRec: { name: "DebrisRec", sys_id: "a3", files: [] } } },
157
+ },
158
+ });
159
+ await AppUtils.syncManifest("x_cadso_core");
160
+ expect(mockClient.getManifest).toHaveBeenCalledWith("x_cadso_core", expect.any(Object));
161
+ expect(mockFUtils.writeScopeManifest).toHaveBeenCalledTimes(1);
162
+ var writtenScope = mockFUtils.writeScopeManifest.mock.calls[0][0];
163
+ var writtenManifest = mockFUtils.writeScopeManifest.mock.calls[0][1];
164
+ expect(writtenScope).toBe("x_cadso_core");
165
+ var writtenTables = Object.keys(writtenManifest.tables);
166
+ expect(writtenTables).toContain("sys_script_include");
167
+ expect(writtenTables).toContain("sys_script");
168
+ expect(writtenTables).not.toContain("sys_alias");
169
+ });
170
+ test("no-scope call iterates only declared scopes, even when manifest has stale ones", async function () {
171
+ mockConfig.getConfig.mockReturnValue({
172
+ scopes: { x_cadso_core: {}, x_cadso_work: {} },
173
+ });
174
+ // Persisted multi-scope manifest still carries stale undeclared scopes.
175
+ mockConfig.getManifest.mockResolvedValue({
176
+ x_cadso_core: { scope: "x_cadso_core", tables: {} },
177
+ x_cadso_work: { scope: "x_cadso_work", tables: {} },
178
+ x_cadso_click: { scope: "x_cadso_click", tables: {} },
179
+ x_nuvo_sinc: { scope: "x_nuvo_sinc", tables: {} },
180
+ x_cadso_ti_agile: { scope: "x_cadso_ti_agile", tables: {} },
181
+ });
182
+ mockClient.getManifest.mockImplementation(function (scope) {
183
+ return Promise.resolve({ scope: scope, tables: {} });
184
+ });
185
+ await AppUtils.syncManifest();
186
+ var refreshedScopes = mockClient.getManifest.mock.calls.map(function (c) { return c[0]; });
187
+ expect(refreshedScopes.sort()).toEqual(["x_cadso_core", "x_cadso_work"]);
188
+ expect(refreshedScopes).not.toContain("x_cadso_click");
189
+ expect(refreshedScopes).not.toContain("x_nuvo_sinc");
190
+ expect(refreshedScopes).not.toContain("x_cadso_ti_agile");
191
+ });
192
+ });