@tenonhq/dovetail-core 0.0.121 → 0.0.123
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 +9 -1
- package/dist/allScopesCommands.js +8 -0
- package/dist/claudeSession.js +58 -0
- package/dist/commander.js +13 -3
- package/dist/createRecordCommand.js +232 -8
- package/dist/snClient.js +15 -0
- package/dist/tests/claudeSession.test.js +42 -0
- package/dist/tests/createRecordUpdateSet.test.js +276 -0
- package/dist/tests/watchHidden.test.js +146 -0
- package/package.json +1 -1
- package/release-manifest.json +246 -292
- package/skills/dove-manage-update-sets.md +4 -4
- package/skills/dove-setup-project.md +12 -6
- package/skills/dove-troubleshoot-sync.md +3 -3
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tests for `dove create sys_update_set` (TenonHQ/Dovetail#231).
|
|
3
|
+
//
|
|
4
|
+
// A generic record insert into sys_update_set lets ServiceNow default
|
|
5
|
+
// `application` to the API session's current app, so `--scope` was silently
|
|
6
|
+
// ignored. `dove create` must instead:
|
|
7
|
+
// 1. route sys_update_set through snClient.createUpdateSet with the resolved
|
|
8
|
+
// scope's sys_id — and NOT call the generic createRecord;
|
|
9
|
+
// 2. read the set back and exit non-zero, naming requested vs. actual scope
|
|
10
|
+
// and the set sys_id, when the application does not match;
|
|
11
|
+
// 3. require --scope for sys_update_set in --ci mode;
|
|
12
|
+
// 4. leave every other table on the generic createRecord path.
|
|
13
|
+
//
|
|
14
|
+
// Everything is mocked — no instance is contacted.
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
var logMessages = [];
|
|
17
|
+
function pushLog(level) {
|
|
18
|
+
return jest.fn(function (msg) {
|
|
19
|
+
logMessages.push({ level: level, msg: String(msg) });
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
jest.mock("../Logger", function () {
|
|
23
|
+
return {
|
|
24
|
+
logger: {
|
|
25
|
+
setLogLevel: jest.fn(),
|
|
26
|
+
success: pushLog("success"),
|
|
27
|
+
info: pushLog("info"),
|
|
28
|
+
error: pushLog("error"),
|
|
29
|
+
warn: pushLog("warn"),
|
|
30
|
+
debug: jest.fn(),
|
|
31
|
+
getLogLevel: function () { return "info"; },
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
jest.mock("../FileLogger", function () {
|
|
36
|
+
return {
|
|
37
|
+
fileLogger: { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn() },
|
|
38
|
+
};
|
|
39
|
+
});
|
|
40
|
+
jest.mock("../commands", function () {
|
|
41
|
+
return { setLogLevel: jest.fn() };
|
|
42
|
+
});
|
|
43
|
+
jest.mock("../config", function () {
|
|
44
|
+
return {
|
|
45
|
+
getManifest: jest.fn(function () { return undefined; }),
|
|
46
|
+
isMultiScopeManifest: jest.fn(function () { return false; }),
|
|
47
|
+
getConfig: jest.fn(function () { return {}; }),
|
|
48
|
+
getSourcePathForScope: jest.fn(function () { return "/virtual/src"; }),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
var mockSyncManifest = jest.fn().mockResolvedValue(undefined);
|
|
52
|
+
jest.mock("../appUtils", function () {
|
|
53
|
+
return { syncManifest: mockSyncManifest };
|
|
54
|
+
});
|
|
55
|
+
jest.mock("../projectFiles", function () {
|
|
56
|
+
return {
|
|
57
|
+
getUpdateSetsConfigPath: jest.fn(function () {
|
|
58
|
+
return "/nonexistent/dove-create-update-set-test/.dove-update-sets.json";
|
|
59
|
+
}),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
jest.mock("inquirer", function () {
|
|
63
|
+
return { prompt: jest.fn() };
|
|
64
|
+
});
|
|
65
|
+
var mockClient = {
|
|
66
|
+
getScopeId: jest.fn(),
|
|
67
|
+
getScopeById: jest.fn(),
|
|
68
|
+
getUpdateSetById: jest.fn(),
|
|
69
|
+
createUpdateSet: jest.fn(),
|
|
70
|
+
createRecord: jest.fn(),
|
|
71
|
+
};
|
|
72
|
+
jest.mock("../snClient", function () {
|
|
73
|
+
return {
|
|
74
|
+
defaultClient: jest.fn(function () { return mockClient; }),
|
|
75
|
+
// Mirror the real unwrapSNResponse: await the axios promise, return .data.result
|
|
76
|
+
unwrapSNResponse: jest.fn(async function (p) {
|
|
77
|
+
var resp = await p;
|
|
78
|
+
return resp.data.result;
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
|
+
const createRecordCommand_1 = require("../createRecordCommand");
|
|
83
|
+
function axiosResult(result) {
|
|
84
|
+
return Promise.resolve({ status: 200, data: { result: result } });
|
|
85
|
+
}
|
|
86
|
+
var REQUESTED_SCOPE = "x_cadso_journey";
|
|
87
|
+
var REQUESTED_SCOPE_SYS_ID = "journeyScopeSysId0000000000000001";
|
|
88
|
+
var SESSION_SCOPE = "x_cadso_core";
|
|
89
|
+
var SESSION_SCOPE_SYS_ID = "coreScopeSysId00000000000000000002";
|
|
90
|
+
var SET_SYS_ID = "updateSetSysId000000000000000000ab";
|
|
91
|
+
class ExitSignal extends Error {
|
|
92
|
+
code;
|
|
93
|
+
constructor(code) {
|
|
94
|
+
super("process.exit(" + code + ")");
|
|
95
|
+
this.code = code;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
describe("dove create sys_update_set (#231)", function () {
|
|
99
|
+
var exitSpy;
|
|
100
|
+
beforeEach(function () {
|
|
101
|
+
logMessages = [];
|
|
102
|
+
jest.clearAllMocks();
|
|
103
|
+
exitSpy = jest
|
|
104
|
+
.spyOn(process, "exit")
|
|
105
|
+
.mockImplementation(function (code) {
|
|
106
|
+
throw new ExitSignal(typeof code === "number" ? code : undefined);
|
|
107
|
+
});
|
|
108
|
+
mockClient.getScopeId.mockImplementation(function (scopeName) {
|
|
109
|
+
if (scopeName === REQUESTED_SCOPE) {
|
|
110
|
+
return axiosResult([{ sys_id: REQUESTED_SCOPE_SYS_ID }]);
|
|
111
|
+
}
|
|
112
|
+
return axiosResult([]);
|
|
113
|
+
});
|
|
114
|
+
mockClient.getScopeById.mockImplementation(function (sysId) {
|
|
115
|
+
if (sysId === SESSION_SCOPE_SYS_ID) {
|
|
116
|
+
return axiosResult([{ sys_id: SESSION_SCOPE_SYS_ID, scope: SESSION_SCOPE }]);
|
|
117
|
+
}
|
|
118
|
+
if (sysId === REQUESTED_SCOPE_SYS_ID) {
|
|
119
|
+
return axiosResult([{ sys_id: REQUESTED_SCOPE_SYS_ID, scope: REQUESTED_SCOPE }]);
|
|
120
|
+
}
|
|
121
|
+
return axiosResult([]);
|
|
122
|
+
});
|
|
123
|
+
mockClient.createUpdateSet.mockImplementation(function (name, scopeSysId) {
|
|
124
|
+
return axiosResult({ sys_id: SET_SYS_ID, name: name, application: scopeSysId });
|
|
125
|
+
});
|
|
126
|
+
mockClient.createRecord.mockResolvedValue({
|
|
127
|
+
status: 200,
|
|
128
|
+
data: { result: { sys_id: "genericSysId", table: "sys_script_include", name: "Generic" } },
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
afterEach(function () {
|
|
132
|
+
exitSpy.mockRestore();
|
|
133
|
+
});
|
|
134
|
+
function errorText() {
|
|
135
|
+
return logMessages
|
|
136
|
+
.filter(function (m) { return m.level === "error"; })
|
|
137
|
+
.map(function (m) { return m.msg; })
|
|
138
|
+
.join("\n");
|
|
139
|
+
}
|
|
140
|
+
it("routes through createUpdateSet with the requested scope and never calls the generic insert", async function () {
|
|
141
|
+
// Table API shape for a reference field without display values: { value, link }
|
|
142
|
+
mockClient.getUpdateSetById.mockReturnValue(axiosResult([{ sys_id: SET_SYS_ID, application: { value: REQUESTED_SCOPE_SYS_ID, link: "ref-link" } }]));
|
|
143
|
+
await (0, createRecordCommand_1.createRecordCommand)({
|
|
144
|
+
table: "sys_update_set",
|
|
145
|
+
name: "Journey - Fix 231",
|
|
146
|
+
scope: REQUESTED_SCOPE,
|
|
147
|
+
field: ["description=Issue 231 regression set"],
|
|
148
|
+
ci: true,
|
|
149
|
+
logLevel: "info",
|
|
150
|
+
});
|
|
151
|
+
expect(mockClient.createUpdateSet).toHaveBeenCalledTimes(1);
|
|
152
|
+
expect(mockClient.createUpdateSet).toHaveBeenCalledWith("Journey - Fix 231", REQUESTED_SCOPE_SYS_ID, "Issue 231 regression set");
|
|
153
|
+
expect(mockClient.createRecord).not.toHaveBeenCalled();
|
|
154
|
+
expect(mockClient.getUpdateSetById).toHaveBeenCalledWith(SET_SYS_ID);
|
|
155
|
+
expect(mockSyncManifest).not.toHaveBeenCalled();
|
|
156
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
157
|
+
var success = logMessages.filter(function (m) { return m.level === "success"; });
|
|
158
|
+
expect(success.length).toBe(1);
|
|
159
|
+
expect(success[0].msg).toContain(SET_SYS_ID);
|
|
160
|
+
expect(success[0].msg).toContain(REQUESTED_SCOPE);
|
|
161
|
+
});
|
|
162
|
+
it("fails loudly (exit 1, no success) when the created set's application is not the requested scope", async function () {
|
|
163
|
+
// The op "succeeded" but the set landed in the session's current app.
|
|
164
|
+
mockClient.createUpdateSet.mockImplementation(function (name) {
|
|
165
|
+
return axiosResult({ sys_id: SET_SYS_ID, name: name, application: SESSION_SCOPE_SYS_ID });
|
|
166
|
+
});
|
|
167
|
+
mockClient.getUpdateSetById.mockReturnValue(axiosResult([{ sys_id: SET_SYS_ID, application: { value: SESSION_SCOPE_SYS_ID } }]));
|
|
168
|
+
await expect((0, createRecordCommand_1.createRecordCommand)({
|
|
169
|
+
table: "sys_update_set",
|
|
170
|
+
name: "Journey - Fix 231",
|
|
171
|
+
scope: REQUESTED_SCOPE,
|
|
172
|
+
ci: true,
|
|
173
|
+
logLevel: "info",
|
|
174
|
+
})).rejects.toBeInstanceOf(ExitSignal);
|
|
175
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
176
|
+
expect(mockClient.createRecord).not.toHaveBeenCalled();
|
|
177
|
+
expect(logMessages.filter(function (m) { return m.level === "success"; })).toHaveLength(0);
|
|
178
|
+
var errors = errorText();
|
|
179
|
+
expect(errors).toContain(SET_SYS_ID);
|
|
180
|
+
expect(errors).toContain(REQUESTED_SCOPE);
|
|
181
|
+
expect(errors).toContain(SESSION_SCOPE);
|
|
182
|
+
expect(errors).toContain(REQUESTED_SCOPE_SYS_ID);
|
|
183
|
+
expect(errors).toContain(SESSION_SCOPE_SYS_ID);
|
|
184
|
+
});
|
|
185
|
+
it("falls back to the op response when read-back is unavailable, and still catches a mismatch", async function () {
|
|
186
|
+
mockClient.createUpdateSet.mockImplementation(function (name) {
|
|
187
|
+
return axiosResult({ sys_id: SET_SYS_ID, name: name, application: SESSION_SCOPE_SYS_ID });
|
|
188
|
+
});
|
|
189
|
+
mockClient.getUpdateSetById.mockReturnValue(Promise.reject(new Error("read denied")));
|
|
190
|
+
await expect((0, createRecordCommand_1.createRecordCommand)({
|
|
191
|
+
table: "sys_update_set",
|
|
192
|
+
name: "Journey - Fix 231",
|
|
193
|
+
scope: REQUESTED_SCOPE,
|
|
194
|
+
ci: true,
|
|
195
|
+
logLevel: "info",
|
|
196
|
+
})).rejects.toBeInstanceOf(ExitSignal);
|
|
197
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
198
|
+
expect(errorText()).toContain(SESSION_SCOPE);
|
|
199
|
+
expect(errorText()).toContain(SET_SYS_ID);
|
|
200
|
+
});
|
|
201
|
+
it("refuses to verify-by-assumption: no application from any source is a failure, not a success", async function () {
|
|
202
|
+
mockClient.createUpdateSet.mockImplementation(function (name) {
|
|
203
|
+
return axiosResult({ sys_id: SET_SYS_ID, name: name });
|
|
204
|
+
});
|
|
205
|
+
mockClient.getUpdateSetById.mockReturnValue(axiosResult([{ sys_id: SET_SYS_ID }]));
|
|
206
|
+
await expect((0, createRecordCommand_1.createRecordCommand)({
|
|
207
|
+
table: "sys_update_set",
|
|
208
|
+
name: "Journey - Fix 231",
|
|
209
|
+
scope: REQUESTED_SCOPE,
|
|
210
|
+
ci: true,
|
|
211
|
+
logLevel: "info",
|
|
212
|
+
})).rejects.toBeInstanceOf(ExitSignal);
|
|
213
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
214
|
+
expect(errorText()).toContain("could not be verified");
|
|
215
|
+
expect(logMessages.filter(function (m) { return m.level === "success"; })).toHaveLength(0);
|
|
216
|
+
});
|
|
217
|
+
it("requires --scope for sys_update_set in --ci mode before touching the instance", async function () {
|
|
218
|
+
await expect((0, createRecordCommand_1.createRecordCommand)({
|
|
219
|
+
table: "sys_update_set",
|
|
220
|
+
name: "No Scope Set",
|
|
221
|
+
ci: true,
|
|
222
|
+
logLevel: "info",
|
|
223
|
+
})).rejects.toBeInstanceOf(ExitSignal);
|
|
224
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
225
|
+
expect(mockClient.createUpdateSet).not.toHaveBeenCalled();
|
|
226
|
+
expect(mockClient.createRecord).not.toHaveBeenCalled();
|
|
227
|
+
expect(mockClient.getScopeId).not.toHaveBeenCalled();
|
|
228
|
+
expect(errorText()).toContain("--scope");
|
|
229
|
+
});
|
|
230
|
+
it("errors when the requested scope does not exist on the instance", async function () {
|
|
231
|
+
await expect((0, createRecordCommand_1.createRecordCommand)({
|
|
232
|
+
table: "sys_update_set",
|
|
233
|
+
name: "Ghost Scope Set",
|
|
234
|
+
scope: "x_cadso_nope",
|
|
235
|
+
ci: true,
|
|
236
|
+
logLevel: "info",
|
|
237
|
+
})).rejects.toBeInstanceOf(ExitSignal);
|
|
238
|
+
expect(mockClient.createUpdateSet).not.toHaveBeenCalled();
|
|
239
|
+
expect(errorText()).toContain("x_cadso_nope");
|
|
240
|
+
});
|
|
241
|
+
it("keeps every other table on the generic createRecord path", async function () {
|
|
242
|
+
await (0, createRecordCommand_1.createRecordCommand)({
|
|
243
|
+
table: "sys_script_include",
|
|
244
|
+
name: "Generic",
|
|
245
|
+
scope: REQUESTED_SCOPE,
|
|
246
|
+
ci: true,
|
|
247
|
+
refresh: false,
|
|
248
|
+
logLevel: "info",
|
|
249
|
+
});
|
|
250
|
+
expect(mockClient.createRecord).toHaveBeenCalledTimes(1);
|
|
251
|
+
expect(mockClient.createRecord).toHaveBeenCalledWith({
|
|
252
|
+
table: "sys_script_include",
|
|
253
|
+
fields: { name: "Generic" },
|
|
254
|
+
scope: REQUESTED_SCOPE,
|
|
255
|
+
update_set_sys_id: undefined,
|
|
256
|
+
});
|
|
257
|
+
expect(mockClient.createUpdateSet).not.toHaveBeenCalled();
|
|
258
|
+
expect(mockClient.getScopeId).not.toHaveBeenCalled();
|
|
259
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
260
|
+
});
|
|
261
|
+
it("exposes a typed mismatch error carrying both scopes", function () {
|
|
262
|
+
var err = new createRecordCommand_1.UpdateSetScopeMismatchError({
|
|
263
|
+
updateSetName: "n",
|
|
264
|
+
updateSetSysId: "s",
|
|
265
|
+
requestedScope: REQUESTED_SCOPE,
|
|
266
|
+
requestedScopeSysId: REQUESTED_SCOPE_SYS_ID,
|
|
267
|
+
actualScope: SESSION_SCOPE,
|
|
268
|
+
actualScopeSysId: SESSION_SCOPE_SYS_ID,
|
|
269
|
+
});
|
|
270
|
+
expect(err).toBeInstanceOf(Error);
|
|
271
|
+
expect(err.name).toBe("UpdateSetScopeMismatchError");
|
|
272
|
+
expect(err.requestedScope).toBe(REQUESTED_SCOPE);
|
|
273
|
+
expect(err.actualScope).toBe(SESSION_SCOPE);
|
|
274
|
+
expect(err.message).toContain("MIS-SCOPED");
|
|
275
|
+
});
|
|
276
|
+
});
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// `dove watch` is hidden from the `--help` command index but stays wired for
|
|
3
|
+
// humans (TenonHQ/Dovetail#155). These tests pin both halves: the help text
|
|
4
|
+
// never lists `watch` / `w` / `watchAllScopes`, and all three spellings still
|
|
5
|
+
// route to the watch handler. Mocks mirror commanderStrict.test.ts — the parser
|
|
6
|
+
// is under test, not the commands, which would hit ServiceNow.
|
|
7
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
8
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
9
|
+
};
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
jest.mock("../commands", () => ({
|
|
12
|
+
refreshCommand: jest.fn(),
|
|
13
|
+
pushCommand: jest.fn(),
|
|
14
|
+
downloadCommand: jest.fn(),
|
|
15
|
+
initCommand: jest.fn(),
|
|
16
|
+
buildCommand: jest.fn(),
|
|
17
|
+
deployCommand: jest.fn(),
|
|
18
|
+
statusCommand: jest.fn(),
|
|
19
|
+
taskClearCommand: jest.fn(),
|
|
20
|
+
}));
|
|
21
|
+
jest.mock("../allScopesCommands", () => ({
|
|
22
|
+
initScopesCommand: jest.fn(),
|
|
23
|
+
watchAllScopesCommand: jest.fn(),
|
|
24
|
+
}));
|
|
25
|
+
jest.mock("../updateSetCommands", () => ({
|
|
26
|
+
createUpdateSetCommand: jest.fn(),
|
|
27
|
+
switchUpdateSetCommand: jest.fn(),
|
|
28
|
+
listUpdateSetsCommand: jest.fn(),
|
|
29
|
+
showCurrentUpdateSetCommand: jest.fn(),
|
|
30
|
+
changeScopeCommand: jest.fn(),
|
|
31
|
+
showCurrentScopeCommand: jest.fn(),
|
|
32
|
+
}));
|
|
33
|
+
jest.mock("../dashboardCommand", () => ({ dashboardCommand: jest.fn() }));
|
|
34
|
+
jest.mock("../schemaCommand", () => ({
|
|
35
|
+
schemaPullCommand: jest.fn(),
|
|
36
|
+
schemaDiffCommand: jest.fn(),
|
|
37
|
+
schemaSnapshotsCommand: jest.fn(),
|
|
38
|
+
}));
|
|
39
|
+
jest.mock("../claudeCommand", () => ({ initClaudeCommand: jest.fn() }));
|
|
40
|
+
jest.mock("../createRecordCommand", () => ({ createRecordCommand: jest.fn() }));
|
|
41
|
+
jest.mock("../deleteRecordCommand", () => ({ deleteRecordCommand: jest.fn() }));
|
|
42
|
+
jest.mock("../reconcileCommand", () => ({ reconcileCommand: jest.fn() }));
|
|
43
|
+
jest.mock("../migrateCommand", () => ({ migrateCommand: jest.fn() }));
|
|
44
|
+
jest.mock("../loginCommand", () => ({ loginCommand: jest.fn() }));
|
|
45
|
+
jest.mock("../knowledgeDiffCommand", () => ({ knowledgeDiffCommand: jest.fn() }));
|
|
46
|
+
jest.mock("../clickupCommands", () => ({
|
|
47
|
+
clickupTasksCommand: jest.fn(),
|
|
48
|
+
clickupTaskCommand: jest.fn(),
|
|
49
|
+
clickupCreateCommand: jest.fn(),
|
|
50
|
+
clickupUpdateCommand: jest.fn(),
|
|
51
|
+
clickupCommentCommand: jest.fn(),
|
|
52
|
+
clickupSetupCommand: jest.fn(),
|
|
53
|
+
clickupTeamsCommand: jest.fn(),
|
|
54
|
+
clickupSpacesCommand: jest.fn(),
|
|
55
|
+
clickupListsCommand: jest.fn(),
|
|
56
|
+
}));
|
|
57
|
+
// --- Imports (after mocks) ---
|
|
58
|
+
const yargs_1 = __importDefault(require("yargs/yargs"));
|
|
59
|
+
const commander_1 = require("../commander");
|
|
60
|
+
const allScopesCommands_1 = require("../allScopesCommands");
|
|
61
|
+
/**
|
|
62
|
+
* @description Runs a fake `dove` invocation through a fresh, non-exiting parser.
|
|
63
|
+
* @param {string[]} args - Argv as the user would type it, minus the binary name.
|
|
64
|
+
* @returns {Promise<ParseResult>} The parse error (if any) and yargs' captured output.
|
|
65
|
+
*/
|
|
66
|
+
async function run(args) {
|
|
67
|
+
const cli = (0, commander_1.configureCli)((0, yargs_1.default)([])).exitProcess(false);
|
|
68
|
+
const result = await new Promise(function (resolve) {
|
|
69
|
+
cli.parse(args, {}, function (error, _argv, output) {
|
|
70
|
+
resolve({
|
|
71
|
+
error: error === null ? undefined : error,
|
|
72
|
+
output: output || "",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
await new Promise(function (resolve) {
|
|
77
|
+
setImmediate(resolve);
|
|
78
|
+
});
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* @description Extracts the `dove <cmd>` command names listed in a yargs help
|
|
83
|
+
* body (the "Commands:" index), so assertions target the index rather than
|
|
84
|
+
* incidental prose in option descriptions.
|
|
85
|
+
* @param {string} help - Captured `--help` output.
|
|
86
|
+
* @returns {string[]} Command tokens exactly as the index prints them.
|
|
87
|
+
*/
|
|
88
|
+
function listedCommands(help) {
|
|
89
|
+
const names = [];
|
|
90
|
+
const lines = help.split("\n");
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
const match = /^\s*dove\s+(\S+)/.exec(lines[i]);
|
|
93
|
+
if (match)
|
|
94
|
+
names.push(match[1]);
|
|
95
|
+
}
|
|
96
|
+
return names;
|
|
97
|
+
}
|
|
98
|
+
beforeEach(function () {
|
|
99
|
+
jest.clearAllMocks();
|
|
100
|
+
});
|
|
101
|
+
describe("dove --help hides watch", function () {
|
|
102
|
+
it("does not list watch, w, or watchAllScopes in the command index", async function () {
|
|
103
|
+
const { output } = await run(["--help"]);
|
|
104
|
+
const commands = listedCommands(output);
|
|
105
|
+
expect(commands.length).toBeGreaterThan(0);
|
|
106
|
+
expect(commands).not.toContain("watch");
|
|
107
|
+
expect(commands).not.toContain("w");
|
|
108
|
+
expect(commands).not.toContain("watchAllScopes");
|
|
109
|
+
expect(output).not.toMatch(/\bwatchAllScopes\b/);
|
|
110
|
+
expect(output).not.toMatch(/\bdove\s+w(atch)?\b/);
|
|
111
|
+
});
|
|
112
|
+
it("still lists the safe headless verbs", async function () {
|
|
113
|
+
const { output } = await run(["--help"]);
|
|
114
|
+
const commands = listedCommands(output);
|
|
115
|
+
expect(commands).toContain("refresh");
|
|
116
|
+
expect(commands).toContain("push");
|
|
117
|
+
expect(commands).toContain("status");
|
|
118
|
+
});
|
|
119
|
+
it("does not surface watch in the bare-invocation help either", async function () {
|
|
120
|
+
const { output, error } = await run([]);
|
|
121
|
+
expect(error).toBeDefined();
|
|
122
|
+
expect(output + String(error)).not.toMatch(/\bwatchAllScopes\b/);
|
|
123
|
+
expect(listedCommands(output)).not.toContain("watch");
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
describe("dove watch stays wired for humans", function () {
|
|
127
|
+
it("routes `watch` to the watch handler", async function () {
|
|
128
|
+
const { error } = await run(["watch"]);
|
|
129
|
+
expect(error).toBeUndefined();
|
|
130
|
+
expect(allScopesCommands_1.watchAllScopesCommand).toHaveBeenCalledTimes(1);
|
|
131
|
+
});
|
|
132
|
+
it("routes the `w` and `watchAllScopes` aliases too", async function () {
|
|
133
|
+
await run(["w"]);
|
|
134
|
+
expect(allScopesCommands_1.watchAllScopesCommand).toHaveBeenCalledTimes(1);
|
|
135
|
+
await run(["watchAllScopes"]);
|
|
136
|
+
expect(allScopesCommands_1.watchAllScopesCommand).toHaveBeenCalledTimes(2);
|
|
137
|
+
});
|
|
138
|
+
it("passes watch flags through", async function () {
|
|
139
|
+
await run(["watch", "--noDashboard", "--port", "4000"]);
|
|
140
|
+
expect(allScopesCommands_1.watchAllScopesCommand).toHaveBeenCalledTimes(1);
|
|
141
|
+
const mock = allScopesCommands_1.watchAllScopesCommand;
|
|
142
|
+
const args = mock.mock.calls[0][0];
|
|
143
|
+
expect(args.noDashboard).toBe(true);
|
|
144
|
+
expect(args.port).toBe(4000);
|
|
145
|
+
});
|
|
146
|
+
});
|