@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,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
|
+
});
|