@dainprotocol/cli 1.2.26 → 1.2.30
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/dist/__tests__/build.test.js +289 -0
- package/dist/__tests__/deploy.test.js +126 -0
- package/dist/__tests__/dev.test.js +321 -0
- package/dist/__tests__/init.test.js +290 -0
- package/dist/__tests__/integration.test.js +134 -0
- package/dist/__tests__/logs.test.js +135 -0
- package/dist/__tests__/testchat.test.js +214 -0
- package/dist/__tests__/utils.test.js +324 -0
- package/dist/commands/build.js +28 -61
- package/dist/commands/deploy.js +123 -162
- package/dist/commands/dev.js +118 -164
- package/dist/commands/init.js +2 -9
- package/dist/commands/logs.js +94 -88
- package/dist/commands/start.js +15 -4
- package/dist/commands/status.js +27 -31
- package/dist/commands/undeploy.js +29 -41
- package/dist/index.js +0 -7
- package/dist/templates/default/dain.json +1 -1
- package/dist/utils.js +112 -37
- package/package.json +12 -5
|
@@ -0,0 +1,321 @@
|
|
|
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
|
+
var path_1 = __importDefault(require("path"));
|
|
7
|
+
/**
|
|
8
|
+
* Dev command tests - security, port handling, process management, file watching
|
|
9
|
+
*/
|
|
10
|
+
describe('dev.ts port handling', function () {
|
|
11
|
+
describe('port resolution priority', function () {
|
|
12
|
+
function resolvePort(envPort, cliPort, defaultPort) {
|
|
13
|
+
if (defaultPort === void 0) { defaultPort = '2022'; }
|
|
14
|
+
if (envPort)
|
|
15
|
+
return { port: envPort, source: '.env file' };
|
|
16
|
+
if (cliPort)
|
|
17
|
+
return { port: cliPort, source: 'command line argument' };
|
|
18
|
+
return { port: defaultPort, source: 'default value' };
|
|
19
|
+
}
|
|
20
|
+
test('env port takes highest priority', function () {
|
|
21
|
+
var result = resolvePort('3000', '4000');
|
|
22
|
+
expect(result.port).toBe('3000');
|
|
23
|
+
expect(result.source).toBe('.env file');
|
|
24
|
+
});
|
|
25
|
+
test('CLI port used when no env port', function () {
|
|
26
|
+
var result = resolvePort(undefined, '4000');
|
|
27
|
+
expect(result.port).toBe('4000');
|
|
28
|
+
expect(result.source).toBe('command line argument');
|
|
29
|
+
});
|
|
30
|
+
test('default port used when no env or CLI port', function () {
|
|
31
|
+
var result = resolvePort(undefined, undefined);
|
|
32
|
+
expect(result.port).toBe('2022');
|
|
33
|
+
expect(result.source).toBe('default value');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('port validation', function () {
|
|
37
|
+
function validatePortNumber(port) {
|
|
38
|
+
var portNumber = parseInt(port, 10);
|
|
39
|
+
var valid = !isNaN(portNumber) && portNumber >= 1 && portNumber <= 65535;
|
|
40
|
+
return { valid: valid, parsed: portNumber };
|
|
41
|
+
}
|
|
42
|
+
test('accepts port 1', function () {
|
|
43
|
+
expect(validatePortNumber('1').valid).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
test('accepts port 65535', function () {
|
|
46
|
+
expect(validatePortNumber('65535').valid).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
test('accepts common ports', function () {
|
|
49
|
+
expect(validatePortNumber('80').valid).toBe(true);
|
|
50
|
+
expect(validatePortNumber('443').valid).toBe(true);
|
|
51
|
+
expect(validatePortNumber('3000').valid).toBe(true);
|
|
52
|
+
expect(validatePortNumber('8080').valid).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
test('rejects port 0', function () {
|
|
55
|
+
expect(validatePortNumber('0').valid).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
test('rejects negative ports', function () {
|
|
58
|
+
expect(validatePortNumber('-1').valid).toBe(false);
|
|
59
|
+
expect(validatePortNumber('-80').valid).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
test('rejects ports above 65535', function () {
|
|
62
|
+
expect(validatePortNumber('65536').valid).toBe(false);
|
|
63
|
+
expect(validatePortNumber('70000').valid).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
test('rejects non-numeric strings', function () {
|
|
66
|
+
expect(validatePortNumber('abc').valid).toBe(false);
|
|
67
|
+
expect(validatePortNumber('').valid).toBe(false);
|
|
68
|
+
expect(validatePortNumber('3.14').parsed).toBe(3); // parseInt truncates
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('dev.ts environment variables', function () {
|
|
73
|
+
test('builds correct env vars object', function () {
|
|
74
|
+
var port = '3000';
|
|
75
|
+
var config = {
|
|
76
|
+
'api-key': 'sk_test_123',
|
|
77
|
+
'project-id': 'proj_abc',
|
|
78
|
+
environment: 'development',
|
|
79
|
+
'out-dir': 'dist',
|
|
80
|
+
};
|
|
81
|
+
var envVars = {
|
|
82
|
+
PORT: port,
|
|
83
|
+
DAIN_API_KEY: config['api-key'],
|
|
84
|
+
DAIN_PROJECT_ID: config['project-id'],
|
|
85
|
+
DAIN_ENVIRONMENT: config.environment,
|
|
86
|
+
DAIN_OUT_DIR: config['out-dir'],
|
|
87
|
+
};
|
|
88
|
+
expect(envVars.PORT).toBe('3000');
|
|
89
|
+
expect(envVars.DAIN_API_KEY).toBe('sk_test_123');
|
|
90
|
+
expect(envVars.DAIN_PROJECT_ID).toBe('proj_abc');
|
|
91
|
+
expect(envVars.DAIN_ENVIRONMENT).toBe('development');
|
|
92
|
+
expect(envVars.DAIN_OUT_DIR).toBe('dist');
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe('dev.ts file watching', function () {
|
|
96
|
+
describe('watch paths calculation', function () {
|
|
97
|
+
test('includes main file directory', function () {
|
|
98
|
+
var mainFile = 'src/index.ts';
|
|
99
|
+
var watchPath = path_1.default.dirname(mainFile);
|
|
100
|
+
expect(watchPath).toBe('src');
|
|
101
|
+
});
|
|
102
|
+
test('includes static directory from config', function () {
|
|
103
|
+
var cwd = '/project';
|
|
104
|
+
var config = { 'static-dir': 'public' };
|
|
105
|
+
var staticPath = path_1.default.join(cwd, config['static-dir']);
|
|
106
|
+
expect(staticPath).toBe('/project/public');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
describe('chokidar ignore pattern', function () {
|
|
110
|
+
test('ignores dotfiles and dotdirs', function () {
|
|
111
|
+
var pattern = /(^|[\/\\])\../;
|
|
112
|
+
expect(pattern.test('.git')).toBe(true);
|
|
113
|
+
expect(pattern.test('.env')).toBe(true);
|
|
114
|
+
expect(pattern.test('src/.hidden')).toBe(true);
|
|
115
|
+
expect(pattern.test('/path/to/.config')).toBe(true);
|
|
116
|
+
expect(pattern.test('src/index.ts')).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe('debounce timer', function () {
|
|
120
|
+
beforeEach(function () {
|
|
121
|
+
jest.useFakeTimers();
|
|
122
|
+
});
|
|
123
|
+
afterEach(function () {
|
|
124
|
+
jest.useRealTimers();
|
|
125
|
+
});
|
|
126
|
+
test('debounces rapid file changes', function () {
|
|
127
|
+
var callCount = 0;
|
|
128
|
+
var debounceTimer = null;
|
|
129
|
+
var handleChange = function () {
|
|
130
|
+
if (debounceTimer)
|
|
131
|
+
clearTimeout(debounceTimer);
|
|
132
|
+
debounceTimer = setTimeout(function () {
|
|
133
|
+
callCount++;
|
|
134
|
+
}, 300);
|
|
135
|
+
};
|
|
136
|
+
// Rapid file changes
|
|
137
|
+
handleChange();
|
|
138
|
+
handleChange();
|
|
139
|
+
handleChange();
|
|
140
|
+
expect(callCount).toBe(0);
|
|
141
|
+
jest.advanceTimersByTime(300);
|
|
142
|
+
expect(callCount).toBe(1);
|
|
143
|
+
});
|
|
144
|
+
test('each new change resets debounce', function () {
|
|
145
|
+
var callCount = 0;
|
|
146
|
+
var debounceTimer = null;
|
|
147
|
+
var handleChange = function () {
|
|
148
|
+
if (debounceTimer)
|
|
149
|
+
clearTimeout(debounceTimer);
|
|
150
|
+
debounceTimer = setTimeout(function () {
|
|
151
|
+
callCount++;
|
|
152
|
+
}, 300);
|
|
153
|
+
};
|
|
154
|
+
handleChange();
|
|
155
|
+
jest.advanceTimersByTime(200);
|
|
156
|
+
handleChange(); // Reset timer
|
|
157
|
+
jest.advanceTimersByTime(200);
|
|
158
|
+
handleChange(); // Reset timer again
|
|
159
|
+
jest.advanceTimersByTime(300);
|
|
160
|
+
expect(callCount).toBe(1);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('dev.ts workers runtime', function () {
|
|
165
|
+
test('constructs correct output file path', function () {
|
|
166
|
+
var cwd = '/project';
|
|
167
|
+
var mainFile = 'src/index.ts';
|
|
168
|
+
var dainDir = path_1.default.join(cwd, '.dain');
|
|
169
|
+
var outFile = path_1.default.join(dainDir, path_1.default.basename(mainFile, '.ts') + '.mjs');
|
|
170
|
+
expect(outFile).toBe('/project/.dain/index.mjs');
|
|
171
|
+
});
|
|
172
|
+
test('handles different main file names', function () {
|
|
173
|
+
var getOutFile = function (mainFile) {
|
|
174
|
+
return path_1.default.basename(mainFile, '.ts') + '.mjs';
|
|
175
|
+
};
|
|
176
|
+
expect(getOutFile('src/index.ts')).toBe('index.mjs');
|
|
177
|
+
expect(getOutFile('src/server.ts')).toBe('server.mjs');
|
|
178
|
+
expect(getOutFile('app/main.ts')).toBe('main.mjs');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('dev.ts proxy requirements', function () {
|
|
182
|
+
test('requires api-key when proxy enabled', function () {
|
|
183
|
+
var noproxy = false;
|
|
184
|
+
var apiKey = '';
|
|
185
|
+
var proxyEnabled = !noproxy;
|
|
186
|
+
var apiKeyMissing = !apiKey;
|
|
187
|
+
expect(proxyEnabled && apiKeyMissing).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
test('skips api-key check when proxy disabled', function () {
|
|
190
|
+
var noproxy = true;
|
|
191
|
+
var apiKey = '';
|
|
192
|
+
var shouldCheckApiKey = !noproxy;
|
|
193
|
+
expect(shouldCheckApiKey).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe('dev.ts security', function () {
|
|
197
|
+
describe('path traversal protection', function () {
|
|
198
|
+
// Extract the validation logic for testing
|
|
199
|
+
function validateMainFilePath(mainFile, cwd) {
|
|
200
|
+
var resolvedMain = path_1.default.resolve(cwd, mainFile);
|
|
201
|
+
return resolvedMain.startsWith(cwd);
|
|
202
|
+
}
|
|
203
|
+
test('allows valid relative paths', function () {
|
|
204
|
+
var cwd = '/project';
|
|
205
|
+
expect(validateMainFilePath('src/index.ts', cwd)).toBe(true);
|
|
206
|
+
expect(validateMainFilePath('./src/index.ts', cwd)).toBe(true);
|
|
207
|
+
expect(validateMainFilePath('index.ts', cwd)).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
test('blocks path traversal attempts', function () {
|
|
210
|
+
var cwd = '/project';
|
|
211
|
+
expect(validateMainFilePath('../other/index.ts', cwd)).toBe(false);
|
|
212
|
+
expect(validateMainFilePath('../../etc/passwd', cwd)).toBe(false);
|
|
213
|
+
expect(validateMainFilePath('src/../../other/file.ts', cwd)).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
test('blocks absolute paths outside project', function () {
|
|
216
|
+
var cwd = '/project';
|
|
217
|
+
expect(validateMainFilePath('/etc/passwd', cwd)).toBe(false);
|
|
218
|
+
expect(validateMainFilePath('/other/project/index.ts', cwd)).toBe(false);
|
|
219
|
+
});
|
|
220
|
+
test('allows absolute paths inside project', function () {
|
|
221
|
+
var cwd = '/project';
|
|
222
|
+
expect(validateMainFilePath('/project/src/index.ts', cwd)).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
describe('port validation', function () {
|
|
226
|
+
function validatePort(port) {
|
|
227
|
+
var portNumber = parseInt(port, 10);
|
|
228
|
+
var valid = !isNaN(portNumber) && portNumber >= 1 && portNumber <= 65535;
|
|
229
|
+
return { valid: valid, port: valid ? portNumber : 2022 };
|
|
230
|
+
}
|
|
231
|
+
test('accepts valid ports', function () {
|
|
232
|
+
expect(validatePort('3000')).toEqual({ valid: true, port: 3000 });
|
|
233
|
+
expect(validatePort('8080')).toEqual({ valid: true, port: 8080 });
|
|
234
|
+
expect(validatePort('1')).toEqual({ valid: true, port: 1 });
|
|
235
|
+
expect(validatePort('65535')).toEqual({ valid: true, port: 65535 });
|
|
236
|
+
});
|
|
237
|
+
test('rejects invalid ports', function () {
|
|
238
|
+
expect(validatePort('0').valid).toBe(false);
|
|
239
|
+
expect(validatePort('-1').valid).toBe(false);
|
|
240
|
+
expect(validatePort('65536').valid).toBe(false);
|
|
241
|
+
expect(validatePort('abc').valid).toBe(false);
|
|
242
|
+
expect(validatePort('').valid).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
test('returns default port for invalid input', function () {
|
|
245
|
+
expect(validatePort('invalid').port).toBe(2022);
|
|
246
|
+
expect(validatePort('99999').port).toBe(2022);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe('process spawn vs exec', function () {
|
|
250
|
+
test('spawn uses array for arguments (no shell injection)', function () {
|
|
251
|
+
// This tests the concept - spawn with an array is safe
|
|
252
|
+
// The key point: with spawn + array args, shell metacharacters
|
|
253
|
+
// are treated as literal characters, not commands
|
|
254
|
+
// If someone tried to inject: "src/index.ts; rm -rf /"
|
|
255
|
+
// With exec(), this would execute the rm command
|
|
256
|
+
// With spawn(['src/index.ts; rm -rf /']), it's treated as a filename
|
|
257
|
+
var maliciousInput = 'src/index.ts; rm -rf /';
|
|
258
|
+
// The resolved path will contain the malicious string as-is
|
|
259
|
+
var resolved = path_1.default.resolve('/project', maliciousInput);
|
|
260
|
+
// The malicious part is embedded in the path, proving it's not parsed
|
|
261
|
+
expect(resolved).toMatch(/index\.ts;/);
|
|
262
|
+
// But crucially, the semicolon stays in the path - it's never executed
|
|
263
|
+
expect(resolved.includes(';')).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
describe('graceful shutdown', function () {
|
|
268
|
+
test('cleanup function sets isCleaningUp flag', function () {
|
|
269
|
+
// Simulate the cleanup behavior
|
|
270
|
+
var isCleaningUp = false;
|
|
271
|
+
var cleanupCalled = 0;
|
|
272
|
+
function cleanup() {
|
|
273
|
+
if (isCleaningUp)
|
|
274
|
+
return;
|
|
275
|
+
isCleaningUp = true;
|
|
276
|
+
cleanupCalled++;
|
|
277
|
+
}
|
|
278
|
+
cleanup();
|
|
279
|
+
expect(cleanupCalled).toBe(1);
|
|
280
|
+
// Second call should be no-op
|
|
281
|
+
cleanup();
|
|
282
|
+
expect(cleanupCalled).toBe(1);
|
|
283
|
+
});
|
|
284
|
+
test('cleanup handles null resources safely', function () {
|
|
285
|
+
var childProcess = null;
|
|
286
|
+
var watcher = null;
|
|
287
|
+
function cleanup() {
|
|
288
|
+
if (childProcess) {
|
|
289
|
+
childProcess.kill();
|
|
290
|
+
childProcess = null;
|
|
291
|
+
}
|
|
292
|
+
if (watcher) {
|
|
293
|
+
watcher.close();
|
|
294
|
+
watcher = null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Should not throw
|
|
298
|
+
expect(function () { return cleanup(); }).not.toThrow();
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
describe('error detection in stderr', function () {
|
|
302
|
+
function shouldMarkAsFailed(output) {
|
|
303
|
+
return output.includes('Error:') || output.includes('error:');
|
|
304
|
+
}
|
|
305
|
+
test('detects Error: pattern', function () {
|
|
306
|
+
expect(shouldMarkAsFailed('Error: Something went wrong')).toBe(true);
|
|
307
|
+
expect(shouldMarkAsFailed('TypeError: undefined is not a function')).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
test('detects error: pattern', function () {
|
|
310
|
+
expect(shouldMarkAsFailed('error: cannot find module')).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
test('ignores warnings', function () {
|
|
313
|
+
expect(shouldMarkAsFailed('Warning: deprecated API')).toBe(false);
|
|
314
|
+
expect(shouldMarkAsFailed('WARN: something')).toBe(false);
|
|
315
|
+
expect(shouldMarkAsFailed('DeprecationWarning: this is deprecated')).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
test('ignores normal output', function () {
|
|
318
|
+
expect(shouldMarkAsFailed('Server started on port 3000')).toBe(false);
|
|
319
|
+
expect(shouldMarkAsFailed('Compiling...')).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
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
|
+
var path_1 = __importDefault(require("path"));
|
|
7
|
+
/**
|
|
8
|
+
* Init command tests - project initialization, template handling, validation
|
|
9
|
+
*/
|
|
10
|
+
describe('init.ts', function () {
|
|
11
|
+
describe('Node.js version validation', function () {
|
|
12
|
+
function validateNodeVersion(version) {
|
|
13
|
+
var versionNum = Number(version.slice(1).split('.')[0]);
|
|
14
|
+
return { valid: versionNum >= 20, major: versionNum };
|
|
15
|
+
}
|
|
16
|
+
test('accepts Node.js 20+', function () {
|
|
17
|
+
expect(validateNodeVersion('v20.0.0').valid).toBe(true);
|
|
18
|
+
expect(validateNodeVersion('v20.7.0').valid).toBe(true);
|
|
19
|
+
expect(validateNodeVersion('v21.0.0').valid).toBe(true);
|
|
20
|
+
expect(validateNodeVersion('v22.3.0').valid).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
test('rejects Node.js < 20', function () {
|
|
23
|
+
expect(validateNodeVersion('v18.0.0').valid).toBe(false);
|
|
24
|
+
expect(validateNodeVersion('v19.9.0').valid).toBe(false);
|
|
25
|
+
expect(validateNodeVersion('v16.20.0').valid).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
test('parses version correctly', function () {
|
|
28
|
+
expect(validateNodeVersion('v20.7.0').major).toBe(20);
|
|
29
|
+
expect(validateNodeVersion('v18.15.0').major).toBe(18);
|
|
30
|
+
expect(validateNodeVersion('v22.0.0').major).toBe(22);
|
|
31
|
+
});
|
|
32
|
+
test('handles pre-release versions', function () {
|
|
33
|
+
// Pre-release versions have format like v21.0.0-nightly
|
|
34
|
+
expect(validateNodeVersion('v21.0.0-nightly').major).toBe(21);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('project directory handling', function () {
|
|
38
|
+
test('creates project path from cwd and name', function () {
|
|
39
|
+
var cwd = '/home/user/projects';
|
|
40
|
+
var projectName = 'my-service';
|
|
41
|
+
var projectDir = path_1.default.join(cwd, projectName);
|
|
42
|
+
expect(projectDir).toBe('/home/user/projects/my-service');
|
|
43
|
+
});
|
|
44
|
+
test('handles project names with special characters', function () {
|
|
45
|
+
var cwd = '/project';
|
|
46
|
+
expect(path_1.default.join(cwd, 'my-service')).toBe('/project/my-service');
|
|
47
|
+
expect(path_1.default.join(cwd, 'my_service')).toBe('/project/my_service');
|
|
48
|
+
expect(path_1.default.join(cwd, '@scope/pkg')).toBe('/project/@scope/pkg');
|
|
49
|
+
});
|
|
50
|
+
test('handles spaces in project name', function () {
|
|
51
|
+
var cwd = '/project';
|
|
52
|
+
var projectDir = path_1.default.join(cwd, 'my service');
|
|
53
|
+
expect(projectDir).toBe('/project/my service');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe('template directory resolution', function () {
|
|
57
|
+
test('resolves template path from __dirname', function () {
|
|
58
|
+
// The actual path is: __dirname/../templates/default
|
|
59
|
+
var dirname = '/path/to/dain-dev-cli/dist/commands';
|
|
60
|
+
var templateDir = path_1.default.join(dirname, '..', '..', 'templates', 'default');
|
|
61
|
+
expect(templateDir).toBe('/path/to/dain-dev-cli/templates/default');
|
|
62
|
+
});
|
|
63
|
+
test('uses "default" template', function () {
|
|
64
|
+
var templateName = 'default';
|
|
65
|
+
expect(templateName).toBe('default');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('package.json modification', function () {
|
|
69
|
+
test('updates package name', function () {
|
|
70
|
+
var packageJson = { name: 'template-name', version: '1.0.0' };
|
|
71
|
+
var projectName = 'my-new-project';
|
|
72
|
+
packageJson.name = projectName;
|
|
73
|
+
expect(packageJson.name).toBe('my-new-project');
|
|
74
|
+
});
|
|
75
|
+
test('preserves other package.json fields', function () {
|
|
76
|
+
var packageJson = {
|
|
77
|
+
name: 'template',
|
|
78
|
+
version: '1.0.0',
|
|
79
|
+
dependencies: { 'some-dep': '^1.0.0' },
|
|
80
|
+
scripts: { dev: 'node index.js' },
|
|
81
|
+
};
|
|
82
|
+
var projectName = 'updated-name';
|
|
83
|
+
packageJson.name = projectName;
|
|
84
|
+
expect(packageJson.name).toBe('updated-name');
|
|
85
|
+
expect(packageJson.version).toBe('1.0.0');
|
|
86
|
+
expect(packageJson.dependencies).toEqual({ 'some-dep': '^1.0.0' });
|
|
87
|
+
expect(packageJson.scripts).toEqual({ dev: 'node index.js' });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('project name validation', function () {
|
|
91
|
+
// Valid npm package name patterns
|
|
92
|
+
function isValidProjectName(name) {
|
|
93
|
+
// Simple validation: non-empty, no path separators at start
|
|
94
|
+
if (!name || name.startsWith('/') || name.startsWith('..')) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
test('accepts valid project names', function () {
|
|
100
|
+
expect(isValidProjectName('my-project')).toBe(true);
|
|
101
|
+
expect(isValidProjectName('my_project')).toBe(true);
|
|
102
|
+
expect(isValidProjectName('myproject123')).toBe(true);
|
|
103
|
+
expect(isValidProjectName('@scope/package')).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
test('rejects empty names', function () {
|
|
106
|
+
expect(isValidProjectName('')).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
test('rejects absolute paths', function () {
|
|
109
|
+
expect(isValidProjectName('/etc/passwd')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
test('rejects path traversal', function () {
|
|
112
|
+
expect(isValidProjectName('../other')).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
describe('success messages', function () {
|
|
116
|
+
test('includes project name in success message', function () {
|
|
117
|
+
var projectName = 'my-service';
|
|
118
|
+
var message = "Initialized Dain project: ".concat(projectName);
|
|
119
|
+
expect(message).toContain('my-service');
|
|
120
|
+
});
|
|
121
|
+
test('includes project directory in message', function () {
|
|
122
|
+
var projectDir = '/home/user/projects/my-service';
|
|
123
|
+
var message = "Project created at ".concat(projectDir);
|
|
124
|
+
expect(message).toContain('/home/user/projects/my-service');
|
|
125
|
+
});
|
|
126
|
+
test('includes getting started instructions', function () {
|
|
127
|
+
var projectName = 'my-project';
|
|
128
|
+
var commands = ["cd ".concat(projectName), 'npm install', 'npm run dev'];
|
|
129
|
+
expect(commands[0]).toBe('cd my-project');
|
|
130
|
+
expect(commands[1]).toBe('npm install');
|
|
131
|
+
expect(commands[2]).toBe('npm run dev');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('status.ts', function () {
|
|
136
|
+
describe('URL construction', function () {
|
|
137
|
+
test('builds correct status URL', function () {
|
|
138
|
+
var baseUrl = 'https://api.example.com';
|
|
139
|
+
var orgId = 'org123';
|
|
140
|
+
var serviceId = 'svc456';
|
|
141
|
+
var deploymentId = 'dep789';
|
|
142
|
+
var environment = 'production';
|
|
143
|
+
var statusUrl = "".concat(baseUrl, "/codegen-deploy/status/").concat(orgId, "/").concat(serviceId, "/").concat(deploymentId, "/").concat(environment);
|
|
144
|
+
expect(statusUrl).toBe('https://api.example.com/codegen-deploy/status/org123/svc456/dep789/production');
|
|
145
|
+
});
|
|
146
|
+
test('handles different environments', function () {
|
|
147
|
+
var buildUrl = function (env) {
|
|
148
|
+
return "https://api.example.com/codegen-deploy/status/org/svc/dep/".concat(env);
|
|
149
|
+
};
|
|
150
|
+
expect(buildUrl('production')).toContain('/production');
|
|
151
|
+
expect(buildUrl('staging')).toContain('/staging');
|
|
152
|
+
expect(buildUrl('development')).toContain('/development');
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
describe('predefined parameters override', function () {
|
|
156
|
+
test('uses predefined values over config', function () {
|
|
157
|
+
var config = {
|
|
158
|
+
'deployment-id': 'config-dep',
|
|
159
|
+
'service-id': 'config-svc',
|
|
160
|
+
environment: 'config-env',
|
|
161
|
+
};
|
|
162
|
+
var predefined = {
|
|
163
|
+
deploymentId: 'pre-dep',
|
|
164
|
+
serviceId: 'pre-svc',
|
|
165
|
+
environment: 'pre-env',
|
|
166
|
+
};
|
|
167
|
+
var deploymentId = predefined.deploymentId || config['deployment-id'];
|
|
168
|
+
var serviceId = predefined.serviceId || config['service-id'];
|
|
169
|
+
var environment = predefined.environment || config.environment;
|
|
170
|
+
expect(deploymentId).toBe('pre-dep');
|
|
171
|
+
expect(serviceId).toBe('pre-svc');
|
|
172
|
+
expect(environment).toBe('pre-env');
|
|
173
|
+
});
|
|
174
|
+
test('falls back to config when predefined is undefined', function () {
|
|
175
|
+
var config = {
|
|
176
|
+
'deployment-id': 'config-dep',
|
|
177
|
+
'service-id': 'config-svc',
|
|
178
|
+
};
|
|
179
|
+
var predefinedDeploymentId = undefined;
|
|
180
|
+
var predefinedServiceId = undefined;
|
|
181
|
+
var deploymentId = predefinedDeploymentId || config['deployment-id'];
|
|
182
|
+
var serviceId = predefinedServiceId || config['service-id'];
|
|
183
|
+
expect(deploymentId).toBe('config-dep');
|
|
184
|
+
expect(serviceId).toBe('config-svc');
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
describe('response handling', function () {
|
|
188
|
+
test('returns result when predefined params are provided', function () {
|
|
189
|
+
var preDefinedDeploymentId = 'dep123';
|
|
190
|
+
var shouldReturn = !!preDefinedDeploymentId;
|
|
191
|
+
expect(shouldReturn).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
test('exits when no predefined params', function () {
|
|
194
|
+
var preDefinedDeploymentId = undefined;
|
|
195
|
+
var shouldExit = !preDefinedDeploymentId;
|
|
196
|
+
expect(shouldExit).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
describe('undeploy.ts', function () {
|
|
201
|
+
describe('URL construction', function () {
|
|
202
|
+
test('builds correct undeploy URL', function () {
|
|
203
|
+
var baseUrl = 'https://api.example.com';
|
|
204
|
+
var orgId = 'org123';
|
|
205
|
+
var serviceId = 'svc456';
|
|
206
|
+
var deploymentId = 'dep789';
|
|
207
|
+
var environment = 'production';
|
|
208
|
+
var undeployUrl = "".concat(baseUrl, "/codegen-deploy/undeploy/").concat(orgId, "/").concat(serviceId, "/").concat(deploymentId, "/").concat(environment);
|
|
209
|
+
expect(undeployUrl).toBe('https://api.example.com/codegen-deploy/undeploy/org123/svc456/dep789/production');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
describe('HTTP method', function () {
|
|
213
|
+
test('uses DELETE method for undeploy', function () {
|
|
214
|
+
var method = 'DELETE';
|
|
215
|
+
expect(method).toBe('DELETE');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe('validation requirements', function () {
|
|
219
|
+
test('requires org ID', function () {
|
|
220
|
+
var orgId = '';
|
|
221
|
+
var hasError = !orgId;
|
|
222
|
+
expect(hasError).toBe(true);
|
|
223
|
+
});
|
|
224
|
+
test('requires deployment ID', function () {
|
|
225
|
+
var deploymentId = '';
|
|
226
|
+
var hasError = !deploymentId;
|
|
227
|
+
expect(hasError).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
test('requires service ID', function () {
|
|
230
|
+
var serviceId = '';
|
|
231
|
+
var hasError = !serviceId;
|
|
232
|
+
expect(hasError).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
test('passes validation with all required fields', function () {
|
|
235
|
+
var orgId = 'org123';
|
|
236
|
+
var deploymentId = 'dep456';
|
|
237
|
+
var serviceId = 'svc789';
|
|
238
|
+
var allValid = orgId && deploymentId && serviceId;
|
|
239
|
+
expect(allValid).toBeTruthy();
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
describe('shared patterns across commands', function () {
|
|
244
|
+
describe('fetchWithTimeout pattern', function () {
|
|
245
|
+
var FETCH_TIMEOUT_MS = 30000;
|
|
246
|
+
test('default timeout is 30 seconds', function () {
|
|
247
|
+
expect(FETCH_TIMEOUT_MS).toBe(30000);
|
|
248
|
+
});
|
|
249
|
+
test('uses AbortController for cancellation', function () {
|
|
250
|
+
var controller = new AbortController();
|
|
251
|
+
expect(controller.signal.aborted).toBe(false);
|
|
252
|
+
controller.abort();
|
|
253
|
+
expect(controller.signal.aborted).toBe(true);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
describe('Authorization header', function () {
|
|
257
|
+
test('formats Bearer token correctly', function () {
|
|
258
|
+
var apiKey = 'sk_agent_test_key';
|
|
259
|
+
var header = "Bearer ".concat(apiKey);
|
|
260
|
+
expect(header).toBe('Bearer sk_agent_test_key');
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
describe('error response handling', function () {
|
|
264
|
+
test('creates error message from response', function () {
|
|
265
|
+
var status = 404;
|
|
266
|
+
var statusText = 'Not Found';
|
|
267
|
+
var errorMessage = "failed: ".concat(status, " ").concat(statusText);
|
|
268
|
+
expect(errorMessage).toBe('failed: 404 Not Found');
|
|
269
|
+
});
|
|
270
|
+
test('handles various HTTP status codes', function () {
|
|
271
|
+
var createError = function (status, text) { return "".concat(status, " ").concat(text); };
|
|
272
|
+
expect(createError(400, 'Bad Request')).toBe('400 Bad Request');
|
|
273
|
+
expect(createError(401, 'Unauthorized')).toBe('401 Unauthorized');
|
|
274
|
+
expect(createError(403, 'Forbidden')).toBe('403 Forbidden');
|
|
275
|
+
expect(createError(404, 'Not Found')).toBe('404 Not Found');
|
|
276
|
+
expect(createError(500, 'Internal Server Error')).toBe('500 Internal Server Error');
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
describe('config resolution pattern', function () {
|
|
280
|
+
test('uses platform-base-url from config or default', function () {
|
|
281
|
+
var DEFAULT_PLATFORM_BASE_URL = 'https://default.example.com';
|
|
282
|
+
var config1 = { 'platform-base-url': 'https://custom.example.com' };
|
|
283
|
+
var config2 = {};
|
|
284
|
+
var url1 = config1['platform-base-url'] || DEFAULT_PLATFORM_BASE_URL;
|
|
285
|
+
var url2 = config2['platform-base-url'] || DEFAULT_PLATFORM_BASE_URL;
|
|
286
|
+
expect(url1).toBe('https://custom.example.com');
|
|
287
|
+
expect(url2).toBe('https://default.example.com');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|