@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,170 @@
|
|
|
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
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const commands_1 = require("../commands");
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
// Mock logger
|
|
40
|
+
var logMessages = [];
|
|
41
|
+
jest.mock("../Logger", function () {
|
|
42
|
+
return {
|
|
43
|
+
logger: {
|
|
44
|
+
setLogLevel: jest.fn(),
|
|
45
|
+
success: jest.fn(function (msg) {
|
|
46
|
+
logMessages.push({ level: "success", msg: msg });
|
|
47
|
+
}),
|
|
48
|
+
info: jest.fn(function (msg) {
|
|
49
|
+
logMessages.push({ level: "info", msg: msg });
|
|
50
|
+
}),
|
|
51
|
+
error: jest.fn(function (msg) {
|
|
52
|
+
logMessages.push({ level: "error", msg: msg });
|
|
53
|
+
}),
|
|
54
|
+
warn: jest.fn(function (msg) {
|
|
55
|
+
logMessages.push({ level: "warn", msg: msg });
|
|
56
|
+
}),
|
|
57
|
+
debug: jest.fn(),
|
|
58
|
+
getInternalLogger: jest.fn(function () {
|
|
59
|
+
return { error: jest.fn() };
|
|
60
|
+
}),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
// Mock fs
|
|
65
|
+
jest.mock("fs");
|
|
66
|
+
var mockFs = fs;
|
|
67
|
+
// Mock other imports to prevent side effects
|
|
68
|
+
jest.mock("../config", function () {
|
|
69
|
+
return {
|
|
70
|
+
getConfig: jest.fn(function () { return {}; }),
|
|
71
|
+
loadConfigs: jest.fn(),
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
jest.mock("../appUtils", function () { return {}; });
|
|
75
|
+
jest.mock("../snClient", function () {
|
|
76
|
+
return {
|
|
77
|
+
defaultClient: jest.fn(),
|
|
78
|
+
unwrapSNResponse: jest.fn(),
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
jest.mock("../FileLogger", function () {
|
|
82
|
+
return { fileLogger: { debug: jest.fn() } };
|
|
83
|
+
});
|
|
84
|
+
jest.mock("../logMessages", function () {
|
|
85
|
+
return {
|
|
86
|
+
logPushResults: jest.fn(),
|
|
87
|
+
logBuildResults: jest.fn(),
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
jest.mock("../gitUtils", function () {
|
|
91
|
+
return { gitDiffToEncodedPaths: jest.fn() };
|
|
92
|
+
});
|
|
93
|
+
jest.mock("../FileUtils", function () {
|
|
94
|
+
return { encodedPathsToFilePaths: jest.fn() };
|
|
95
|
+
});
|
|
96
|
+
jest.mock("inquirer", function () {
|
|
97
|
+
return { prompt: jest.fn() };
|
|
98
|
+
});
|
|
99
|
+
describe("taskClearCommand", function () {
|
|
100
|
+
var taskPath;
|
|
101
|
+
var defaultArgs = { logLevel: "info" };
|
|
102
|
+
beforeEach(function () {
|
|
103
|
+
logMessages = [];
|
|
104
|
+
taskPath = path.resolve(process.cwd(), ".dove-active-task.json");
|
|
105
|
+
jest.clearAllMocks();
|
|
106
|
+
});
|
|
107
|
+
it("should remove .dove-active-task.json when it exists", async function () {
|
|
108
|
+
var taskData = JSON.stringify({
|
|
109
|
+
taskId: "abc123",
|
|
110
|
+
taskName: "My Test Task",
|
|
111
|
+
updateSetName: "CU-abc123",
|
|
112
|
+
});
|
|
113
|
+
mockFs.existsSync.mockImplementation(function (p) {
|
|
114
|
+
return p === taskPath;
|
|
115
|
+
});
|
|
116
|
+
mockFs.readFileSync.mockImplementation(function (p) {
|
|
117
|
+
if (p === taskPath)
|
|
118
|
+
return taskData;
|
|
119
|
+
throw new Error("file not found");
|
|
120
|
+
});
|
|
121
|
+
mockFs.unlinkSync.mockImplementation(function () { });
|
|
122
|
+
await (0, commands_1.taskClearCommand)(defaultArgs);
|
|
123
|
+
expect(mockFs.unlinkSync).toHaveBeenCalledWith(taskPath);
|
|
124
|
+
expect(logMessages.some(function (m) {
|
|
125
|
+
return m.level === "success" && m.msg.indexOf("My Test Task") !== -1;
|
|
126
|
+
})).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
it("should show informational message when no active task exists", async function () {
|
|
129
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
130
|
+
await (0, commands_1.taskClearCommand)(defaultArgs);
|
|
131
|
+
expect(mockFs.unlinkSync).not.toHaveBeenCalled();
|
|
132
|
+
expect(logMessages.some(function (m) {
|
|
133
|
+
return m.level === "info" && m.msg.indexOf("No active task") !== -1;
|
|
134
|
+
})).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
it("should still remove file even if JSON is invalid", async function () {
|
|
137
|
+
mockFs.existsSync.mockImplementation(function (p) {
|
|
138
|
+
return p === taskPath;
|
|
139
|
+
});
|
|
140
|
+
mockFs.readFileSync.mockImplementation(function () {
|
|
141
|
+
return "not valid json";
|
|
142
|
+
});
|
|
143
|
+
mockFs.unlinkSync.mockImplementation(function () { });
|
|
144
|
+
await (0, commands_1.taskClearCommand)(defaultArgs);
|
|
145
|
+
expect(mockFs.unlinkSync).toHaveBeenCalledWith(taskPath);
|
|
146
|
+
expect(logMessages.some(function (m) {
|
|
147
|
+
return m.level === "success" && m.msg.indexOf("removed") !== -1;
|
|
148
|
+
})).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
it("should use taskId as fallback when taskName is missing", async function () {
|
|
151
|
+
var taskData = JSON.stringify({
|
|
152
|
+
taskId: "def456",
|
|
153
|
+
updateSetName: "CU-def456",
|
|
154
|
+
});
|
|
155
|
+
mockFs.existsSync.mockImplementation(function (p) {
|
|
156
|
+
return p === taskPath;
|
|
157
|
+
});
|
|
158
|
+
mockFs.readFileSync.mockImplementation(function (p) {
|
|
159
|
+
if (p === taskPath)
|
|
160
|
+
return taskData;
|
|
161
|
+
throw new Error("file not found");
|
|
162
|
+
});
|
|
163
|
+
mockFs.unlinkSync.mockImplementation(function () { });
|
|
164
|
+
await (0, commands_1.taskClearCommand)(defaultArgs);
|
|
165
|
+
expect(mockFs.unlinkSync).toHaveBeenCalledWith(taskPath);
|
|
166
|
+
expect(logMessages.some(function (m) {
|
|
167
|
+
return m.level === "success" && m.msg.indexOf("def456") !== -1;
|
|
168
|
+
})).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for US-012: Add staleness warning for active task
|
|
4
|
+
*
|
|
5
|
+
* Validates:
|
|
6
|
+
* - On startup, if .dove-active-task.json is older than 7 days, a warning is logged
|
|
7
|
+
* - Fresh task files produce no warning
|
|
8
|
+
* - Warning includes task name and age in days
|
|
9
|
+
*/
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
// --- Mock setup ---
|
|
45
|
+
const mockSNClient = {
|
|
46
|
+
getScopeId: jest.fn(),
|
|
47
|
+
getUserSysId: jest.fn(),
|
|
48
|
+
getCurrentAppUserPrefSysId: jest.fn(),
|
|
49
|
+
updateCurrentAppUserPref: jest.fn(),
|
|
50
|
+
createCurrentAppUserPref: jest.fn(),
|
|
51
|
+
changeScope: jest.fn().mockResolvedValue(undefined),
|
|
52
|
+
createUpdateSet: jest.fn(),
|
|
53
|
+
changeUpdateSet: jest.fn(),
|
|
54
|
+
getCurrentUpdateSet: jest.fn(),
|
|
55
|
+
client: {
|
|
56
|
+
get: jest.fn(),
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
jest.mock("../snClient", () => ({
|
|
60
|
+
defaultClient: jest.fn(() => mockSNClient),
|
|
61
|
+
unwrapSNResponse: jest.fn((val) => val),
|
|
62
|
+
}));
|
|
63
|
+
jest.mock("../FileUtils", () => ({
|
|
64
|
+
getFileContextFromPath: jest.fn(),
|
|
65
|
+
getFileContextWithSkipReason: jest.fn(),
|
|
66
|
+
}));
|
|
67
|
+
jest.mock("../appUtils", () => ({
|
|
68
|
+
groupAppFiles: jest.fn(),
|
|
69
|
+
pushFiles: jest.fn(),
|
|
70
|
+
}));
|
|
71
|
+
jest.mock("../logMessages", () => ({
|
|
72
|
+
logFilePush: jest.fn(),
|
|
73
|
+
}));
|
|
74
|
+
jest.mock("../recentEdits", () => ({
|
|
75
|
+
writeRecentEdit: jest.fn(),
|
|
76
|
+
}));
|
|
77
|
+
jest.mock("../Logger", () => ({
|
|
78
|
+
logger: {
|
|
79
|
+
info: jest.fn(),
|
|
80
|
+
error: jest.fn(),
|
|
81
|
+
warn: jest.fn(),
|
|
82
|
+
debug: jest.fn(),
|
|
83
|
+
success: jest.fn(),
|
|
84
|
+
getLogLevel: jest.fn().mockReturnValue("info"),
|
|
85
|
+
},
|
|
86
|
+
}));
|
|
87
|
+
jest.mock("../config", () => ({
|
|
88
|
+
loadConfigs: jest.fn().mockResolvedValue(undefined),
|
|
89
|
+
getConfig: jest.fn(),
|
|
90
|
+
getRootDir: jest.fn().mockReturnValue("/project"),
|
|
91
|
+
updateManifest: jest.fn(),
|
|
92
|
+
getManifest: jest.fn(),
|
|
93
|
+
getSourcePath: jest.fn().mockReturnValue("/project/src"),
|
|
94
|
+
getScopeManifestPath: jest.fn((scope) => `/project/dove.manifest.${scope}.json`),
|
|
95
|
+
getManifestPath: jest.fn().mockReturnValue("/project/dove.manifest.json"),
|
|
96
|
+
}));
|
|
97
|
+
// Filesystem mock store
|
|
98
|
+
var mockFsStore = {};
|
|
99
|
+
var mockFsStatStore = {};
|
|
100
|
+
jest.mock("fs", () => ({
|
|
101
|
+
existsSync: jest.fn((p) => {
|
|
102
|
+
return p in mockFsStore;
|
|
103
|
+
}),
|
|
104
|
+
readFileSync: jest.fn((p) => {
|
|
105
|
+
if (p in mockFsStore)
|
|
106
|
+
return mockFsStore[p];
|
|
107
|
+
throw new Error("ENOENT: " + p);
|
|
108
|
+
}),
|
|
109
|
+
writeFileSync: jest.fn((p, data) => {
|
|
110
|
+
mockFsStore[p] = data;
|
|
111
|
+
}),
|
|
112
|
+
statSync: jest.fn((p) => {
|
|
113
|
+
if (p in mockFsStatStore)
|
|
114
|
+
return mockFsStatStore[p];
|
|
115
|
+
return { mtimeMs: Date.now() };
|
|
116
|
+
}),
|
|
117
|
+
promises: {
|
|
118
|
+
readFile: jest.fn(),
|
|
119
|
+
writeFile: jest.fn(),
|
|
120
|
+
readdir: jest.fn(),
|
|
121
|
+
mkdir: jest.fn(),
|
|
122
|
+
access: jest.fn(),
|
|
123
|
+
stat: jest.fn(),
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
jest.mock("chokidar", () => ({
|
|
127
|
+
watch: jest.fn(() => ({
|
|
128
|
+
on: jest.fn().mockReturnThis(),
|
|
129
|
+
close: jest.fn(),
|
|
130
|
+
})),
|
|
131
|
+
}));
|
|
132
|
+
jest.mock("lodash", () => {
|
|
133
|
+
var actual = jest.requireActual("lodash");
|
|
134
|
+
return {
|
|
135
|
+
...actual,
|
|
136
|
+
debounce: jest.fn((fn) => {
|
|
137
|
+
var wrapper = jest.fn();
|
|
138
|
+
wrapper.cancel = jest.fn();
|
|
139
|
+
wrapper.flush = jest.fn(() => fn());
|
|
140
|
+
return wrapper;
|
|
141
|
+
}),
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
jest.mock("axios", () => ({
|
|
145
|
+
default: {
|
|
146
|
+
create: jest.fn(() => ({
|
|
147
|
+
get: jest.fn().mockResolvedValue({ data: { result: null } }),
|
|
148
|
+
})),
|
|
149
|
+
},
|
|
150
|
+
}));
|
|
151
|
+
// --- Imports ---
|
|
152
|
+
const Logger_1 = require("../Logger");
|
|
153
|
+
const MultiScopeWatcher_1 = require("../MultiScopeWatcher");
|
|
154
|
+
const path = __importStar(require("path"));
|
|
155
|
+
// --- Tests ---
|
|
156
|
+
describe("US-012: Add staleness warning for active task", () => {
|
|
157
|
+
var taskPath;
|
|
158
|
+
beforeEach(() => {
|
|
159
|
+
jest.clearAllMocks();
|
|
160
|
+
mockFsStore = {};
|
|
161
|
+
mockFsStatStore = {};
|
|
162
|
+
taskPath = path.resolve(process.cwd(), ".dove-active-task.json");
|
|
163
|
+
});
|
|
164
|
+
it("should warn when active task file is older than 7 days", () => {
|
|
165
|
+
var validTask = {
|
|
166
|
+
taskId: "TASK-123",
|
|
167
|
+
taskName: "Old Feature",
|
|
168
|
+
updateSetName: "CU-TASK-123",
|
|
169
|
+
};
|
|
170
|
+
mockFsStore[taskPath] = JSON.stringify(validTask);
|
|
171
|
+
// 10 days ago
|
|
172
|
+
mockFsStatStore[taskPath] = { mtimeMs: Date.now() - (10 * 24 * 60 * 60 * 1000) };
|
|
173
|
+
var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
|
|
174
|
+
expect(result).not.toBeNull();
|
|
175
|
+
expect(result.taskId).toBe("TASK-123");
|
|
176
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("Old Feature"));
|
|
177
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("10 days ago"));
|
|
178
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("dove task clear"));
|
|
179
|
+
});
|
|
180
|
+
it("should not warn when active task file is fresh (under 7 days)", () => {
|
|
181
|
+
var validTask = {
|
|
182
|
+
taskId: "TASK-456",
|
|
183
|
+
taskName: "Recent Feature",
|
|
184
|
+
updateSetName: "CU-TASK-456",
|
|
185
|
+
};
|
|
186
|
+
mockFsStore[taskPath] = JSON.stringify(validTask);
|
|
187
|
+
// 2 days ago
|
|
188
|
+
mockFsStatStore[taskPath] = { mtimeMs: Date.now() - (2 * 24 * 60 * 60 * 1000) };
|
|
189
|
+
var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
|
|
190
|
+
expect(result).not.toBeNull();
|
|
191
|
+
expect(result.taskId).toBe("TASK-456");
|
|
192
|
+
expect(Logger_1.logger.warn).not.toHaveBeenCalled();
|
|
193
|
+
});
|
|
194
|
+
it("should use taskId as fallback when taskName is missing", () => {
|
|
195
|
+
var taskNoName = {
|
|
196
|
+
taskId: "TASK-789",
|
|
197
|
+
updateSetName: "CU-TASK-789",
|
|
198
|
+
};
|
|
199
|
+
mockFsStore[taskPath] = JSON.stringify(taskNoName);
|
|
200
|
+
// 14 days ago
|
|
201
|
+
mockFsStatStore[taskPath] = { mtimeMs: Date.now() - (14 * 24 * 60 * 60 * 1000) };
|
|
202
|
+
var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
|
|
203
|
+
expect(result).not.toBeNull();
|
|
204
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("TASK-789"));
|
|
205
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("14 days ago"));
|
|
206
|
+
});
|
|
207
|
+
it("should not warn when task file is exactly 6 days old", () => {
|
|
208
|
+
var validTask = {
|
|
209
|
+
taskId: "TASK-EDGE",
|
|
210
|
+
taskName: "Edge Case",
|
|
211
|
+
updateSetName: "CU-TASK-EDGE",
|
|
212
|
+
};
|
|
213
|
+
mockFsStore[taskPath] = JSON.stringify(validTask);
|
|
214
|
+
// 6 days ago
|
|
215
|
+
mockFsStatStore[taskPath] = { mtimeMs: Date.now() - (6 * 24 * 60 * 60 * 1000) };
|
|
216
|
+
var result = MultiScopeWatcher_1.multiScopeWatcher.readActiveTask();
|
|
217
|
+
expect(result).not.toBeNull();
|
|
218
|
+
expect(Logger_1.logger.warn).not.toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for the enhanced error path in unwrapSNResponse.
|
|
4
|
+
*
|
|
5
|
+
* When a ServiceNow REST call throws, the catch block must:
|
|
6
|
+
* - Log a structured one-liner via logger.error including HTTP status, method, URL
|
|
7
|
+
* and (when present) the ServiceNow-shaped error message from the response body.
|
|
8
|
+
* - Dump the full error surface (status, statusText, responseData, responseHeaders,
|
|
9
|
+
* scope extracted from /getManifest/:scope URLs) via fileLogger.debug.
|
|
10
|
+
* - Re-throw the original error so upstream callers see identical behaviour.
|
|
11
|
+
*
|
|
12
|
+
* Non-Axios errors must preserve the original log shape ("Error from <instance>: <msg>")
|
|
13
|
+
* and also produce a debug dump for future triage.
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
var mockLogger = {
|
|
17
|
+
info: jest.fn(),
|
|
18
|
+
debug: jest.fn(),
|
|
19
|
+
warn: jest.fn(),
|
|
20
|
+
error: jest.fn(),
|
|
21
|
+
getLogLevel: function () { return "debug"; },
|
|
22
|
+
};
|
|
23
|
+
var mockFileLogger = {
|
|
24
|
+
debug: jest.fn(),
|
|
25
|
+
info: jest.fn(),
|
|
26
|
+
warn: jest.fn(),
|
|
27
|
+
error: jest.fn(),
|
|
28
|
+
};
|
|
29
|
+
jest.mock("../Logger", function () {
|
|
30
|
+
return { logger: mockLogger };
|
|
31
|
+
});
|
|
32
|
+
jest.mock("../FileLogger", function () {
|
|
33
|
+
return { fileLogger: mockFileLogger };
|
|
34
|
+
});
|
|
35
|
+
const snClient_1 = require("../snClient");
|
|
36
|
+
function makeAxiosError(overrides) {
|
|
37
|
+
var error = new Error("Request failed with status code " + overrides.status);
|
|
38
|
+
error.isAxiosError = true;
|
|
39
|
+
error.config = {
|
|
40
|
+
method: overrides.method || "post",
|
|
41
|
+
url: overrides.url || "api/sinc/sincronia/getManifest/x_cadso_example",
|
|
42
|
+
};
|
|
43
|
+
error.response = {
|
|
44
|
+
status: overrides.status,
|
|
45
|
+
statusText: overrides.statusText || "",
|
|
46
|
+
headers: overrides.headers || { "x-test": "1" },
|
|
47
|
+
data: overrides.data,
|
|
48
|
+
};
|
|
49
|
+
return error;
|
|
50
|
+
}
|
|
51
|
+
describe("unwrapSNResponse — error handling", function () {
|
|
52
|
+
var origInstance;
|
|
53
|
+
beforeAll(function () {
|
|
54
|
+
origInstance = process.env.SN_INSTANCE;
|
|
55
|
+
process.env.SN_INSTANCE = "tenontest.service-now.com";
|
|
56
|
+
});
|
|
57
|
+
afterAll(function () {
|
|
58
|
+
if (origInstance === undefined)
|
|
59
|
+
delete process.env.SN_INSTANCE;
|
|
60
|
+
else
|
|
61
|
+
process.env.SN_INSTANCE = origInstance;
|
|
62
|
+
});
|
|
63
|
+
beforeEach(function () {
|
|
64
|
+
jest.clearAllMocks();
|
|
65
|
+
});
|
|
66
|
+
test("Axios 500 with ServiceNow-shaped body: one-liner includes status + URL + SN message, debug dump carries full detail", async function () {
|
|
67
|
+
var snBody = {
|
|
68
|
+
error: {
|
|
69
|
+
message: "org.mozilla.javascript.EcmaError: TypeError",
|
|
70
|
+
detail: "Cannot read property 'name' of null",
|
|
71
|
+
},
|
|
72
|
+
status: "failure",
|
|
73
|
+
};
|
|
74
|
+
var axErr = makeAxiosError({
|
|
75
|
+
status: 500,
|
|
76
|
+
method: "post",
|
|
77
|
+
url: "api/sinc/sincronia/getManifest/x_cadso_automate",
|
|
78
|
+
data: snBody,
|
|
79
|
+
statusText: "Internal Server Error",
|
|
80
|
+
});
|
|
81
|
+
var rejected = Promise.reject(axErr);
|
|
82
|
+
// Silence the unhandled-rejection warning before jest inspects it
|
|
83
|
+
rejected.catch(function () { });
|
|
84
|
+
await expect((0, snClient_1.unwrapSNResponse)(rejected)).rejects.toBe(axErr);
|
|
85
|
+
expect(mockLogger.error).toHaveBeenCalledTimes(1);
|
|
86
|
+
var userLine = mockLogger.error.mock.calls[0][0];
|
|
87
|
+
expect(userLine).toContain("tenontest.service-now.com");
|
|
88
|
+
expect(userLine).toContain("HTTP 500");
|
|
89
|
+
expect(userLine).toContain("POST");
|
|
90
|
+
expect(userLine).toContain("getManifest/x_cadso_automate");
|
|
91
|
+
expect(userLine).toContain("org.mozilla.javascript.EcmaError");
|
|
92
|
+
expect(mockFileLogger.debug).toHaveBeenCalledTimes(1);
|
|
93
|
+
var debugLabel = mockFileLogger.debug.mock.calls[0][0];
|
|
94
|
+
var debugPayload = mockFileLogger.debug.mock.calls[0][1];
|
|
95
|
+
expect(debugLabel).toBe("REST error detail");
|
|
96
|
+
expect(debugPayload).toMatchObject({
|
|
97
|
+
instance: "tenontest.service-now.com",
|
|
98
|
+
scope: "x_cadso_automate",
|
|
99
|
+
method: "POST",
|
|
100
|
+
status: 500,
|
|
101
|
+
statusText: "Internal Server Error",
|
|
102
|
+
responseData: snBody,
|
|
103
|
+
responseHeaders: { "x-test": "1" },
|
|
104
|
+
});
|
|
105
|
+
expect(debugPayload.url).toContain("getManifest/x_cadso_automate");
|
|
106
|
+
});
|
|
107
|
+
test("Axios 500 with non-SN body: one-liner falls back cleanly, no crash", async function () {
|
|
108
|
+
var axErr = makeAxiosError({
|
|
109
|
+
status: 500,
|
|
110
|
+
method: "get",
|
|
111
|
+
url: "api/now/table/sys_script_include",
|
|
112
|
+
data: "<html><body>Gateway error</body></html>",
|
|
113
|
+
});
|
|
114
|
+
var rejected = Promise.reject(axErr);
|
|
115
|
+
rejected.catch(function () { });
|
|
116
|
+
await expect((0, snClient_1.unwrapSNResponse)(rejected)).rejects.toBe(axErr);
|
|
117
|
+
var userLine = mockLogger.error.mock.calls[0][0];
|
|
118
|
+
expect(userLine).toContain("HTTP 500");
|
|
119
|
+
expect(userLine).toContain("GET");
|
|
120
|
+
expect(userLine).toContain("table/sys_script_include");
|
|
121
|
+
// No SN message to append; no em dash
|
|
122
|
+
expect(userLine).not.toContain(" — ");
|
|
123
|
+
expect(mockFileLogger.debug).toHaveBeenCalledTimes(1);
|
|
124
|
+
var debugPayload = mockFileLogger.debug.mock.calls[0][1];
|
|
125
|
+
// Non-manifest URL — scope should be undefined
|
|
126
|
+
expect(debugPayload.scope).toBeUndefined();
|
|
127
|
+
expect(debugPayload.responseData).toBe("<html><body>Gateway error</body></html>");
|
|
128
|
+
});
|
|
129
|
+
test("Non-Axios error: preserves legacy log shape and re-throws", async function () {
|
|
130
|
+
var nonAxios = new Error("socket hang up");
|
|
131
|
+
var rejected = Promise.reject(nonAxios);
|
|
132
|
+
rejected.catch(function () { });
|
|
133
|
+
await expect((0, snClient_1.unwrapSNResponse)(rejected)).rejects.toBe(nonAxios);
|
|
134
|
+
expect(mockLogger.error).toHaveBeenCalledTimes(1);
|
|
135
|
+
var userLine = mockLogger.error.mock.calls[0][0];
|
|
136
|
+
expect(userLine).toBe("Error from tenontest.service-now.com: socket hang up");
|
|
137
|
+
expect(mockFileLogger.debug).toHaveBeenCalledTimes(1);
|
|
138
|
+
var debugLabel = mockFileLogger.debug.mock.calls[0][0];
|
|
139
|
+
expect(debugLabel).toBe("Non-Axios error detail");
|
|
140
|
+
var debugPayload = mockFileLogger.debug.mock.calls[0][1];
|
|
141
|
+
expect(debugPayload.message).toBe("socket hang up");
|
|
142
|
+
expect(debugPayload.errorName).toBe("Error");
|
|
143
|
+
expect(typeof debugPayload.errorStack).toBe("string");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const zlib_1 = require("zlib");
|
|
4
|
+
const values_1 = require("../flowDesigner/values");
|
|
5
|
+
describe("decodeV2Values / encodeV2Values", () => {
|
|
6
|
+
describe("round-trip", () => {
|
|
7
|
+
it("decodes what encodeV2Values encoded (primitive array)", () => {
|
|
8
|
+
const original = [
|
|
9
|
+
{ actionInstanceSysId: "abc", id: "1", name: "to", value: "user@example.com" },
|
|
10
|
+
{ actionInstanceSysId: "abc", id: "2", name: "subject", value: "hi" },
|
|
11
|
+
];
|
|
12
|
+
const blob = (0, values_1.encodeV2Values)(original);
|
|
13
|
+
expect((0, values_1.decodeV2Values)(blob)).toEqual(original);
|
|
14
|
+
});
|
|
15
|
+
it("decodes what encodeV2Values encoded (nested parameter objects)", () => {
|
|
16
|
+
const original = [
|
|
17
|
+
{
|
|
18
|
+
actionInstanceSysId: "abc123",
|
|
19
|
+
id: "input_a",
|
|
20
|
+
name: "record",
|
|
21
|
+
value: { table: "incident", sys_id: "xyz" },
|
|
22
|
+
parameter: {
|
|
23
|
+
type: "reference",
|
|
24
|
+
labels: { en: "Record" },
|
|
25
|
+
validation: { mandatory: true },
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
const blob = (0, values_1.encodeV2Values)(original);
|
|
30
|
+
expect((0, values_1.decodeV2Values)(blob)).toEqual(original);
|
|
31
|
+
});
|
|
32
|
+
it("handles empty array", () => {
|
|
33
|
+
expect((0, values_1.decodeV2Values)((0, values_1.encodeV2Values)([]))).toEqual([]);
|
|
34
|
+
});
|
|
35
|
+
it("handles large payloads without truncation", () => {
|
|
36
|
+
const big = Array.from({ length: 500 }, (_, i) => ({
|
|
37
|
+
id: String(i),
|
|
38
|
+
name: "field_" + i,
|
|
39
|
+
value: "x".repeat(200),
|
|
40
|
+
}));
|
|
41
|
+
expect((0, values_1.decodeV2Values)((0, values_1.encodeV2Values)(big))).toEqual(big);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("decode — input validation", () => {
|
|
45
|
+
it("throws on empty string", () => {
|
|
46
|
+
expect(() => (0, values_1.decodeV2Values)("")).toThrow(/non-empty string/);
|
|
47
|
+
});
|
|
48
|
+
it("throws on whitespace-only string", () => {
|
|
49
|
+
expect(() => (0, values_1.decodeV2Values)(" \n ")).toThrow(/non-empty string/);
|
|
50
|
+
});
|
|
51
|
+
it("throws on non-string input", () => {
|
|
52
|
+
expect(() => (0, values_1.decodeV2Values)(null)).toThrow(/non-empty string/);
|
|
53
|
+
expect(() => (0, values_1.decodeV2Values)(undefined)).toThrow(/non-empty string/);
|
|
54
|
+
expect(() => (0, values_1.decodeV2Values)(123)).toThrow(/non-empty string/);
|
|
55
|
+
});
|
|
56
|
+
it("throws with a clear message on non-gzip base64 input", () => {
|
|
57
|
+
// Valid base64, but the decoded bytes are not gzip.
|
|
58
|
+
const notGzip = Buffer.from("hello world").toString("base64");
|
|
59
|
+
expect(() => (0, values_1.decodeV2Values)(notGzip)).toThrow(/decompression failed/);
|
|
60
|
+
});
|
|
61
|
+
it("throws on gzip of invalid JSON", () => {
|
|
62
|
+
const badJson = (0, zlib_1.gzipSync)(Buffer.from("{not valid json", "utf8")).toString("base64");
|
|
63
|
+
expect(() => (0, values_1.decodeV2Values)(badJson)).toThrow(/not valid JSON/);
|
|
64
|
+
});
|
|
65
|
+
it("tolerates trailing whitespace and newlines (common from file reads)", () => {
|
|
66
|
+
const blob = (0, values_1.encodeV2Values)([{ id: "1", value: "a" }]);
|
|
67
|
+
expect((0, values_1.decodeV2Values)(blob + "\n")).toEqual([{ id: "1", value: "a" }]);
|
|
68
|
+
expect((0, values_1.decodeV2Values)(" " + blob + " ")).toEqual([{ id: "1", value: "a" }]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
describe("generic typing", () => {
|
|
72
|
+
it("preserves the caller's expected type", () => {
|
|
73
|
+
const blob = (0, values_1.encodeV2Values)([{ actionInstanceSysId: "a", id: "1" }]);
|
|
74
|
+
const decoded = (0, values_1.decodeV2Values)(blob);
|
|
75
|
+
expect(decoded[0].actionInstanceSysId).toBe("a");
|
|
76
|
+
expect(decoded[0].id).toBe("1");
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|