@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.
@@ -0,0 +1,134 @@
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
+ var utils_1 = require("../utils");
12
+ describe('Bug Detection Tests', function () {
13
+ describe('ENV parsing - CRITICAL: = in values', function () {
14
+ test('REGRESSION: base64 values with == suffix must be preserved', function () {
15
+ // OLD BUG: line.split('=') would break this into ['JWT', 'eyJ...', '', '']
16
+ var content = 'JWT_SECRET=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test==';
17
+ var result = (0, utils_1.parseEnvContent)(content);
18
+ expect(result).toHaveLength(1);
19
+ expect(result[0].name).toBe('JWT_SECRET');
20
+ expect(result[0].value).toBe('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.test==');
21
+ });
22
+ test('REGRESSION: database URLs with query params', function () {
23
+ // OLD BUG: would only get 'postgresql://user:pass@host:5432/db?sslmode'
24
+ var content = 'DATABASE_URL=postgresql://user:pass@host:5432/db?sslmode=require';
25
+ var result = (0, utils_1.parseEnvContent)(content);
26
+ expect(result[0].value).toBe('postgresql://user:pass@host:5432/db?sslmode=require');
27
+ });
28
+ test('REGRESSION: multiple = signs in value', function () {
29
+ var content = 'FORMULA=a=b=c=d';
30
+ var result = (0, utils_1.parseEnvContent)(content);
31
+ expect(result[0].value).toBe('a=b=c=d');
32
+ });
33
+ test('handles Windows CRLF line endings', function () {
34
+ var content = 'FOO=bar\r\nBAZ=qux\r\n';
35
+ var result = (0, utils_1.parseEnvContent)(content);
36
+ expect(result).toHaveLength(2);
37
+ // Values should not have trailing \r
38
+ expect(result[0].value.endsWith('\r')).toBe(false);
39
+ expect(result[1].value.endsWith('\r')).toBe(false);
40
+ });
41
+ });
42
+ describe('Path traversal protection', function () {
43
+ function validateMainFilePath(mainFile, cwd) {
44
+ var resolvedMain = path_1.default.resolve(cwd, mainFile);
45
+ return resolvedMain.startsWith(cwd);
46
+ }
47
+ test('SECURITY: blocks ../../../etc/passwd', function () {
48
+ expect(validateMainFilePath('../../../etc/passwd', '/project')).toBe(false);
49
+ });
50
+ test('SECURITY: blocks src/../../../etc/passwd', function () {
51
+ expect(validateMainFilePath('src/../../../etc/passwd', '/project')).toBe(false);
52
+ });
53
+ test('SECURITY: blocks absolute paths outside project', function () {
54
+ expect(validateMainFilePath('/etc/passwd', '/project')).toBe(false);
55
+ });
56
+ test('allows normal relative paths', function () {
57
+ expect(validateMainFilePath('src/index.ts', '/project')).toBe(true);
58
+ expect(validateMainFilePath('./src/index.ts', '/project')).toBe(true);
59
+ });
60
+ });
61
+ describe('Port validation', function () {
62
+ function validatePort(port) {
63
+ var portNumber = parseInt(port, 10);
64
+ var valid = !isNaN(portNumber) && portNumber >= 1 && portNumber <= 65535;
65
+ return { valid: valid, port: valid ? portNumber : 2022 };
66
+ }
67
+ test('rejects port 0', function () {
68
+ expect(validatePort('0').valid).toBe(false);
69
+ });
70
+ test('rejects port > 65535', function () {
71
+ expect(validatePort('65536').valid).toBe(false);
72
+ });
73
+ test('rejects non-numeric', function () {
74
+ expect(validatePort('abc').valid).toBe(false);
75
+ });
76
+ test('accepts valid ports', function () {
77
+ expect(validatePort('3000').valid).toBe(true);
78
+ expect(validatePort('8080').valid).toBe(true);
79
+ });
80
+ });
81
+ describe('Graceful shutdown - cleanup idempotency', function () {
82
+ test('cleanup can be called multiple times safely', function () {
83
+ var isCleaningUp = false;
84
+ var cleanupCount = 0;
85
+ function cleanup() {
86
+ if (isCleaningUp)
87
+ return;
88
+ isCleaningUp = true;
89
+ cleanupCount++;
90
+ }
91
+ cleanup();
92
+ cleanup();
93
+ cleanup();
94
+ expect(cleanupCount).toBe(1);
95
+ });
96
+ });
97
+ describe('Interval-based polling vs recursive setTimeout', function () {
98
+ test('setInterval can be properly cleared', function () {
99
+ jest.useFakeTimers();
100
+ var callCount = 0;
101
+ var intervalId = null;
102
+ intervalId = setInterval(function () {
103
+ callCount++;
104
+ }, 1000);
105
+ jest.advanceTimersByTime(3000);
106
+ expect(callCount).toBe(3);
107
+ clearInterval(intervalId);
108
+ intervalId = null;
109
+ jest.advanceTimersByTime(3000);
110
+ expect(callCount).toBe(3); // Should NOT increase
111
+ jest.useRealTimers();
112
+ });
113
+ });
114
+ describe('AbortController timeout pattern', function () {
115
+ test('controller aborts after timeout', function () {
116
+ jest.useFakeTimers();
117
+ var controller = new AbortController();
118
+ setTimeout(function () { return controller.abort(); }, 100);
119
+ expect(controller.signal.aborted).toBe(false);
120
+ jest.advanceTimersByTime(150);
121
+ expect(controller.signal.aborted).toBe(true);
122
+ jest.useRealTimers();
123
+ });
124
+ test('clearTimeout prevents abort', function () {
125
+ jest.useFakeTimers();
126
+ var controller = new AbortController();
127
+ var timeoutId = setTimeout(function () { return controller.abort(); }, 100);
128
+ clearTimeout(timeoutId);
129
+ jest.advanceTimersByTime(200);
130
+ expect(controller.signal.aborted).toBe(false);
131
+ jest.useRealTimers();
132
+ });
133
+ });
134
+ });
@@ -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
+ });
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ /**
3
+ * Testchat command tests - URL resolution, API key validation, message handling
4
+ */
5
+ var __assign = (this && this.__assign) || function () {
6
+ __assign = Object.assign || function(t) {
7
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
8
+ s = arguments[i];
9
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
10
+ t[p] = s[p];
11
+ }
12
+ return t;
13
+ };
14
+ return __assign.apply(this, arguments);
15
+ };
16
+ describe('testchat.ts', function () {
17
+ describe('URL resolution', function () {
18
+ function resolveUrl(optionUrl, envUrl) {
19
+ if (optionUrl)
20
+ return { url: optionUrl, source: '--url option' };
21
+ if (envUrl)
22
+ return { url: envUrl, source: 'TEST_CHAT_URL env' };
23
+ return { url: undefined, source: null };
24
+ }
25
+ test('prioritizes --url option over env', function () {
26
+ var result = resolveUrl('http://cli-url.com', 'http://env-url.com');
27
+ expect(result.url).toBe('http://cli-url.com');
28
+ expect(result.source).toBe('--url option');
29
+ });
30
+ test('falls back to env when no option provided', function () {
31
+ var result = resolveUrl(undefined, 'http://env-url.com');
32
+ expect(result.url).toBe('http://env-url.com');
33
+ expect(result.source).toBe('TEST_CHAT_URL env');
34
+ });
35
+ test('returns undefined when neither provided', function () {
36
+ var result = resolveUrl(undefined, undefined);
37
+ expect(result.url).toBeUndefined();
38
+ expect(result.source).toBeNull();
39
+ });
40
+ });
41
+ describe('API key validation', function () {
42
+ test('requires ANTHROPIC_API_KEY', function () {
43
+ var apiKey = '';
44
+ var hasAnthropicKey = !!apiKey;
45
+ expect(hasAnthropicKey).toBe(false);
46
+ });
47
+ test('requires config api-key', function () {
48
+ var config = { 'api-key': '' };
49
+ var hasApiKey = !!config['api-key'];
50
+ expect(hasApiKey).toBe(false);
51
+ });
52
+ test('passes validation with all keys present', function () {
53
+ var anthropicKey = 'sk-ant-test';
54
+ var configApiKey = 'sk_agent_test';
55
+ var hasAllKeys = !!anthropicKey && !!configApiKey;
56
+ expect(hasAllKeys).toBe(true);
57
+ });
58
+ });
59
+ describe('exit command handling', function () {
60
+ test('recognizes "exit" command (lowercase)', function () {
61
+ var input = 'exit';
62
+ var shouldExit = input.toLowerCase() === 'exit';
63
+ expect(shouldExit).toBe(true);
64
+ });
65
+ test('recognizes "EXIT" command (uppercase)', function () {
66
+ var input = 'EXIT';
67
+ var shouldExit = input.toLowerCase() === 'exit';
68
+ expect(shouldExit).toBe(true);
69
+ });
70
+ test('recognizes "Exit" command (mixed case)', function () {
71
+ var input = 'Exit';
72
+ var shouldExit = input.toLowerCase() === 'exit';
73
+ expect(shouldExit).toBe(true);
74
+ });
75
+ test('does not exit on normal input', function () {
76
+ var input = 'hello';
77
+ var shouldExit = input.toLowerCase() === 'exit';
78
+ expect(shouldExit).toBe(false);
79
+ });
80
+ test('does not exit on input containing "exit"', function () {
81
+ var input = 'how do I exit vim';
82
+ var shouldExit = input.toLowerCase() === 'exit';
83
+ expect(shouldExit).toBe(false);
84
+ });
85
+ });
86
+ describe('message handling', function () {
87
+ test('adds user message to array', function () {
88
+ var messages = [];
89
+ var userInput = 'Hello world';
90
+ messages.push({ role: 'user', content: userInput });
91
+ expect(messages).toHaveLength(1);
92
+ expect(messages[0].role).toBe('user');
93
+ expect(messages[0].content).toBe('Hello world');
94
+ });
95
+ test('maintains message history', function () {
96
+ var messages = [];
97
+ messages.push({ role: 'user', content: 'First message' });
98
+ messages.push({ role: 'assistant', content: 'First response' });
99
+ messages.push({ role: 'user', content: 'Second message' });
100
+ expect(messages).toHaveLength(3);
101
+ expect(messages[0].role).toBe('user');
102
+ expect(messages[1].role).toBe('assistant');
103
+ expect(messages[2].role).toBe('user');
104
+ });
105
+ });
106
+ describe('tool response tracking', function () {
107
+ test('sets toolResponseAvailable when tool calls present', function () {
108
+ var toolCalls = [{ toolName: 'test', args: {} }];
109
+ var toolResponseAvailable = toolCalls.length > 0;
110
+ expect(toolResponseAvailable).toBe(true);
111
+ });
112
+ test('clears toolResponseAvailable when no tool calls', function () {
113
+ var toolCalls = [];
114
+ var toolResponseAvailable = toolCalls.length > 0;
115
+ expect(toolResponseAvailable).toBe(false);
116
+ });
117
+ test('skips user input when tool response available', function () {
118
+ var toolResponseAvailable = true;
119
+ var askedForInput = false;
120
+ // Simulates the chat loop logic
121
+ if (!toolResponseAvailable) {
122
+ askedForInput = true;
123
+ }
124
+ expect(askedForInput).toBe(false);
125
+ });
126
+ });
127
+ describe('toolbox loading', function () {
128
+ test('filters toolboxes without id', function () {
129
+ var toolboxes = [
130
+ { id: 'box1', tools: {} },
131
+ { id: null, tools: {} },
132
+ { id: 'box2', tools: {} },
133
+ { id: undefined, tools: {} },
134
+ ];
135
+ var validToolboxes = toolboxes.filter(function (tb) { return !!tb.id; });
136
+ expect(validToolboxes).toHaveLength(2);
137
+ expect(validToolboxes[0].id).toBe('box1');
138
+ expect(validToolboxes[1].id).toBe('box2');
139
+ });
140
+ test('merges tools from multiple toolboxes', function () {
141
+ var allTools = {};
142
+ var toolbox1 = { tool1: { name: 'Tool 1' } };
143
+ var toolbox2 = { tool2: { name: 'Tool 2' } };
144
+ allTools = __assign(__assign({}, allTools), toolbox1);
145
+ allTools = __assign(__assign({}, allTools), toolbox2);
146
+ expect(Object.keys(allTools)).toEqual(['tool1', 'tool2']);
147
+ });
148
+ test('later toolbox overwrites duplicate tool names', function () {
149
+ var allTools = {};
150
+ var toolbox1 = { duplicate: { name: 'Original' } };
151
+ var toolbox2 = { duplicate: { name: 'Overwritten' } };
152
+ allTools = __assign(__assign({}, allTools), toolbox1);
153
+ allTools = __assign(__assign({}, allTools), toolbox2);
154
+ expect(allTools.duplicate.name).toBe('Overwritten');
155
+ });
156
+ });
157
+ describe('tool output formatting', function () {
158
+ test('formats tool call output', function () {
159
+ var toolName = 'searchWeb';
160
+ var args = { query: 'test' };
161
+ var output = "Tool call: '".concat(toolName, "' ").concat(JSON.stringify(args));
162
+ expect(output).toBe("Tool call: 'searchWeb' {\"query\":\"test\"}");
163
+ });
164
+ test('formats tool response output', function () {
165
+ var toolName = 'searchWeb';
166
+ var result = { results: ['a', 'b'] };
167
+ var output = "Tool response: '".concat(toolName, "' ").concat(JSON.stringify(result));
168
+ expect(output).toBe("Tool response: 'searchWeb' {\"results\":[\"a\",\"b\"]}");
169
+ });
170
+ });
171
+ describe('max steps configuration', function () {
172
+ test('default max steps is 5', function () {
173
+ var maxSteps = 5;
174
+ expect(maxSteps).toBe(5);
175
+ });
176
+ });
177
+ describe('error handling', function () {
178
+ test('continues chat loop on error', function () {
179
+ var loopContinued = false;
180
+ var errorCaught = false;
181
+ try {
182
+ throw new Error('API Error');
183
+ }
184
+ catch (_a) {
185
+ errorCaught = true;
186
+ // Simulates continuing the loop after error
187
+ loopContinued = true;
188
+ }
189
+ expect(errorCaught).toBe(true);
190
+ expect(loopContinued).toBe(true);
191
+ });
192
+ });
193
+ });
194
+ describe('DainServiceConnection setup', function () {
195
+ test('constructs with url and auth', function () {
196
+ var url = 'https://service.example.com';
197
+ var apiKey = 'sk_agent_test';
198
+ var connection = {
199
+ url: url,
200
+ auth: { apiKey: apiKey },
201
+ };
202
+ expect(connection.url).toBe(url);
203
+ expect(connection.auth.apiKey).toBe(apiKey);
204
+ });
205
+ });
206
+ describe('system prompt', function () {
207
+ test('includes required instructions', function () {
208
+ var systemPrompt = "Keep your responses brief but insightful.\n use lots of emojis, to show your enthusiasm!\n Your name is Butterfly, Built by the team at Dain.";
209
+ expect(systemPrompt).toContain('brief');
210
+ expect(systemPrompt).toContain('emojis');
211
+ expect(systemPrompt).toContain('Butterfly');
212
+ expect(systemPrompt).toContain('Dain');
213
+ });
214
+ });