@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,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// --- Mock setup (must be before imports) ---
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
var mockApi = {
|
|
5
|
+
getTask: jest.fn(),
|
|
6
|
+
};
|
|
7
|
+
jest.mock("@tenonhq/dovetail-clickup", function () {
|
|
8
|
+
return {
|
|
9
|
+
createClickUpApi: jest.fn(function () {
|
|
10
|
+
return mockApi;
|
|
11
|
+
}),
|
|
12
|
+
parseClickUpIdentifier: jest.fn(function (input) {
|
|
13
|
+
return { taskId: input.replace(/.*\/t\//, ""), raw: input };
|
|
14
|
+
}),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
jest.mock("../clickupCommands", function () {
|
|
18
|
+
return {
|
|
19
|
+
refineUpdateSetName: jest.fn(function (params) {
|
|
20
|
+
return "Refined: " + params.taskName;
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
jest.mock("../Logger", function () {
|
|
25
|
+
return {
|
|
26
|
+
logger: {
|
|
27
|
+
info: jest.fn(),
|
|
28
|
+
error: jest.fn(),
|
|
29
|
+
warn: jest.fn(),
|
|
30
|
+
debug: jest.fn(),
|
|
31
|
+
success: jest.fn(),
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
jest.mock("chalk", function () {
|
|
36
|
+
var identity = function (s) { return s; };
|
|
37
|
+
identity.green = identity;
|
|
38
|
+
return {
|
|
39
|
+
default: identity,
|
|
40
|
+
green: identity,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
// --- Imports (after mocks) ---
|
|
44
|
+
const clickupPushHelper_1 = require("../clickupPushHelper");
|
|
45
|
+
const dovetail_clickup_1 = require("@tenonhq/dovetail-clickup");
|
|
46
|
+
const clickupCommands_1 = require("../clickupCommands");
|
|
47
|
+
// --- Tests ---
|
|
48
|
+
describe("resolveClickUpForPush", function () {
|
|
49
|
+
var originalEnv;
|
|
50
|
+
beforeEach(function () {
|
|
51
|
+
jest.clearAllMocks();
|
|
52
|
+
originalEnv = process.env.CLICKUP_API_TOKEN;
|
|
53
|
+
process.env.CLICKUP_API_TOKEN = "pk_test_token";
|
|
54
|
+
});
|
|
55
|
+
afterEach(function () {
|
|
56
|
+
if (originalEnv !== undefined) {
|
|
57
|
+
process.env.CLICKUP_API_TOKEN = originalEnv;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
delete process.env.CLICKUP_API_TOKEN;
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
it("throws when CLICKUP_API_TOKEN is not set", async function () {
|
|
64
|
+
delete process.env.CLICKUP_API_TOKEN;
|
|
65
|
+
await expect((0, clickupPushHelper_1.resolveClickUpForPush)("abc123")).rejects.toThrow("CLICKUP_API_TOKEN not set");
|
|
66
|
+
});
|
|
67
|
+
it("throws when CLICKUP_API_TOKEN is empty", async function () {
|
|
68
|
+
process.env.CLICKUP_API_TOKEN = "";
|
|
69
|
+
await expect((0, clickupPushHelper_1.resolveClickUpForPush)("abc123")).rejects.toThrow("CLICKUP_API_TOKEN not set");
|
|
70
|
+
});
|
|
71
|
+
it("resolves task and returns refined update set name", async function () {
|
|
72
|
+
mockApi.getTask.mockResolvedValue({
|
|
73
|
+
id: "abc123",
|
|
74
|
+
name: "Fix Login Bug",
|
|
75
|
+
description: "Login page has redirect issue",
|
|
76
|
+
});
|
|
77
|
+
var result = await (0, clickupPushHelper_1.resolveClickUpForPush)("abc123");
|
|
78
|
+
expect(dovetail_clickup_1.parseClickUpIdentifier).toHaveBeenCalledWith("abc123");
|
|
79
|
+
expect(dovetail_clickup_1.createClickUpApi).toHaveBeenCalledWith({ token: "pk_test_token" });
|
|
80
|
+
expect(mockApi.getTask).toHaveBeenCalled();
|
|
81
|
+
expect(clickupCommands_1.refineUpdateSetName).toHaveBeenCalledWith({
|
|
82
|
+
taskName: "Fix Login Bug",
|
|
83
|
+
taskId: "abc123",
|
|
84
|
+
taskDescription: "Login page has redirect issue",
|
|
85
|
+
});
|
|
86
|
+
expect(result).toBe("Refined: Fix Login Bug");
|
|
87
|
+
});
|
|
88
|
+
it("passes empty string for description when task has no description", async function () {
|
|
89
|
+
mockApi.getTask.mockResolvedValue({
|
|
90
|
+
id: "abc123",
|
|
91
|
+
name: "No Desc Task",
|
|
92
|
+
description: "",
|
|
93
|
+
});
|
|
94
|
+
await (0, clickupPushHelper_1.resolveClickUpForPush)("abc123");
|
|
95
|
+
expect(clickupCommands_1.refineUpdateSetName).toHaveBeenCalledWith(expect.objectContaining({
|
|
96
|
+
taskDescription: "",
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
99
|
+
it("passes empty string when description is undefined", async function () {
|
|
100
|
+
mockApi.getTask.mockResolvedValue({
|
|
101
|
+
id: "abc123",
|
|
102
|
+
name: "Undefined Desc",
|
|
103
|
+
description: undefined,
|
|
104
|
+
});
|
|
105
|
+
await (0, clickupPushHelper_1.resolveClickUpForPush)("abc123");
|
|
106
|
+
expect(clickupCommands_1.refineUpdateSetName).toHaveBeenCalledWith(expect.objectContaining({
|
|
107
|
+
taskDescription: "",
|
|
108
|
+
}));
|
|
109
|
+
});
|
|
110
|
+
it("propagates API errors", async function () {
|
|
111
|
+
mockApi.getTask.mockRejectedValue(new Error("Task not found"));
|
|
112
|
+
await expect((0, clickupPushHelper_1.resolveClickUpForPush)("missing123")).rejects.toThrow("Task not found");
|
|
113
|
+
});
|
|
114
|
+
it("works with ClickUp URL input", async function () {
|
|
115
|
+
mockApi.getTask.mockResolvedValue({
|
|
116
|
+
id: "86a3bx7wz",
|
|
117
|
+
name: "URL Task",
|
|
118
|
+
description: "From URL",
|
|
119
|
+
});
|
|
120
|
+
// The mock parseClickUpIdentifier strips the URL prefix
|
|
121
|
+
await (0, clickupPushHelper_1.resolveClickUpForPush)("https://app.clickup.com/t/86a3bx7wz");
|
|
122
|
+
expect(dovetail_clickup_1.parseClickUpIdentifier).toHaveBeenCalledWith("https://app.clickup.com/t/86a3bx7wz");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tests for the dove.config.js / sinc.config.js loader fallback in config.ts.
|
|
3
|
+
// Covers: dove.config.js wins when both are present (no warning); sinc.config.js
|
|
4
|
+
// is accepted when alone (with a deprecation warning); neither present walks
|
|
5
|
+
// up the tree (returns false at the filesystem root).
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
var mockLogger = {
|
|
11
|
+
info: jest.fn(),
|
|
12
|
+
debug: jest.fn(),
|
|
13
|
+
warn: jest.fn(),
|
|
14
|
+
error: jest.fn(),
|
|
15
|
+
success: jest.fn(),
|
|
16
|
+
getLogLevel: function () { return "debug"; },
|
|
17
|
+
setLogLevel: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
jest.mock("../Logger", function () {
|
|
20
|
+
return { logger: mockLogger };
|
|
21
|
+
});
|
|
22
|
+
jest.mock("../defaultOptions", function () {
|
|
23
|
+
return { includes: {}, excludes: {}, tableOptions: {}, scopes: {} };
|
|
24
|
+
});
|
|
25
|
+
const fs_1 = __importDefault(require("fs"));
|
|
26
|
+
const os_1 = __importDefault(require("os"));
|
|
27
|
+
const path_1 = __importDefault(require("path"));
|
|
28
|
+
const config_1 = require("../config");
|
|
29
|
+
function makeTempDir() {
|
|
30
|
+
return fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), "dove-config-test-"));
|
|
31
|
+
}
|
|
32
|
+
function rmrf(dir) {
|
|
33
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
describe("loadConfigPath fallback", function () {
|
|
36
|
+
var tmp = "";
|
|
37
|
+
beforeEach(function () {
|
|
38
|
+
jest.clearAllMocks();
|
|
39
|
+
tmp = makeTempDir();
|
|
40
|
+
});
|
|
41
|
+
afterEach(function () {
|
|
42
|
+
rmrf(tmp);
|
|
43
|
+
});
|
|
44
|
+
it("prefers dove.config.js when both files exist (no warning)", async function () {
|
|
45
|
+
fs_1.default.writeFileSync(path_1.default.join(tmp, "dove.config.js"), "module.exports = {};\n");
|
|
46
|
+
fs_1.default.writeFileSync(path_1.default.join(tmp, "sinc.config.js"), "module.exports = {};\n");
|
|
47
|
+
var result = await (0, config_1.loadConfigPath)(tmp);
|
|
48
|
+
expect(result).toBe(path_1.default.join(tmp, "dove.config.js"));
|
|
49
|
+
expect(mockLogger.warn).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
it("falls back to sinc.config.js with a deprecation warning when only legacy exists", async function () {
|
|
52
|
+
fs_1.default.writeFileSync(path_1.default.join(tmp, "sinc.config.js"), "module.exports = {};\n");
|
|
53
|
+
var result = await (0, config_1.loadConfigPath)(tmp);
|
|
54
|
+
expect(result).toBe(path_1.default.join(tmp, "sinc.config.js"));
|
|
55
|
+
expect(mockLogger.warn).toHaveBeenCalledTimes(1);
|
|
56
|
+
expect(mockLogger.warn.mock.calls[0][0]).toContain("legacy 'sinc.config.js'");
|
|
57
|
+
expect(mockLogger.warn.mock.calls[0][0]).toContain("dove migrate");
|
|
58
|
+
});
|
|
59
|
+
it("returns false when neither file exists anywhere up the tree (from a temp dir)", async function () {
|
|
60
|
+
// Nested empty subdir to exercise the recursive walk-up at least one step.
|
|
61
|
+
var nested = path_1.default.join(tmp, "subdir");
|
|
62
|
+
fs_1.default.mkdirSync(nested);
|
|
63
|
+
var result = await (0, config_1.loadConfigPath)(nested);
|
|
64
|
+
// The walk eventually reaches filesystem root and returns false. We don't
|
|
65
|
+
// assert on intermediate behavior — just the terminal result.
|
|
66
|
+
expect(result).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const bootstrap_1 = require("../bootstrap");
|
|
9
|
+
let envPath = path_1.default.join(process.cwd(), ".env");
|
|
10
|
+
test('Credentials undefined when file is missing', async () => {
|
|
11
|
+
// Removes any existing test env file from past tests
|
|
12
|
+
if (fs_1.default.existsSync(envPath)) {
|
|
13
|
+
fs_1.default.unlinkSync(envPath);
|
|
14
|
+
}
|
|
15
|
+
// Run init command (command being tested)
|
|
16
|
+
await (0, bootstrap_1.init)();
|
|
17
|
+
// Check "process" variables match expected results
|
|
18
|
+
expect(process.env.SN_USER).toBeUndefined();
|
|
19
|
+
expect(process.env.SN_PASSWORD).toBeUndefined();
|
|
20
|
+
expect(process.env.SN_INSTANCE).toBeUndefined();
|
|
21
|
+
});
|
|
22
|
+
test('Credentials undefined when file is broken', async () => {
|
|
23
|
+
// Sample Credential Data
|
|
24
|
+
let t_user = "Tyler";
|
|
25
|
+
let t_pass = "Edwards";
|
|
26
|
+
let t_instance = "dev90755.service-now.com";
|
|
27
|
+
// Create a faulty .env file
|
|
28
|
+
fs_1.default.writeFile(envPath, 'SN_USR=' + t_user + ' \nSN_PASWORD' + t_pass + ' \nSN_INSTACE=' + t_instance, (err) => {
|
|
29
|
+
if (err)
|
|
30
|
+
throw err;
|
|
31
|
+
});
|
|
32
|
+
// Run init command (command being tested)
|
|
33
|
+
await (0, bootstrap_1.init)();
|
|
34
|
+
// Check "process" variables match expected results
|
|
35
|
+
expect(process.env.SN_USER).toBeUndefined();
|
|
36
|
+
expect(process.env.SN_PASSWORD).toBeUndefined();
|
|
37
|
+
expect(process.env.SN_INSTANCE).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
test('Credentials correct when file is correct', async () => {
|
|
40
|
+
// Sample Credential Data
|
|
41
|
+
let t_user = "Tyler";
|
|
42
|
+
let t_pass = "Edwards";
|
|
43
|
+
let t_instance = "dev90755.service-now.com";
|
|
44
|
+
// Create a correct .env file
|
|
45
|
+
fs_1.default.writeFile(envPath, 'SN_USER=' + t_user + ' \nSN_PASSWORD=' + t_pass + ' \nSN_INSTANCE=' + t_instance, (err) => {
|
|
46
|
+
if (err)
|
|
47
|
+
throw err;
|
|
48
|
+
});
|
|
49
|
+
// Run init command (command being tested)
|
|
50
|
+
await (0, bootstrap_1.init)();
|
|
51
|
+
// Check "process" variables match expected results
|
|
52
|
+
expect(process.env.SN_USER).toEqual(t_user);
|
|
53
|
+
expect(process.env.SN_PASSWORD).toEqual(t_pass);
|
|
54
|
+
expect(process.env.SN_INSTANCE).toEqual(t_instance);
|
|
55
|
+
});
|
|
56
|
+
// Cleans up leftover files
|
|
57
|
+
if (fs_1.default.existsSync(envPath)) {
|
|
58
|
+
fs_1.default.unlinkSync(envPath);
|
|
59
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// --- Mock setup (must be before imports) ---
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
var mockReaddirSync = jest.fn();
|
|
5
|
+
jest.mock("fs", function () {
|
|
6
|
+
var actual = jest.requireActual("fs");
|
|
7
|
+
return Object.assign({}, actual, {
|
|
8
|
+
readdirSync: mockReaddirSync,
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
jest.mock("../Logger", function () {
|
|
12
|
+
return {
|
|
13
|
+
logger: {
|
|
14
|
+
info: jest.fn(),
|
|
15
|
+
error: jest.fn(),
|
|
16
|
+
warn: jest.fn(),
|
|
17
|
+
debug: jest.fn(),
|
|
18
|
+
success: jest.fn(),
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
const discovery_1 = require("../initSystem/discovery");
|
|
23
|
+
const Logger_1 = require("../Logger");
|
|
24
|
+
describe("discoverPlugins", function () {
|
|
25
|
+
beforeEach(function () {
|
|
26
|
+
mockReaddirSync.mockReset();
|
|
27
|
+
// Default: no directories found
|
|
28
|
+
mockReaddirSync.mockImplementation(function () {
|
|
29
|
+
throw new Error("ENOENT");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
it("returns empty array when no node_modules found", function () {
|
|
33
|
+
const plugins = (0, discovery_1.discoverPlugins)();
|
|
34
|
+
expect(plugins).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
it("skips sincronia-core and sincronia-types", function () {
|
|
37
|
+
mockReaddirSync.mockReturnValue([
|
|
38
|
+
"sincronia-core",
|
|
39
|
+
"sincronia-types",
|
|
40
|
+
]);
|
|
41
|
+
const plugins = (0, discovery_1.discoverPlugins)();
|
|
42
|
+
expect(plugins).toEqual([]);
|
|
43
|
+
});
|
|
44
|
+
it("warns on plugin load failure", function () {
|
|
45
|
+
mockReaddirSync.mockReturnValue(["sincronia-nonexistent"]);
|
|
46
|
+
(0, discovery_1.discoverPlugins)();
|
|
47
|
+
expect(Logger_1.logger.warn).toHaveBeenCalled();
|
|
48
|
+
});
|
|
49
|
+
it("skips packages without sincPlugin export", function () {
|
|
50
|
+
mockReaddirSync.mockReturnValue(["sincronia-sass-plugin"]);
|
|
51
|
+
// sass-plugin exists but doesn't export sincPlugin — it just won't be added
|
|
52
|
+
const plugins = (0, discovery_1.discoverPlugins)();
|
|
53
|
+
const names = plugins.map(function (p) { return p.name; });
|
|
54
|
+
expect(names).not.toContain("sass-plugin");
|
|
55
|
+
});
|
|
56
|
+
it("skips sincronia-dashboard and sincronia-schema", function () {
|
|
57
|
+
Logger_1.logger.warn.mockClear();
|
|
58
|
+
mockReaddirSync.mockReturnValue([
|
|
59
|
+
"sincronia-dashboard",
|
|
60
|
+
"sincronia-schema",
|
|
61
|
+
]);
|
|
62
|
+
const plugins = (0, discovery_1.discoverPlugins)();
|
|
63
|
+
expect(plugins).toEqual([]);
|
|
64
|
+
// No require() attempted, so no warn from load failure
|
|
65
|
+
expect(Logger_1.logger.warn).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tests for US-006: Surface warning when no update set is configured
|
|
4
|
+
*
|
|
5
|
+
* Validates:
|
|
6
|
+
* - Prominent warning when readActiveTask() returns null and no update set configured
|
|
7
|
+
* - Warning message includes specific text about Default and remediation steps
|
|
8
|
+
* - Invalid scope name produces a clear error (not a global-scope update set)
|
|
9
|
+
* - scopeSysId validated before creating update set; null/undefined skips with error
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
// --- Mock setup ---
|
|
13
|
+
const mockSNClient = {
|
|
14
|
+
getScopeId: jest.fn(),
|
|
15
|
+
getUserSysId: jest.fn(),
|
|
16
|
+
getCurrentAppUserPrefSysId: jest.fn(),
|
|
17
|
+
updateCurrentAppUserPref: jest.fn(),
|
|
18
|
+
createCurrentAppUserPref: jest.fn(),
|
|
19
|
+
changeScope: jest.fn().mockResolvedValue(undefined),
|
|
20
|
+
createUpdateSet: jest.fn(),
|
|
21
|
+
changeUpdateSet: jest.fn(),
|
|
22
|
+
client: {
|
|
23
|
+
get: jest.fn(),
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
jest.mock("../snClient", () => ({
|
|
27
|
+
defaultClient: jest.fn(() => mockSNClient),
|
|
28
|
+
unwrapSNResponse: jest.fn((val) => val),
|
|
29
|
+
}));
|
|
30
|
+
jest.mock("../FileUtils", () => ({
|
|
31
|
+
getFileContextFromPath: jest.fn(),
|
|
32
|
+
getFileContextWithSkipReason: jest.fn(),
|
|
33
|
+
}));
|
|
34
|
+
jest.mock("../appUtils", () => ({
|
|
35
|
+
groupAppFiles: jest.fn(),
|
|
36
|
+
pushFiles: jest.fn(),
|
|
37
|
+
}));
|
|
38
|
+
jest.mock("../logMessages", () => ({
|
|
39
|
+
logFilePush: jest.fn(),
|
|
40
|
+
}));
|
|
41
|
+
jest.mock("../recentEdits", () => ({
|
|
42
|
+
writeRecentEdit: jest.fn(),
|
|
43
|
+
}));
|
|
44
|
+
jest.mock("../Logger", () => ({
|
|
45
|
+
logger: {
|
|
46
|
+
info: jest.fn(),
|
|
47
|
+
error: jest.fn(),
|
|
48
|
+
warn: jest.fn(),
|
|
49
|
+
debug: jest.fn(),
|
|
50
|
+
success: jest.fn(),
|
|
51
|
+
getLogLevel: jest.fn().mockReturnValue("info"),
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
jest.mock("../config", () => ({
|
|
55
|
+
loadConfigs: jest.fn().mockResolvedValue(undefined),
|
|
56
|
+
getConfig: jest.fn(),
|
|
57
|
+
getRootDir: jest.fn().mockReturnValue("/project"),
|
|
58
|
+
updateManifest: jest.fn(),
|
|
59
|
+
getManifest: jest.fn(),
|
|
60
|
+
getSourcePath: jest.fn().mockReturnValue("/project/src"),
|
|
61
|
+
getScopeManifestPath: jest.fn((scope) => `/project/dove.manifest.${scope}.json`),
|
|
62
|
+
getManifestPath: jest.fn().mockReturnValue("/project/dove.manifest.json"),
|
|
63
|
+
}));
|
|
64
|
+
// Filesystem mock store
|
|
65
|
+
var mockFsStore = {};
|
|
66
|
+
jest.mock("fs", () => ({
|
|
67
|
+
existsSync: jest.fn((p) => {
|
|
68
|
+
return p in mockFsStore;
|
|
69
|
+
}),
|
|
70
|
+
readFileSync: jest.fn((p) => {
|
|
71
|
+
if (p in mockFsStore)
|
|
72
|
+
return mockFsStore[p];
|
|
73
|
+
throw new Error("ENOENT: " + p);
|
|
74
|
+
}),
|
|
75
|
+
writeFileSync: jest.fn((p, data) => {
|
|
76
|
+
mockFsStore[p] = data;
|
|
77
|
+
}),
|
|
78
|
+
statSync: jest.fn(() => ({ mtimeMs: Date.now() })),
|
|
79
|
+
promises: {
|
|
80
|
+
readFile: jest.fn(),
|
|
81
|
+
writeFile: jest.fn(),
|
|
82
|
+
readdir: jest.fn(),
|
|
83
|
+
mkdir: jest.fn(),
|
|
84
|
+
access: jest.fn(),
|
|
85
|
+
stat: jest.fn(),
|
|
86
|
+
},
|
|
87
|
+
}));
|
|
88
|
+
jest.mock("chokidar", () => ({
|
|
89
|
+
watch: jest.fn(() => ({
|
|
90
|
+
on: jest.fn().mockReturnThis(),
|
|
91
|
+
close: jest.fn(),
|
|
92
|
+
})),
|
|
93
|
+
}));
|
|
94
|
+
jest.mock("lodash", () => {
|
|
95
|
+
var actual = jest.requireActual("lodash");
|
|
96
|
+
return {
|
|
97
|
+
...actual,
|
|
98
|
+
debounce: jest.fn((fn) => {
|
|
99
|
+
var wrapper = jest.fn();
|
|
100
|
+
wrapper.cancel = jest.fn();
|
|
101
|
+
wrapper.flush = jest.fn(() => fn());
|
|
102
|
+
return wrapper;
|
|
103
|
+
}),
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
jest.mock("axios", () => ({
|
|
107
|
+
default: {
|
|
108
|
+
create: jest.fn(() => ({
|
|
109
|
+
get: jest.fn().mockResolvedValue({ data: { result: null } }),
|
|
110
|
+
})),
|
|
111
|
+
},
|
|
112
|
+
}));
|
|
113
|
+
// --- Imports ---
|
|
114
|
+
const Logger_1 = require("../Logger");
|
|
115
|
+
const MultiScopeWatcher_1 = require("../MultiScopeWatcher");
|
|
116
|
+
// --- Tests ---
|
|
117
|
+
describe("US-006: Surface warning when no update set is configured", () => {
|
|
118
|
+
beforeEach(() => {
|
|
119
|
+
jest.clearAllMocks();
|
|
120
|
+
mockFsStore = {};
|
|
121
|
+
// Default: scope switching succeeds
|
|
122
|
+
mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "scope_sys_id" }]);
|
|
123
|
+
mockSNClient.getUserSysId.mockResolvedValue([{ sys_id: "user_sys_id" }]);
|
|
124
|
+
mockSNClient.getCurrentAppUserPrefSysId.mockResolvedValue([{ sys_id: "pref_sys_id" }]);
|
|
125
|
+
mockSNClient.updateCurrentAppUserPref.mockResolvedValue({});
|
|
126
|
+
mockSNClient.createCurrentAppUserPref.mockResolvedValue({});
|
|
127
|
+
});
|
|
128
|
+
afterEach(() => {
|
|
129
|
+
(0, MultiScopeWatcher_1.stopMultiScopeWatching)();
|
|
130
|
+
});
|
|
131
|
+
it("warns with specific message when no active task and no update set configured", async () => {
|
|
132
|
+
// No .dove-update-sets.json and no .dove-active-task.json
|
|
133
|
+
await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_test_core");
|
|
134
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("No update set configured for scope x_test_core"));
|
|
135
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("Changes will go to Default"));
|
|
136
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("dove createUpdateSet"));
|
|
137
|
+
expect(Logger_1.logger.warn).toHaveBeenCalledWith(expect.stringContaining("activate a task in the dashboard"));
|
|
138
|
+
});
|
|
139
|
+
it("does not warn when update set already configured for scope", async () => {
|
|
140
|
+
// Pre-populate the update set config
|
|
141
|
+
var configPath = require("path").resolve(process.cwd(), ".dove-update-sets.json");
|
|
142
|
+
mockFsStore[configPath] = JSON.stringify({
|
|
143
|
+
x_test_core: { sys_id: "us_123", name: "My Update Set" }
|
|
144
|
+
});
|
|
145
|
+
await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_test_core");
|
|
146
|
+
// Should return early — no warning
|
|
147
|
+
expect(Logger_1.logger.warn).not.toHaveBeenCalled();
|
|
148
|
+
expect(Logger_1.logger.error).not.toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
it("logs error and skips creation when scope not found on instance (scopeSysId is undefined)", async () => {
|
|
151
|
+
// Active task exists so we get past the no-task check
|
|
152
|
+
var taskPath = require("path").resolve(process.cwd(), ".dove-active-task.json");
|
|
153
|
+
mockFsStore[taskPath] = JSON.stringify({
|
|
154
|
+
taskId: "abc123",
|
|
155
|
+
taskName: "Test Task",
|
|
156
|
+
taskDescription: "Test",
|
|
157
|
+
updateSetName: "CU-abc123 Test Task",
|
|
158
|
+
description: "Test update set",
|
|
159
|
+
taskUrl: "https://example.com",
|
|
160
|
+
scopes: {},
|
|
161
|
+
});
|
|
162
|
+
// getScopeId returns empty — scope doesn't exist
|
|
163
|
+
mockSNClient.getScopeId.mockResolvedValue([]);
|
|
164
|
+
await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_invalid_scope");
|
|
165
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("x_invalid_scope"));
|
|
166
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("not found on the instance"));
|
|
167
|
+
// Should NOT attempt to create an update set
|
|
168
|
+
expect(mockSNClient.createUpdateSet).not.toHaveBeenCalled();
|
|
169
|
+
expect(mockSNClient.client.get).not.toHaveBeenCalled();
|
|
170
|
+
});
|
|
171
|
+
it("logs error and skips creation when getScopeId returns null", async () => {
|
|
172
|
+
var taskPath = require("path").resolve(process.cwd(), ".dove-active-task.json");
|
|
173
|
+
mockFsStore[taskPath] = JSON.stringify({
|
|
174
|
+
taskId: "abc123",
|
|
175
|
+
taskName: "Test Task",
|
|
176
|
+
taskDescription: "Test",
|
|
177
|
+
updateSetName: "CU-abc123 Test Task",
|
|
178
|
+
description: "Test update set",
|
|
179
|
+
taskUrl: "https://example.com",
|
|
180
|
+
scopes: {},
|
|
181
|
+
});
|
|
182
|
+
// getScopeId returns null
|
|
183
|
+
mockSNClient.getScopeId.mockResolvedValue(null);
|
|
184
|
+
await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_bad_scope");
|
|
185
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("x_bad_scope"));
|
|
186
|
+
expect(Logger_1.logger.error).toHaveBeenCalledWith(expect.stringContaining("not found on the instance"));
|
|
187
|
+
expect(mockSNClient.createUpdateSet).not.toHaveBeenCalled();
|
|
188
|
+
});
|
|
189
|
+
it("proceeds to create update set when scope is valid", async () => {
|
|
190
|
+
var taskPath = require("path").resolve(process.cwd(), ".dove-active-task.json");
|
|
191
|
+
mockFsStore[taskPath] = JSON.stringify({
|
|
192
|
+
taskId: "abc123",
|
|
193
|
+
taskName: "Test Task",
|
|
194
|
+
taskDescription: "Test",
|
|
195
|
+
updateSetName: "CU-abc123 Test Task",
|
|
196
|
+
description: "Test update set",
|
|
197
|
+
taskUrl: "https://example.com",
|
|
198
|
+
scopes: {},
|
|
199
|
+
});
|
|
200
|
+
// Valid scope
|
|
201
|
+
mockSNClient.getScopeId.mockResolvedValue([{ sys_id: "valid_scope_sys_id" }]);
|
|
202
|
+
// Search returns existing update set
|
|
203
|
+
mockSNClient.client.get.mockResolvedValue({
|
|
204
|
+
data: { result: [{ sys_id: "us_existing", name: "CU-abc123 Test Task", state: "in progress" }] }
|
|
205
|
+
});
|
|
206
|
+
// changeUpdateSet succeeds
|
|
207
|
+
mockSNClient.changeUpdateSet.mockResolvedValue(undefined);
|
|
208
|
+
await MultiScopeWatcher_1.multiScopeWatcher.ensureUpdateSetForScope("x_valid_scope");
|
|
209
|
+
// Should have searched for existing update sets (scope was valid)
|
|
210
|
+
expect(mockSNClient.client.get).toHaveBeenCalled();
|
|
211
|
+
// No "not found on the instance" error
|
|
212
|
+
var errorCalls = Logger_1.logger.error.mock.calls.map(function (c) { return c[0]; });
|
|
213
|
+
var hasInvalidScopeError = errorCalls.some(function (msg) {
|
|
214
|
+
return msg.indexOf("not found on the instance") !== -1;
|
|
215
|
+
});
|
|
216
|
+
expect(hasInvalidScopeError).toBe(false);
|
|
217
|
+
});
|
|
218
|
+
});
|