@dainprotocol/cli 1.2.26 → 1.2.28
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__/deploy.test.js +143 -0
- package/dist/__tests__/dev.test.js +132 -0
- package/dist/__tests__/integration.test.js +151 -0
- package/dist/__tests__/logs.test.js +135 -0
- package/dist/commands/deploy.js +116 -90
- package/dist/commands/dev.js +143 -128
- package/dist/commands/logs.js +99 -28
- package/dist/commands/status.js +43 -10
- package/dist/commands/undeploy.js +48 -19
- package/dist/index.js +0 -0
- package/package.json +12 -5
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Mock fs-extra before importing
|
|
4
|
+
jest.mock('fs-extra', function () { return ({
|
|
5
|
+
readFile: jest.fn(),
|
|
6
|
+
readdir: jest.fn(),
|
|
7
|
+
existsSync: jest.fn(),
|
|
8
|
+
readFileSync: jest.fn(),
|
|
9
|
+
}); });
|
|
10
|
+
// Test the env parsing logic directly
|
|
11
|
+
describe('loadEnvVariables', function () {
|
|
12
|
+
// Extract the parsing logic for testing
|
|
13
|
+
function parseEnvContent(envContent) {
|
|
14
|
+
return envContent
|
|
15
|
+
.split('\n')
|
|
16
|
+
.filter(function (line) { return line.trim() && !line.trim().startsWith('#'); })
|
|
17
|
+
.map(function (line) {
|
|
18
|
+
var equalsIndex = line.indexOf('=');
|
|
19
|
+
if (equalsIndex === -1)
|
|
20
|
+
return null;
|
|
21
|
+
var name = line.substring(0, equalsIndex).trim();
|
|
22
|
+
var value = line.substring(equalsIndex + 1).trim();
|
|
23
|
+
// Remove surrounding quotes if present
|
|
24
|
+
var unquotedValue = value.replace(/^["']|["']$/g, '');
|
|
25
|
+
return { name: name, value: unquotedValue };
|
|
26
|
+
})
|
|
27
|
+
.filter(function (env) {
|
|
28
|
+
return env !== null && env.name !== '' && env.value !== '';
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
test('parses simple key=value pairs', function () {
|
|
32
|
+
var content = "FOO=bar\nBAZ=qux";
|
|
33
|
+
var result = parseEnvContent(content);
|
|
34
|
+
expect(result).toEqual([
|
|
35
|
+
{ name: 'FOO', value: 'bar' },
|
|
36
|
+
{ name: 'BAZ', value: 'qux' },
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
39
|
+
test('handles values with = signs (like base64)', function () {
|
|
40
|
+
var content = "API_KEY=sk_test_abc123==\nSECRET=a=b=c=d";
|
|
41
|
+
var result = parseEnvContent(content);
|
|
42
|
+
expect(result).toEqual([
|
|
43
|
+
{ name: 'API_KEY', value: 'sk_test_abc123==' },
|
|
44
|
+
{ name: 'SECRET', value: 'a=b=c=d' },
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
test('ignores comment lines', function () {
|
|
48
|
+
var content = "# This is a comment\nFOO=bar\n# Another comment\nBAZ=qux";
|
|
49
|
+
var result = parseEnvContent(content);
|
|
50
|
+
expect(result).toEqual([
|
|
51
|
+
{ name: 'FOO', value: 'bar' },
|
|
52
|
+
{ name: 'BAZ', value: 'qux' },
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
test('ignores empty lines', function () {
|
|
56
|
+
var content = "FOO=bar\n\nBAZ=qux\n\n";
|
|
57
|
+
var result = parseEnvContent(content);
|
|
58
|
+
expect(result).toEqual([
|
|
59
|
+
{ name: 'FOO', value: 'bar' },
|
|
60
|
+
{ name: 'BAZ', value: 'qux' },
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
test('removes surrounding quotes from values', function () {
|
|
64
|
+
var content = "FOO=\"bar\"\nBAZ='qux'\nSINGLE=\"quoted value\"";
|
|
65
|
+
var result = parseEnvContent(content);
|
|
66
|
+
expect(result).toEqual([
|
|
67
|
+
{ name: 'FOO', value: 'bar' },
|
|
68
|
+
{ name: 'BAZ', value: 'qux' },
|
|
69
|
+
{ name: 'SINGLE', value: 'quoted value' },
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
test('handles whitespace around = sign', function () {
|
|
73
|
+
var content = " FOO = bar\n BAZ=qux";
|
|
74
|
+
var result = parseEnvContent(content);
|
|
75
|
+
expect(result).toEqual([
|
|
76
|
+
{ name: 'FOO', value: 'bar' },
|
|
77
|
+
{ name: 'BAZ', value: 'qux' },
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
test('skips lines without = sign', function () {
|
|
81
|
+
var content = "FOO=bar\nINVALID LINE\nBAZ=qux";
|
|
82
|
+
var result = parseEnvContent(content);
|
|
83
|
+
expect(result).toEqual([
|
|
84
|
+
{ name: 'FOO', value: 'bar' },
|
|
85
|
+
{ name: 'BAZ', value: 'qux' },
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
test('skips lines with empty name or value', function () {
|
|
89
|
+
var content = "=bar\nFOO=\nVALID=value";
|
|
90
|
+
var result = parseEnvContent(content);
|
|
91
|
+
expect(result).toEqual([
|
|
92
|
+
{ name: 'VALID', value: 'value' },
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
test('handles complex production-like env file', function () {
|
|
96
|
+
var content = "# Production environment\nDATABASE_URL=postgresql://user:pass@host:5432/db?schema=public\nJWT_SECRET=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ\nREDIS_URL=\"redis://localhost:6379\"\nAPI_KEY='sk_live_abc123=='\n\n# Comment in the middle\nEMPTY_LINE_AFTER=true";
|
|
97
|
+
var result = parseEnvContent(content);
|
|
98
|
+
expect(result).toHaveLength(5);
|
|
99
|
+
expect(result[0].name).toBe('DATABASE_URL');
|
|
100
|
+
expect(result[0].value).toContain('postgresql://');
|
|
101
|
+
expect(result[1].name).toBe('JWT_SECRET');
|
|
102
|
+
expect(result[1].value).toContain('eyJ');
|
|
103
|
+
expect(result[2].value).toBe('redis://localhost:6379');
|
|
104
|
+
expect(result[3].value).toBe('sk_live_abc123==');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
describe('fetchWithTimeout', function () {
|
|
108
|
+
test('AbortController pattern is correct', function () {
|
|
109
|
+
// Test that our timeout pattern is implemented correctly
|
|
110
|
+
var controller = new AbortController();
|
|
111
|
+
// Signal starts as not aborted
|
|
112
|
+
expect(controller.signal.aborted).toBe(false);
|
|
113
|
+
// Abort changes the signal
|
|
114
|
+
controller.abort();
|
|
115
|
+
expect(controller.signal.aborted).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
test('setTimeout + AbortController integration', function () {
|
|
118
|
+
jest.useFakeTimers();
|
|
119
|
+
var controller = new AbortController();
|
|
120
|
+
var timeoutMs = 100;
|
|
121
|
+
// Set up timeout to abort
|
|
122
|
+
var timeoutId = setTimeout(function () { return controller.abort(); }, timeoutMs);
|
|
123
|
+
expect(controller.signal.aborted).toBe(false);
|
|
124
|
+
// Advance time past timeout
|
|
125
|
+
jest.advanceTimersByTime(150);
|
|
126
|
+
expect(controller.signal.aborted).toBe(true);
|
|
127
|
+
clearTimeout(timeoutId);
|
|
128
|
+
jest.useRealTimers();
|
|
129
|
+
});
|
|
130
|
+
test('clearTimeout prevents abort', function () {
|
|
131
|
+
jest.useFakeTimers();
|
|
132
|
+
var controller = new AbortController();
|
|
133
|
+
var timeoutMs = 100;
|
|
134
|
+
var timeoutId = setTimeout(function () { return controller.abort(); }, timeoutMs);
|
|
135
|
+
// Clear before timeout
|
|
136
|
+
clearTimeout(timeoutId);
|
|
137
|
+
// Advance time past what would have been the timeout
|
|
138
|
+
jest.advanceTimersByTime(200);
|
|
139
|
+
// Should NOT be aborted since we cleared the timeout
|
|
140
|
+
expect(controller.signal.aborted).toBe(false);
|
|
141
|
+
jest.useRealTimers();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
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
|
+
describe('dev.ts security', function () {
|
|
8
|
+
describe('path traversal protection', function () {
|
|
9
|
+
// Extract the validation logic for testing
|
|
10
|
+
function validateMainFilePath(mainFile, cwd) {
|
|
11
|
+
var resolvedMain = path_1.default.resolve(cwd, mainFile);
|
|
12
|
+
return resolvedMain.startsWith(cwd);
|
|
13
|
+
}
|
|
14
|
+
test('allows valid relative paths', function () {
|
|
15
|
+
var cwd = '/project';
|
|
16
|
+
expect(validateMainFilePath('src/index.ts', cwd)).toBe(true);
|
|
17
|
+
expect(validateMainFilePath('./src/index.ts', cwd)).toBe(true);
|
|
18
|
+
expect(validateMainFilePath('index.ts', cwd)).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
test('blocks path traversal attempts', function () {
|
|
21
|
+
var cwd = '/project';
|
|
22
|
+
expect(validateMainFilePath('../other/index.ts', cwd)).toBe(false);
|
|
23
|
+
expect(validateMainFilePath('../../etc/passwd', cwd)).toBe(false);
|
|
24
|
+
expect(validateMainFilePath('src/../../other/file.ts', cwd)).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
test('blocks absolute paths outside project', function () {
|
|
27
|
+
var cwd = '/project';
|
|
28
|
+
expect(validateMainFilePath('/etc/passwd', cwd)).toBe(false);
|
|
29
|
+
expect(validateMainFilePath('/other/project/index.ts', cwd)).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
test('allows absolute paths inside project', function () {
|
|
32
|
+
var cwd = '/project';
|
|
33
|
+
expect(validateMainFilePath('/project/src/index.ts', cwd)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('port validation', function () {
|
|
37
|
+
function validatePort(port) {
|
|
38
|
+
var portNumber = parseInt(port, 10);
|
|
39
|
+
var valid = !isNaN(portNumber) && portNumber >= 1 && portNumber <= 65535;
|
|
40
|
+
return { valid: valid, port: valid ? portNumber : 2022 };
|
|
41
|
+
}
|
|
42
|
+
test('accepts valid ports', function () {
|
|
43
|
+
expect(validatePort('3000')).toEqual({ valid: true, port: 3000 });
|
|
44
|
+
expect(validatePort('8080')).toEqual({ valid: true, port: 8080 });
|
|
45
|
+
expect(validatePort('1')).toEqual({ valid: true, port: 1 });
|
|
46
|
+
expect(validatePort('65535')).toEqual({ valid: true, port: 65535 });
|
|
47
|
+
});
|
|
48
|
+
test('rejects invalid ports', function () {
|
|
49
|
+
expect(validatePort('0').valid).toBe(false);
|
|
50
|
+
expect(validatePort('-1').valid).toBe(false);
|
|
51
|
+
expect(validatePort('65536').valid).toBe(false);
|
|
52
|
+
expect(validatePort('abc').valid).toBe(false);
|
|
53
|
+
expect(validatePort('').valid).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
test('returns default port for invalid input', function () {
|
|
56
|
+
expect(validatePort('invalid').port).toBe(2022);
|
|
57
|
+
expect(validatePort('99999').port).toBe(2022);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe('process spawn vs exec', function () {
|
|
61
|
+
test('spawn uses array for arguments (no shell injection)', function () {
|
|
62
|
+
// This tests the concept - spawn with an array is safe
|
|
63
|
+
// The key point: with spawn + array args, shell metacharacters
|
|
64
|
+
// are treated as literal characters, not commands
|
|
65
|
+
// If someone tried to inject: "src/index.ts; rm -rf /"
|
|
66
|
+
// With exec(), this would execute the rm command
|
|
67
|
+
// With spawn(['src/index.ts; rm -rf /']), it's treated as a filename
|
|
68
|
+
var maliciousInput = 'src/index.ts; rm -rf /';
|
|
69
|
+
// The resolved path will contain the malicious string as-is
|
|
70
|
+
var resolved = path_1.default.resolve('/project', maliciousInput);
|
|
71
|
+
// The malicious part is embedded in the path, proving it's not parsed
|
|
72
|
+
expect(resolved).toMatch(/index\.ts;/);
|
|
73
|
+
// But crucially, the semicolon stays in the path - it's never executed
|
|
74
|
+
expect(resolved.includes(';')).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('graceful shutdown', function () {
|
|
79
|
+
test('cleanup function sets isCleaningUp flag', function () {
|
|
80
|
+
// Simulate the cleanup behavior
|
|
81
|
+
var isCleaningUp = false;
|
|
82
|
+
var cleanupCalled = 0;
|
|
83
|
+
function cleanup() {
|
|
84
|
+
if (isCleaningUp)
|
|
85
|
+
return;
|
|
86
|
+
isCleaningUp = true;
|
|
87
|
+
cleanupCalled++;
|
|
88
|
+
}
|
|
89
|
+
cleanup();
|
|
90
|
+
expect(cleanupCalled).toBe(1);
|
|
91
|
+
// Second call should be no-op
|
|
92
|
+
cleanup();
|
|
93
|
+
expect(cleanupCalled).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
test('cleanup handles null resources safely', function () {
|
|
96
|
+
var childProcess = null;
|
|
97
|
+
var watcher = null;
|
|
98
|
+
function cleanup() {
|
|
99
|
+
if (childProcess) {
|
|
100
|
+
childProcess.kill();
|
|
101
|
+
childProcess = null;
|
|
102
|
+
}
|
|
103
|
+
if (watcher) {
|
|
104
|
+
watcher.close();
|
|
105
|
+
watcher = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Should not throw
|
|
109
|
+
expect(function () { return cleanup(); }).not.toThrow();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('error detection in stderr', function () {
|
|
113
|
+
function shouldMarkAsFailed(output) {
|
|
114
|
+
return output.includes('Error:') || output.includes('error:');
|
|
115
|
+
}
|
|
116
|
+
test('detects Error: pattern', function () {
|
|
117
|
+
expect(shouldMarkAsFailed('Error: Something went wrong')).toBe(true);
|
|
118
|
+
expect(shouldMarkAsFailed('TypeError: undefined is not a function')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
test('detects error: pattern', function () {
|
|
121
|
+
expect(shouldMarkAsFailed('error: cannot find module')).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
test('ignores warnings', function () {
|
|
124
|
+
expect(shouldMarkAsFailed('Warning: deprecated API')).toBe(false);
|
|
125
|
+
expect(shouldMarkAsFailed('WARN: something')).toBe(false);
|
|
126
|
+
expect(shouldMarkAsFailed('DeprecationWarning: this is deprecated')).toBe(false);
|
|
127
|
+
});
|
|
128
|
+
test('ignores normal output', function () {
|
|
129
|
+
expect(shouldMarkAsFailed('Server started on port 3000')).toBe(false);
|
|
130
|
+
expect(shouldMarkAsFailed('Compiling...')).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Integration tests for bug detection
|
|
4
|
+
* Focus on actual bugs that the refactoring could have introduced
|
|
5
|
+
*/
|
|
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 path_1 = __importDefault(require("path"));
|
|
11
|
+
describe('Bug Detection Tests', function () {
|
|
12
|
+
describe('ENV parsing - CRITICAL: = in values', function () {
|
|
13
|
+
// This is the ACTUAL parsing logic from deploy.ts
|
|
14
|
+
function parseEnvContent(envContent) {
|
|
15
|
+
return envContent
|
|
16
|
+
.split('\n')
|
|
17
|
+
.filter(function (line) { return line.trim() && !line.trim().startsWith('#'); })
|
|
18
|
+
.map(function (line) {
|
|
19
|
+
var equalsIndex = line.indexOf('=');
|
|
20
|
+
if (equalsIndex === -1)
|
|
21
|
+
return null;
|
|
22
|
+
var name = line.substring(0, equalsIndex).trim();
|
|
23
|
+
var value = line.substring(equalsIndex + 1).trim();
|
|
24
|
+
var unquotedValue = value.replace(/^["']|["']$/g, '');
|
|
25
|
+
return { name: name, value: unquotedValue };
|
|
26
|
+
})
|
|
27
|
+
.filter(function (env) {
|
|
28
|
+
return env !== null && env.name !== '' && env.value !== '';
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
test('REGRESSION: base64 values with == suffix must be preserved', function () {
|
|
32
|
+
// OLD BUG: line.split('=') would break this into ['JWT', 'eyJ...', '', '']
|
|
33
|
+
var content = 'JWT_SECRET=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test==';
|
|
34
|
+
var result = parseEnvContent(content);
|
|
35
|
+
expect(result).toHaveLength(1);
|
|
36
|
+
expect(result[0].name).toBe('JWT_SECRET');
|
|
37
|
+
expect(result[0].value).toBe('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test==');
|
|
38
|
+
});
|
|
39
|
+
test('REGRESSION: database URLs with query params', function () {
|
|
40
|
+
// OLD BUG: would only get 'postgresql://user:pass@host:5432/db?sslmode'
|
|
41
|
+
var content = 'DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require';
|
|
42
|
+
var result = parseEnvContent(content);
|
|
43
|
+
expect(result[0].value).toBe('postgresql://user:pass@host:5432/db?sslmode=require');
|
|
44
|
+
});
|
|
45
|
+
test('REGRESSION: multiple = signs in value', function () {
|
|
46
|
+
var content = 'FORMULA=a=b=c=d';
|
|
47
|
+
var result = parseEnvContent(content);
|
|
48
|
+
expect(result[0].value).toBe('a=b=c=d');
|
|
49
|
+
});
|
|
50
|
+
test('handles Windows CRLF line endings', function () {
|
|
51
|
+
var content = 'FOO=bar\r\nBAZ=qux\r\n';
|
|
52
|
+
var result = parseEnvContent(content);
|
|
53
|
+
expect(result).toHaveLength(2);
|
|
54
|
+
// Values should not have trailing \r
|
|
55
|
+
expect(result[0].value.endsWith('\r')).toBe(false);
|
|
56
|
+
expect(result[1].value.endsWith('\r')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('Path traversal protection', function () {
|
|
60
|
+
function validateMainFilePath(mainFile, cwd) {
|
|
61
|
+
var resolvedMain = path_1.default.resolve(cwd, mainFile);
|
|
62
|
+
return resolvedMain.startsWith(cwd);
|
|
63
|
+
}
|
|
64
|
+
test('SECURITY: blocks ../../../etc/passwd', function () {
|
|
65
|
+
expect(validateMainFilePath('../../../etc/passwd', '/project')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
test('SECURITY: blocks src/../../../etc/passwd', function () {
|
|
68
|
+
expect(validateMainFilePath('src/../../../etc/passwd', '/project')).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
test('SECURITY: blocks absolute paths outside project', function () {
|
|
71
|
+
expect(validateMainFilePath('/etc/passwd', '/project')).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
test('allows normal relative paths', function () {
|
|
74
|
+
expect(validateMainFilePath('src/index.ts', '/project')).toBe(true);
|
|
75
|
+
expect(validateMainFilePath('./src/index.ts', '/project')).toBe(true);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('Port validation', function () {
|
|
79
|
+
function validatePort(port) {
|
|
80
|
+
var portNumber = parseInt(port, 10);
|
|
81
|
+
var valid = !isNaN(portNumber) && portNumber >= 1 && portNumber <= 65535;
|
|
82
|
+
return { valid: valid, port: valid ? portNumber : 2022 };
|
|
83
|
+
}
|
|
84
|
+
test('rejects port 0', function () {
|
|
85
|
+
expect(validatePort('0').valid).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
test('rejects port > 65535', function () {
|
|
88
|
+
expect(validatePort('65536').valid).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
test('rejects non-numeric', function () {
|
|
91
|
+
expect(validatePort('abc').valid).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
test('accepts valid ports', function () {
|
|
94
|
+
expect(validatePort('3000').valid).toBe(true);
|
|
95
|
+
expect(validatePort('8080').valid).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('Graceful shutdown - cleanup idempotency', function () {
|
|
99
|
+
test('cleanup can be called multiple times safely', function () {
|
|
100
|
+
var isCleaningUp = false;
|
|
101
|
+
var cleanupCount = 0;
|
|
102
|
+
function cleanup() {
|
|
103
|
+
if (isCleaningUp)
|
|
104
|
+
return;
|
|
105
|
+
isCleaningUp = true;
|
|
106
|
+
cleanupCount++;
|
|
107
|
+
}
|
|
108
|
+
cleanup();
|
|
109
|
+
cleanup();
|
|
110
|
+
cleanup();
|
|
111
|
+
expect(cleanupCount).toBe(1);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
describe('Interval-based polling vs recursive setTimeout', function () {
|
|
115
|
+
test('setInterval can be properly cleared', function () {
|
|
116
|
+
jest.useFakeTimers();
|
|
117
|
+
var callCount = 0;
|
|
118
|
+
var intervalId = null;
|
|
119
|
+
intervalId = setInterval(function () {
|
|
120
|
+
callCount++;
|
|
121
|
+
}, 1000);
|
|
122
|
+
jest.advanceTimersByTime(3000);
|
|
123
|
+
expect(callCount).toBe(3);
|
|
124
|
+
clearInterval(intervalId);
|
|
125
|
+
intervalId = null;
|
|
126
|
+
jest.advanceTimersByTime(3000);
|
|
127
|
+
expect(callCount).toBe(3); // Should NOT increase
|
|
128
|
+
jest.useRealTimers();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
describe('AbortController timeout pattern', function () {
|
|
132
|
+
test('controller aborts after timeout', function () {
|
|
133
|
+
jest.useFakeTimers();
|
|
134
|
+
var controller = new AbortController();
|
|
135
|
+
setTimeout(function () { return controller.abort(); }, 100);
|
|
136
|
+
expect(controller.signal.aborted).toBe(false);
|
|
137
|
+
jest.advanceTimersByTime(150);
|
|
138
|
+
expect(controller.signal.aborted).toBe(true);
|
|
139
|
+
jest.useRealTimers();
|
|
140
|
+
});
|
|
141
|
+
test('clearTimeout prevents abort', function () {
|
|
142
|
+
jest.useFakeTimers();
|
|
143
|
+
var controller = new AbortController();
|
|
144
|
+
var timeoutId = setTimeout(function () { return controller.abort(); }, 100);
|
|
145
|
+
clearTimeout(timeoutId);
|
|
146
|
+
jest.advanceTimersByTime(200);
|
|
147
|
+
expect(controller.signal.aborted).toBe(false);
|
|
148
|
+
jest.useRealTimers();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
});
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
|
+
var t = {};
|
|
4
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
+
t[p] = s[p];
|
|
6
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
+
t[p[i]] = s[p[i]];
|
|
10
|
+
}
|
|
11
|
+
return t;
|
|
12
|
+
};
|
|
13
|
+
describe('logs.ts', function () {
|
|
14
|
+
describe('formatProjectLogs', function () {
|
|
15
|
+
function formatProjectLogs(projectLogs, deploymentId) {
|
|
16
|
+
try {
|
|
17
|
+
var _a = projectLogs.logs, logs = _a === void 0 ? '' : _a, metadata = __rest(projectLogs, ["logs"]);
|
|
18
|
+
var formatDate_1 = function (dateString) {
|
|
19
|
+
try {
|
|
20
|
+
return new Date(dateString).toLocaleString();
|
|
21
|
+
}
|
|
22
|
+
catch (_a) {
|
|
23
|
+
return dateString;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var formattedMetadata = Object.entries(metadata)
|
|
27
|
+
.map(function (_a) {
|
|
28
|
+
var _b;
|
|
29
|
+
var key = _a[0], value = _a[1];
|
|
30
|
+
var formattedValue = ((_b = value === null || value === void 0 ? void 0 : value.includes) === null || _b === void 0 ? void 0 : _b.call(value, 'T'))
|
|
31
|
+
? formatDate_1(value)
|
|
32
|
+
: value;
|
|
33
|
+
var formattedKey = key.replace(/([A-Z])/g, ' $1').toLowerCase();
|
|
34
|
+
return "\u001B[36m".concat(formattedKey, ":\u001B[0m ").concat(formattedValue);
|
|
35
|
+
})
|
|
36
|
+
.join('\n');
|
|
37
|
+
var projectUrl = "\nurl: https://".concat(deploymentId.replace('codegen-', ''), "-agent.dainapp.com");
|
|
38
|
+
var output = [
|
|
39
|
+
'\n',
|
|
40
|
+
'\x1b[1m=== Project ===\x1b[0m',
|
|
41
|
+
formattedMetadata,
|
|
42
|
+
projectUrl,
|
|
43
|
+
'\n\x1b[1m=== Logs ===\x1b[0m',
|
|
44
|
+
logs,
|
|
45
|
+
];
|
|
46
|
+
return output.join('\n');
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return "Error formatting logs: ".concat(error.message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
test('formats basic log output', function () {
|
|
53
|
+
var logs = { logs: 'Server started', status: 'running' };
|
|
54
|
+
var result = formatProjectLogs(logs, 'codegen-test123');
|
|
55
|
+
expect(result).toContain('=== Project ===');
|
|
56
|
+
expect(result).toContain('=== Logs ===');
|
|
57
|
+
expect(result).toContain('Server started');
|
|
58
|
+
expect(result).toContain('test123-agent.dainapp.com');
|
|
59
|
+
});
|
|
60
|
+
test('formats date fields correctly', function () {
|
|
61
|
+
var logs = {
|
|
62
|
+
logs: 'test',
|
|
63
|
+
createdAt: '2024-01-15T10:30:00.000Z',
|
|
64
|
+
};
|
|
65
|
+
var result = formatProjectLogs(logs, 'codegen-test');
|
|
66
|
+
expect(result).toContain('created at:');
|
|
67
|
+
// Date formatting is locale-dependent, just check it's present
|
|
68
|
+
expect(result).toMatch(/\d/);
|
|
69
|
+
});
|
|
70
|
+
test('handles empty logs', function () {
|
|
71
|
+
var logs = { logs: '' };
|
|
72
|
+
var result = formatProjectLogs(logs, 'codegen-test');
|
|
73
|
+
expect(result).toContain('=== Logs ===');
|
|
74
|
+
});
|
|
75
|
+
test('handles camelCase key formatting', function () {
|
|
76
|
+
var logs = {
|
|
77
|
+
logs: '',
|
|
78
|
+
deploymentId: '123',
|
|
79
|
+
serviceStatus: 'running',
|
|
80
|
+
};
|
|
81
|
+
var result = formatProjectLogs(logs, 'codegen-test');
|
|
82
|
+
expect(result).toContain('deployment id:');
|
|
83
|
+
expect(result).toContain('service status:');
|
|
84
|
+
});
|
|
85
|
+
test('handles errors gracefully', function () {
|
|
86
|
+
var result = formatProjectLogs(null, 'test');
|
|
87
|
+
expect(result).toContain('Error formatting logs:');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('interval-based polling', function () {
|
|
91
|
+
jest.useFakeTimers();
|
|
92
|
+
test('setInterval is used instead of recursive setTimeout', function () {
|
|
93
|
+
var callCount = 0;
|
|
94
|
+
var intervalId = null;
|
|
95
|
+
function startPolling() {
|
|
96
|
+
intervalId = setInterval(function () {
|
|
97
|
+
callCount++;
|
|
98
|
+
}, 1000);
|
|
99
|
+
}
|
|
100
|
+
function stopPolling() {
|
|
101
|
+
if (intervalId) {
|
|
102
|
+
clearInterval(intervalId);
|
|
103
|
+
intervalId = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
startPolling();
|
|
107
|
+
jest.advanceTimersByTime(5000);
|
|
108
|
+
expect(callCount).toBe(5);
|
|
109
|
+
stopPolling();
|
|
110
|
+
jest.advanceTimersByTime(5000);
|
|
111
|
+
expect(callCount).toBe(5); // Should not increase after stopping
|
|
112
|
+
});
|
|
113
|
+
afterEach(function () {
|
|
114
|
+
jest.useRealTimers();
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
describe('cleanup on shutdown', function () {
|
|
118
|
+
test('cleanup clears interval and sets flag', function () {
|
|
119
|
+
var watchInterval = null;
|
|
120
|
+
var isShuttingDown = false;
|
|
121
|
+
function cleanup() {
|
|
122
|
+
isShuttingDown = true;
|
|
123
|
+
if (watchInterval) {
|
|
124
|
+
clearInterval(watchInterval);
|
|
125
|
+
watchInterval = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
watchInterval = setInterval(function () { }, 1000);
|
|
129
|
+
expect(watchInterval).not.toBeNull();
|
|
130
|
+
cleanup();
|
|
131
|
+
expect(isShuttingDown).toBe(true);
|
|
132
|
+
expect(watchInterval).toBeNull();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|