cdd-cli 4.0.0 → 4.2.0
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.es.md +60 -26
- package/README.md +60 -26
- package/dist/App.js +1 -0
- package/dist/components/ContainerCreationPrompt.js +14 -0
- package/dist/components/PromptField.js +13 -6
- package/dist/components/TextField.js +54 -0
- package/dist/components/UsageMenu.js +1 -1
- package/dist/helpers/appState.js +26 -0
- package/dist/helpers/dockerService/serviceComponents/containerExec.js +211 -0
- package/dist/helpers/textEditing.js +293 -0
- package/dist/hooks/creation/useContainerCreation.js +132 -15
- package/dist/hooks/useContainerCommandRouter.js +12 -2
- package/dist/hooks/useControls.js +21 -26
- package/dist/hooks/useShellMode.js +101 -0
- package/dist/index.js +3 -1
- package/package.json +4 -1
- package/.atl/skill-registry.md +0 -30
- package/.editorconfig +0 -12
- package/.eslintrc.json +0 -30
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -30
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -19
- package/.github/PULL_REQUEST_TEMPLATE/pull_request.md +0 -14
- package/.github/workflows/ci.yml +0 -32
- package/.github/workflows/docs.yml +0 -42
- package/.prettierignore +0 -4
- package/.prettierrc +0 -8
- package/.~lock.ROADMAP.md# +0 -1
- package/CHANGELOG.md +0 -216
- package/CODE_OF_CONDUCT.md +0 -12
- package/CONTRIBUTING.md +0 -35
- package/ROADMAP.md +0 -647
- package/SECURITY.md +0 -5
- package/__mocks__/ink.cjs +0 -17
- package/_config.yml +0 -13
- package/babel.config.cjs +0 -23
- package/coverage/clover.xml +0 -575
- package/coverage/coverage-final.json +0 -17
- package/coverage/lcov-report/ContainerCreationPrompt.jsx.html +0 -400
- package/coverage/lcov-report/ControlsHUD.jsx.html +0 -256
- package/coverage/lcov-report/base.css +0 -224
- package/coverage/lcov-report/block-navigation.js +0 -87
- package/coverage/lcov-report/components/ContainerCreationPrompt.jsx.html +0 -394
- package/coverage/lcov-report/components/PromptField.jsx.html +0 -229
- package/coverage/lcov-report/components/SuggestionPanel.jsx.html +0 -268
- package/coverage/lcov-report/components/index.html +0 -146
- package/coverage/lcov-report/favicon.png +0 -0
- package/coverage/lcov-report/helpers/constants.js.html +0 -352
- package/coverage/lcov-report/helpers/containerOptionsBuilder.js.html +0 -244
- package/coverage/lcov-report/helpers/dockerHubService.js.html +0 -310
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerActions.js.html +0 -931
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/containerList.js.html +0 -307
- package/coverage/lcov-report/helpers/dockerService/serviceComponents/index.html +0 -131
- package/coverage/lcov-report/helpers/imageNameUtils.js.html +0 -181
- package/coverage/lcov-report/helpers/index.html +0 -206
- package/coverage/lcov-report/helpers/logger.js.html +0 -424
- package/coverage/lcov-report/helpers/safeCall.js.html +0 -139
- package/coverage/lcov-report/helpers/validationHelpers.js.html +0 -346
- package/coverage/lcov-report/hooks/creation/index.html +0 -146
- package/coverage/lcov-report/hooks/creation/useContainerActions.js.html +0 -313
- package/coverage/lcov-report/hooks/creation/useContainerCreation.js.html +0 -982
- package/coverage/lcov-report/hooks/creation/useLogsViewer.js.html +0 -238
- package/coverage/lcov-report/hooks/debug/index.html +0 -116
- package/coverage/lcov-report/hooks/debug/useDebugLogs.js.html +0 -187
- package/coverage/lcov-report/hooks/index.html +0 -161
- package/coverage/lcov-report/hooks/navigation/index.html +0 -116
- package/coverage/lcov-report/hooks/navigation/useContainerSelection.js.html +0 -169
- package/coverage/lcov-report/hooks/useContainerCommandRouter.js.html +0 -454
- package/coverage/lcov-report/hooks/useControls.js.html +0 -868
- package/coverage/lcov-report/hooks/useEraseConfirmation.js.html +0 -202
- package/coverage/lcov-report/hooks/useExitHandler.js.html +0 -292
- package/coverage/lcov-report/index.html +0 -176
- package/coverage/lcov-report/prettify.css +0 -1
- package/coverage/lcov-report/prettify.js +0 -2
- package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
- package/coverage/lcov-report/sorter.js +0 -210
- package/coverage/lcov.info +0 -1163
- package/fix-imports.cjs +0 -54
- package/jest.config.cjs +0 -14
- package/src/App.jsx +0 -102
- package/src/components/ContainerCreationPrompt.jsx +0 -107
- package/src/components/ContainerList.jsx +0 -32
- package/src/components/ContainerRow.jsx +0 -84
- package/src/components/ContainerSection.jsx +0 -24
- package/src/components/ControlsHUD.jsx +0 -66
- package/src/components/Footer.jsx +0 -15
- package/src/components/Header.jsx +0 -41
- package/src/components/LogViewer.jsx +0 -36
- package/src/components/MessageFeedback.jsx +0 -25
- package/src/components/PromptField.jsx +0 -48
- package/src/components/StatsBar.jsx +0 -70
- package/src/components/SuggestionPanel.jsx +0 -61
- package/src/components/UsageMenu.jsx +0 -52
- package/src/helpers/appInfo.js +0 -23
- package/src/helpers/constants.js +0 -89
- package/src/helpers/containerOptionsBuilder.js +0 -53
- package/src/helpers/dockerHubService.js +0 -75
- package/src/helpers/dockerService/dockerService.js +0 -28
- package/src/helpers/dockerService/serviceComponents/containerActions.js +0 -282
- package/src/helpers/dockerService/serviceComponents/containerList.js +0 -74
- package/src/helpers/dockerService/serviceComponents/containerLogs.js +0 -60
- package/src/helpers/dockerService/serviceComponents/containerStats.js +0 -69
- package/src/helpers/dockerService/serviceComponents/imageUtils.js +0 -32
- package/src/helpers/exitWithMessage.js +0 -32
- package/src/helpers/imageNameUtils.js +0 -32
- package/src/helpers/logger.js +0 -112
- package/src/helpers/safeCall.js +0 -18
- package/src/helpers/validationHelpers.js +0 -87
- package/src/hooks/creation/useContainerActions.js +0 -89
- package/src/hooks/creation/useContainerCreation.js +0 -377
- package/src/hooks/creation/useLogsViewer.js +0 -51
- package/src/hooks/debug/useDebugLogs.js +0 -34
- package/src/hooks/navigation/useContainerSelection.js +0 -28
- package/src/hooks/useContainerCommandRouter.js +0 -123
- package/src/hooks/useContainerStats.js +0 -44
- package/src/hooks/useContainers.js +0 -26
- package/src/hooks/useControls.js +0 -263
- package/src/hooks/useEraseConfirmation.js +0 -39
- package/src/hooks/useExitHandler.js +0 -69
- package/src/index.js +0 -20
- package/test/ContainerCreationPrompt.dom.test.js +0 -313
- package/test/ControlsHUD.dom.test.js +0 -125
- package/test/SuggestionPanel.dom.test.js +0 -83
- package/test/constants.test.js +0 -41
- package/test/containerActions.test.js +0 -229
- package/test/containerList.test.js +0 -79
- package/test/containerOptionsBuilder.test.js +0 -36
- package/test/dockerHubService.test.js +0 -213
- package/test/jest.setup.js +0 -7
- package/test/logger.test.js +0 -58
- package/test/resolveImageTag.test.js +0 -26
- package/test/safeCall.test.js +0 -25
- package/test/useContainerCreation.dom.test.js +0 -567
- package/test/useContainerCreationHub.dom.test.js +0 -184
- package/test/useControls.dom.test.js +0 -519
- package/test/validationHelpers.test.js +0 -111
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import { jest } from '@jest/globals';
|
|
2
|
-
|
|
3
|
-
describe('containerActions service functions (mocked ESM imports)', () => {
|
|
4
|
-
afterEach(() => jest.resetModules());
|
|
5
|
-
|
|
6
|
-
test('createContainer returns { id, ports } when image exists', async () => {
|
|
7
|
-
const imageUtilsMock = {
|
|
8
|
-
imageExists: jest.fn().mockResolvedValue(true),
|
|
9
|
-
pullImage: jest.fn()
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: { '3306/tcp': {} } } });
|
|
13
|
-
const dockerMock = {
|
|
14
|
-
createContainer: jest.fn().mockResolvedValue({ id: 'cid-123' }),
|
|
15
|
-
getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
|
|
16
|
-
listContainers: jest.fn().mockResolvedValue([])
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
|
|
20
|
-
...imageUtilsMock
|
|
21
|
-
}));
|
|
22
|
-
|
|
23
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
24
|
-
docker: dockerMock
|
|
25
|
-
}));
|
|
26
|
-
|
|
27
|
-
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
28
|
-
|
|
29
|
-
const result = await mod.createContainer('mysql:8', { Name: 'test' });
|
|
30
|
-
expect(result).toHaveProperty('id', 'cid-123');
|
|
31
|
-
expect(result).toHaveProperty('ports');
|
|
32
|
-
expect(Array.isArray(result.ports)).toBe(true);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('createContainer ports contain containerPort, hostPort, protocol, source fields', async () => {
|
|
36
|
-
const imageUtilsMock = {
|
|
37
|
-
imageExists: jest.fn().mockResolvedValue(true),
|
|
38
|
-
pullImage: jest.fn()
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: { '80/tcp': {} } } });
|
|
42
|
-
const dockerMock = {
|
|
43
|
-
createContainer: jest.fn().mockResolvedValue({ id: 'cid-ports' }),
|
|
44
|
-
getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
|
|
45
|
-
listContainers: jest.fn().mockResolvedValue([])
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
|
|
49
|
-
...imageUtilsMock
|
|
50
|
-
}));
|
|
51
|
-
|
|
52
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
53
|
-
docker: dockerMock
|
|
54
|
-
}));
|
|
55
|
-
|
|
56
|
-
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
57
|
-
const result = await mod.createContainer('nginx:latest', {});
|
|
58
|
-
expect(result.ports.length).toBeGreaterThan(0);
|
|
59
|
-
const port = result.ports[0];
|
|
60
|
-
expect(port).toHaveProperty('containerPort');
|
|
61
|
-
expect(port).toHaveProperty('hostPort');
|
|
62
|
-
expect(port).toHaveProperty('protocol');
|
|
63
|
-
expect(port).toHaveProperty('source');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('createContainer uses IMAGE_PROFILES defaultPort as fallback when ExposedPorts empty', async () => {
|
|
67
|
-
const imageUtilsMock = {
|
|
68
|
-
imageExists: jest.fn().mockResolvedValue(true),
|
|
69
|
-
pullImage: jest.fn()
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// inspect returns empty ExposedPorts
|
|
73
|
-
const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: {} } });
|
|
74
|
-
const dockerMock = {
|
|
75
|
-
createContainer: jest.fn().mockResolvedValue({ id: 'cid-fallback' }),
|
|
76
|
-
getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
|
|
77
|
-
listContainers: jest.fn().mockResolvedValue([])
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
|
|
81
|
-
...imageUtilsMock
|
|
82
|
-
}));
|
|
83
|
-
|
|
84
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
85
|
-
docker: dockerMock
|
|
86
|
-
}));
|
|
87
|
-
|
|
88
|
-
const { IMAGE_PROFILES } = await import('../src/helpers/constants.js');
|
|
89
|
-
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
90
|
-
const result = await mod.createContainer('mysql:8', {}, IMAGE_PROFILES);
|
|
91
|
-
|
|
92
|
-
expect(result.id).toBe('cid-fallback');
|
|
93
|
-
// Should have auto port from defaultPort fallback
|
|
94
|
-
const mysqlDefault = IMAGE_PROFILES.mysql.defaultPort;
|
|
95
|
-
const portEntry = result.ports.find(p => p.containerPort === mysqlDefault);
|
|
96
|
-
expect(portEntry).toBeDefined();
|
|
97
|
-
expect(portEntry.source).toBe('auto');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test('createContainer returns { id, ports: [] } when no ports and image not in profiles', async () => {
|
|
101
|
-
const imageUtilsMock = {
|
|
102
|
-
imageExists: jest.fn().mockResolvedValue(true),
|
|
103
|
-
pullImage: jest.fn()
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: {} } });
|
|
107
|
-
const dockerMock = {
|
|
108
|
-
createContainer: jest.fn().mockResolvedValue({ id: 'cid-noport' }),
|
|
109
|
-
getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
|
|
110
|
-
listContainers: jest.fn().mockResolvedValue([])
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
|
|
114
|
-
...imageUtilsMock
|
|
115
|
-
}));
|
|
116
|
-
|
|
117
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
118
|
-
docker: dockerMock
|
|
119
|
-
}));
|
|
120
|
-
|
|
121
|
-
const { IMAGE_PROFILES } = await import('../src/helpers/constants.js');
|
|
122
|
-
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
123
|
-
const result = await mod.createContainer('unknownimage:latest', {}, IMAGE_PROFILES);
|
|
124
|
-
|
|
125
|
-
expect(result.id).toBe('cid-noport');
|
|
126
|
-
expect(result.ports).toEqual([]);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('createContainer pulls image when not present and returns { id, ports }', async () => {
|
|
130
|
-
const imageUtilsMock = {
|
|
131
|
-
imageExists: jest.fn().mockResolvedValue(false),
|
|
132
|
-
pullImage: jest.fn().mockResolvedValue(true)
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: {} } });
|
|
136
|
-
const dockerMock = {
|
|
137
|
-
createContainer: jest.fn().mockResolvedValue({ Id: 'CID-456' }),
|
|
138
|
-
getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
|
|
139
|
-
listContainers: jest.fn().mockResolvedValue([])
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
|
|
143
|
-
...imageUtilsMock
|
|
144
|
-
}));
|
|
145
|
-
|
|
146
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
147
|
-
docker: dockerMock
|
|
148
|
-
}));
|
|
149
|
-
|
|
150
|
-
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
151
|
-
const result = await mod.createContainer('busybox:1.0');
|
|
152
|
-
expect(result.id).toBe('CID-456');
|
|
153
|
-
expect(result).toHaveProperty('ports');
|
|
154
|
-
const imageUtils = await import('../src/helpers/dockerService/serviceComponents/imageUtils.js');
|
|
155
|
-
expect(imageUtils.pullImage).toHaveBeenCalledWith('busybox:1.0');
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('createContainer auto maps exposed ports when ports not provided', async () => {
|
|
159
|
-
const imageUtilsMock = {
|
|
160
|
-
imageExists: jest.fn().mockResolvedValue(true),
|
|
161
|
-
pullImage: jest.fn()
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const inspectMock = jest.fn().mockResolvedValue({ Config: { ExposedPorts: { '80/tcp': {} } } });
|
|
165
|
-
const createContainerMock = jest.fn().mockResolvedValue({ id: 'cid-auto' });
|
|
166
|
-
const dockerMock = {
|
|
167
|
-
createContainer: createContainerMock,
|
|
168
|
-
getImage: jest.fn().mockReturnValue({ inspect: inspectMock }),
|
|
169
|
-
listContainers: jest.fn().mockResolvedValue([
|
|
170
|
-
{ Ports: [{ PublicPort: 80, PrivatePort: 80 }] }
|
|
171
|
-
])
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({
|
|
175
|
-
...imageUtilsMock
|
|
176
|
-
}));
|
|
177
|
-
|
|
178
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
179
|
-
docker: dockerMock
|
|
180
|
-
}));
|
|
181
|
-
|
|
182
|
-
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
183
|
-
await mod.createContainer('nginx:alpine');
|
|
184
|
-
|
|
185
|
-
const [[createArgs]] = createContainerMock.mock.calls;
|
|
186
|
-
expect(createArgs.ExposedPorts).toEqual({ '80/tcp': {} });
|
|
187
|
-
expect(createArgs.HostConfig.PortBindings['80/tcp']).toEqual([{ HostPort: '81' }]);
|
|
188
|
-
expect(dockerMock.listContainers).toHaveBeenCalled();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test('removeContainer calls docker.remove with force', async () => {
|
|
192
|
-
const removeMock = jest.fn().mockResolvedValue(undefined);
|
|
193
|
-
const dockerMock = {
|
|
194
|
-
getContainer: jest.fn().mockReturnValue({ remove: removeMock })
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({ docker: dockerMock }));
|
|
198
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({ imageExists: jest.fn(), pullImage: jest.fn() }));
|
|
199
|
-
|
|
200
|
-
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
201
|
-
await mod.removeContainer('abc');
|
|
202
|
-
const ds = await import('../src/helpers/dockerService/dockerService.js');
|
|
203
|
-
expect(ds.docker.getContainer).toHaveBeenCalledWith('abc');
|
|
204
|
-
expect(removeMock).toHaveBeenCalledWith({ force: true });
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('start/stop/restart call respective container methods', async () => {
|
|
208
|
-
const startMock = jest.fn().mockResolvedValue(undefined);
|
|
209
|
-
const stopMock = jest.fn().mockResolvedValue(undefined);
|
|
210
|
-
const restartMock = jest.fn().mockResolvedValue(undefined);
|
|
211
|
-
|
|
212
|
-
const dockerMock = {
|
|
213
|
-
getContainer: jest.fn().mockReturnValue({ start: startMock, stop: stopMock, restart: restartMock })
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({ docker: dockerMock }));
|
|
217
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/serviceComponents/imageUtils.js', () => ({ imageExists: jest.fn(), pullImage: jest.fn() }));
|
|
218
|
-
|
|
219
|
-
const mod = await import('../src/helpers/dockerService/serviceComponents/containerActions.js');
|
|
220
|
-
|
|
221
|
-
await mod.startContainer('c1');
|
|
222
|
-
await mod.stopContainer('c1');
|
|
223
|
-
await mod.restartContainer('c1');
|
|
224
|
-
|
|
225
|
-
expect(startMock).toHaveBeenCalled();
|
|
226
|
-
expect(stopMock).toHaveBeenCalled();
|
|
227
|
-
expect(restartMock).toHaveBeenCalled();
|
|
228
|
-
});
|
|
229
|
-
});
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { jest } from '@jest/globals';
|
|
2
|
-
|
|
3
|
-
describe('containerList.getContainers', () => {
|
|
4
|
-
afterEach(() => jest.resetModules());
|
|
5
|
-
|
|
6
|
-
const baseContainer = {
|
|
7
|
-
Id: 'abc123',
|
|
8
|
-
Image: 'nginx:latest',
|
|
9
|
-
State: 'running',
|
|
10
|
-
Status: 'Up 5 minutes',
|
|
11
|
-
Ports: []
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
test('falls back to container.Name when Names array is missing', async () => {
|
|
15
|
-
const dockerMock = {
|
|
16
|
-
listContainers: jest.fn().mockResolvedValue([
|
|
17
|
-
{
|
|
18
|
-
...baseContainer,
|
|
19
|
-
Names: undefined,
|
|
20
|
-
Name: '/custom-container'
|
|
21
|
-
}
|
|
22
|
-
])
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
26
|
-
docker: dockerMock
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
const mod = await import('../src/helpers/dockerService/serviceComponents/containerList.js');
|
|
30
|
-
const containers = await mod.getContainers();
|
|
31
|
-
|
|
32
|
-
expect(containers).toHaveLength(1);
|
|
33
|
-
expect(containers[0].name).toBe('custom-container');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('returns Unknown when no usable name data is present', async () => {
|
|
37
|
-
const dockerMock = {
|
|
38
|
-
listContainers: jest.fn().mockResolvedValue([
|
|
39
|
-
{
|
|
40
|
-
...baseContainer,
|
|
41
|
-
Names: [],
|
|
42
|
-
Name: '',
|
|
43
|
-
Id: 'def456'
|
|
44
|
-
}
|
|
45
|
-
])
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
49
|
-
docker: dockerMock
|
|
50
|
-
}));
|
|
51
|
-
|
|
52
|
-
const mod = await import('../src/helpers/dockerService/serviceComponents/containerList.js');
|
|
53
|
-
const containers = await mod.getContainers();
|
|
54
|
-
|
|
55
|
-
expect(containers).toHaveLength(1);
|
|
56
|
-
expect(containers[0].name).toBe('Unknown');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('normalizes first entry in Names array', async () => {
|
|
60
|
-
const dockerMock = {
|
|
61
|
-
listContainers: jest.fn().mockResolvedValue([
|
|
62
|
-
{
|
|
63
|
-
...baseContainer,
|
|
64
|
-
Names: ['///leading/slash', '/ignored-second']
|
|
65
|
-
}
|
|
66
|
-
])
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
await jest.unstable_mockModule('../src/helpers/dockerService/dockerService.js', () => ({
|
|
70
|
-
docker: dockerMock
|
|
71
|
-
}));
|
|
72
|
-
|
|
73
|
-
const mod = await import('../src/helpers/dockerService/serviceComponents/containerList.js');
|
|
74
|
-
const containers = await mod.getContainers();
|
|
75
|
-
|
|
76
|
-
expect(containers).toHaveLength(1);
|
|
77
|
-
expect(containers[0].name).toBe('leading/slash');
|
|
78
|
-
});
|
|
79
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
let buildContainerOptions;
|
|
2
|
-
|
|
3
|
-
beforeAll(async () => {
|
|
4
|
-
const mod = await import('../src/helpers/containerOptionsBuilder.js');
|
|
5
|
-
buildContainerOptions = mod.buildContainerOptions;
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
describe('buildContainerOptions', () => {
|
|
9
|
-
test('basic image only sets Tty', () => {
|
|
10
|
-
const opts = buildContainerOptions({ imageName: 'nginx' });
|
|
11
|
-
expect(opts.Tty).toBe(true);
|
|
12
|
-
expect(opts.Env).toBeUndefined();
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test('env var with value containing = is preserved intact', () => {
|
|
16
|
-
const opts = buildContainerOptions({ imageName: 'nginx', envInput: 'TOKEN=a=b' });
|
|
17
|
-
expect(opts.Env).toEqual(['TOKEN=a=b']);
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test('multiple env vars with = in values are all preserved', () => {
|
|
21
|
-
const opts = buildContainerOptions({ imageName: 'nginx', envInput: 'TOKEN=a=b,DSN=user:pass@host/db?opt=1' });
|
|
22
|
-
expect(opts.Env).toContain('TOKEN=a=b');
|
|
23
|
-
expect(opts.Env).toContain('DSN=user:pass@host/db?opt=1');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
test('port input builds ExposedPorts and PortBindings', () => {
|
|
27
|
-
const opts = buildContainerOptions({ imageName: 'nginx', portInput: '8080:80' });
|
|
28
|
-
expect(opts.ExposedPorts).toEqual({ '80/tcp': {} });
|
|
29
|
-
expect(opts.HostConfig.PortBindings['80/tcp']).toEqual([{ HostPort: '8080' }]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('container name is included when provided', () => {
|
|
33
|
-
const opts = buildContainerOptions({ imageName: 'nginx', containerName: 'my-nginx' });
|
|
34
|
-
expect(opts.name).toBe('my-nginx');
|
|
35
|
-
});
|
|
36
|
-
});
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @jest-environment node
|
|
3
|
-
*/
|
|
4
|
-
import { jest } from '@jest/globals';
|
|
5
|
-
import { formatHubResult } from '../src/helpers/dockerHubService.js';
|
|
6
|
-
|
|
7
|
-
// We mock fetch at the global level using jest.spyOn
|
|
8
|
-
// The module under test is imported dynamically after spy setup
|
|
9
|
-
|
|
10
|
-
describe('searchDockerHub — unit tests', () => {
|
|
11
|
-
let searchDockerHub;
|
|
12
|
-
let fetchSpy;
|
|
13
|
-
|
|
14
|
-
beforeEach(async () => {
|
|
15
|
-
// Reset modules so each test gets fresh state
|
|
16
|
-
jest.resetModules();
|
|
17
|
-
// Dynamically import after setting up spy
|
|
18
|
-
({ searchDockerHub } = await import('../src/helpers/dockerHubService.js'));
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
if (fetchSpy) fetchSpy.mockRestore();
|
|
23
|
-
fetchSpy = null;
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// ── 1. Query trim & encode ──────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
test('trims whitespace from query before encoding', async () => {
|
|
29
|
-
fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
|
|
30
|
-
ok: true,
|
|
31
|
-
json: async () => ({ results: [] }),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
await searchDockerHub(' nginx ');
|
|
35
|
-
|
|
36
|
-
const calledUrl = fetchSpy.mock.calls[0][0];
|
|
37
|
-
expect(calledUrl).toContain('query=nginx');
|
|
38
|
-
expect(calledUrl).not.toContain('query=+nginx');
|
|
39
|
-
expect(calledUrl).not.toContain(' ');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('encodes special characters in query', async () => {
|
|
43
|
-
fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
|
|
44
|
-
ok: true,
|
|
45
|
-
json: async () => ({ results: [] }),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
await searchDockerHub('hello world');
|
|
49
|
-
|
|
50
|
-
const calledUrl = fetchSpy.mock.calls[0][0];
|
|
51
|
-
expect(calledUrl).toContain('query=hello%20world');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('returns [] immediately for empty query (no fetch call)', async () => {
|
|
55
|
-
fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
|
|
56
|
-
ok: true,
|
|
57
|
-
json: async () => ({ results: [] }),
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const result = await searchDockerHub(' ');
|
|
61
|
-
|
|
62
|
-
expect(result).toEqual([]);
|
|
63
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// ── 2. AbortSignal.timeout(5000) default ──────────────────────────────
|
|
67
|
-
|
|
68
|
-
test('uses AbortSignal.timeout(5000) by default when no signal provided', async () => {
|
|
69
|
-
const timeoutSignalSpy = jest.spyOn(AbortSignal, 'timeout');
|
|
70
|
-
fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
|
|
71
|
-
ok: true,
|
|
72
|
-
json: async () => ({ results: [] }),
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
await searchDockerHub('nginx');
|
|
76
|
-
|
|
77
|
-
expect(timeoutSignalSpy).toHaveBeenCalledWith(5000);
|
|
78
|
-
timeoutSignalSpy.mockRestore();
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('does NOT call AbortSignal.timeout when signal is injected', async () => {
|
|
82
|
-
const timeoutSignalSpy = jest.spyOn(AbortSignal, 'timeout');
|
|
83
|
-
const controller = new AbortController();
|
|
84
|
-
fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
|
|
85
|
-
ok: true,
|
|
86
|
-
json: async () => ({ results: [] }),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
await searchDockerHub('nginx', { signal: controller.signal });
|
|
90
|
-
|
|
91
|
-
expect(timeoutSignalSpy).not.toHaveBeenCalled();
|
|
92
|
-
timeoutSignalSpy.mockRestore();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// ── 3. Injected signal is passed to fetch ──────────────────────────────
|
|
96
|
-
|
|
97
|
-
test('passes injected signal to fetch', async () => {
|
|
98
|
-
const controller = new AbortController();
|
|
99
|
-
fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
|
|
100
|
-
ok: true,
|
|
101
|
-
json: async () => ({ results: [] }),
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
await searchDockerHub('nginx', { signal: controller.signal });
|
|
105
|
-
|
|
106
|
-
const fetchOptions = fetchSpy.mock.calls[0][1];
|
|
107
|
-
expect(fetchOptions.signal).toBe(controller.signal);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// ── 4. Result mapping ──────────────────────────────────────────────────
|
|
111
|
-
|
|
112
|
-
test('maps results to normalized objects with name, isOfficial, pullCount, description', async () => {
|
|
113
|
-
fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
|
|
114
|
-
ok: true,
|
|
115
|
-
json: async () => ({
|
|
116
|
-
results: [
|
|
117
|
-
{
|
|
118
|
-
repo_name: 'library/nginx',
|
|
119
|
-
is_official: true,
|
|
120
|
-
pull_count: 1000000000,
|
|
121
|
-
short_description: 'Official nginx image',
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
repo_name: 'bitnami/nginx',
|
|
125
|
-
is_official: false,
|
|
126
|
-
pull_count: 50000000,
|
|
127
|
-
short_description: 'Bitnami nginx image',
|
|
128
|
-
},
|
|
129
|
-
],
|
|
130
|
-
}),
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const results = await searchDockerHub('nginx');
|
|
134
|
-
|
|
135
|
-
expect(results).toHaveLength(2);
|
|
136
|
-
expect(results[0]).toEqual({
|
|
137
|
-
name: 'library/nginx',
|
|
138
|
-
isOfficial: true,
|
|
139
|
-
pullCount: 1000000000,
|
|
140
|
-
description: 'Official nginx image',
|
|
141
|
-
});
|
|
142
|
-
expect(results[1]).toEqual({
|
|
143
|
-
name: 'bitnami/nginx',
|
|
144
|
-
isOfficial: false,
|
|
145
|
-
pullCount: 50000000,
|
|
146
|
-
description: 'Bitnami nginx image',
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test('returns empty array when results is missing from response', async () => {
|
|
151
|
-
fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
|
|
152
|
-
ok: true,
|
|
153
|
-
json: async () => ({}),
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const results = await searchDockerHub('nginx');
|
|
157
|
-
expect(results).toEqual([]);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
// ── 5. Non-OK rejection ────────────────────────────────────────────────
|
|
161
|
-
|
|
162
|
-
test('throws when response.ok is false', async () => {
|
|
163
|
-
fetchSpy = jest.spyOn(global, 'fetch').mockResolvedValue({
|
|
164
|
-
ok: false,
|
|
165
|
-
status: 429,
|
|
166
|
-
json: async () => ({}),
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
await expect(searchDockerHub('nginx')).rejects.toThrow(
|
|
170
|
-
'Docker Hub search failed: 429'
|
|
171
|
-
);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
test('re-throws AbortError from fetch', async () => {
|
|
175
|
-
const abortErr = new DOMException('Aborted', 'AbortError');
|
|
176
|
-
fetchSpy = jest.spyOn(global, 'fetch').mockRejectedValue(abortErr);
|
|
177
|
-
|
|
178
|
-
await expect(searchDockerHub('nginx')).rejects.toThrow('Aborted');
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
describe('formatHubResult — unit tests', () => {
|
|
183
|
-
test('official repo includes [★ official]', () => {
|
|
184
|
-
const result = formatHubResult({ name: 'library/nginx', isOfficial: true, pullCount: 0 });
|
|
185
|
-
expect(result).toContain('[★ official]');
|
|
186
|
-
expect(result).toContain('library/nginx');
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
test('high pull count (≥1B) is formatted as 1B+', () => {
|
|
190
|
-
const result = formatHubResult({ name: 'nginx', isOfficial: true, pullCount: 1e9 });
|
|
191
|
-
expect(result).toContain('[1B+ pulls]');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test('pull count ≥1M is formatted as NM+', () => {
|
|
195
|
-
const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 500e6 });
|
|
196
|
-
expect(result).toContain('[500M+ pulls]');
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
test('pull count ≥1K is formatted as NK+', () => {
|
|
200
|
-
const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 500e3 });
|
|
201
|
-
expect(result).toContain('[500K+ pulls]');
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test('no pull count → just name (no suffix)', () => {
|
|
205
|
-
const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 0 });
|
|
206
|
-
expect(result).toBe('myimage');
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test('unofficial + low pull count → just name with raw count', () => {
|
|
210
|
-
const result = formatHubResult({ name: 'myimage', isOfficial: false, pullCount: 42 });
|
|
211
|
-
expect(result).toBe('myimage [42 pulls]');
|
|
212
|
-
});
|
|
213
|
-
});
|
package/test/jest.setup.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
// Minimal test setup for React/DOM environment
|
|
2
|
-
// Could add more global matchers here
|
|
3
|
-
import '@testing-library/jest-dom';
|
|
4
|
-
|
|
5
|
-
// Silence act() warnings in environments where receiver isn't concurrent
|
|
6
|
-
// (react-test-renderer/act warnings are printed, but tests can proceed)
|
|
7
|
-
// If needed, configure global helpers here.
|
package/test/logger.test.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for src/helpers/logger.js
|
|
3
|
-
* Verifies that console output is suppressed when Ink listeners are active.
|
|
4
|
-
*/
|
|
5
|
-
import { jest } from '@jest/globals';
|
|
6
|
-
|
|
7
|
-
const { logger } = await import('../src/helpers/logger.js');
|
|
8
|
-
|
|
9
|
-
describe('logger — console output suppression', () => {
|
|
10
|
-
let consoleSpy;
|
|
11
|
-
let warnSpy;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
15
|
-
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
afterEach(() => {
|
|
19
|
-
consoleSpy.mockRestore();
|
|
20
|
-
warnSpy.mockRestore();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('when NO listeners are registered, logger.error calls console.error (fallback)', () => {
|
|
24
|
-
logger.error('test error no listener');
|
|
25
|
-
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('when a listener IS registered, logger.error does NOT call console.error', () => {
|
|
29
|
-
const unsubscribe = logger.subscribe(() => {});
|
|
30
|
-
logger.error('test error with listener');
|
|
31
|
-
expect(consoleSpy).not.toHaveBeenCalled();
|
|
32
|
-
unsubscribe();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test('when a listener IS registered, logger.warn does NOT call console.warn', () => {
|
|
36
|
-
const unsubscribe = logger.subscribe(() => {});
|
|
37
|
-
logger.warn('test warn with listener');
|
|
38
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
39
|
-
unsubscribe();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('after unsubscribing last listener, logger.error resumes calling console.error', () => {
|
|
43
|
-
const unsubscribe = logger.subscribe(() => {});
|
|
44
|
-
unsubscribe();
|
|
45
|
-
logger.error('after unsubscribe');
|
|
46
|
-
expect(consoleSpy).toHaveBeenCalledTimes(1);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('listener is invoked with the log entry even when console output is suppressed', () => {
|
|
50
|
-
const received = [];
|
|
51
|
-
const unsubscribe = logger.subscribe((entry) => received.push(entry));
|
|
52
|
-
logger.error('entry delivered', 'extra-arg');
|
|
53
|
-
expect(received).toHaveLength(1);
|
|
54
|
-
expect(received[0].level).toBe('error');
|
|
55
|
-
expect(received[0].message).toBe('entry delivered');
|
|
56
|
-
unsubscribe();
|
|
57
|
-
});
|
|
58
|
-
});
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @jest-environment node
|
|
3
|
-
*/
|
|
4
|
-
import { resolveImageTag, IMAGE_PROFILES } from '../src/helpers/constants.js';
|
|
5
|
-
|
|
6
|
-
describe('resolveImageTag', () => {
|
|
7
|
-
test('appends defaultTag when image has no tag', () => {
|
|
8
|
-
expect(resolveImageTag('postgres', IMAGE_PROFILES)).toBe('postgres:17-alpine');
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
test('preserves explicit tag when image already has one', () => {
|
|
12
|
-
expect(resolveImageTag('postgres:15', IMAGE_PROFILES)).toBe('postgres:15');
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
test('returns image name unchanged when no matching profile', () => {
|
|
16
|
-
expect(resolveImageTag('unknownimage', IMAGE_PROFILES)).toBe('unknownimage');
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
test('appends defaultTag for nginx', () => {
|
|
20
|
-
expect(resolveImageTag('nginx', IMAGE_PROFILES)).toBe('nginx:1.27-alpine');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('passes through empty string unchanged', () => {
|
|
24
|
-
expect(resolveImageTag('', IMAGE_PROFILES)).toBe('');
|
|
25
|
-
});
|
|
26
|
-
});
|
package/test/safeCall.test.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { safeCall } from '../src/helpers/safeCall.js';
|
|
2
|
-
|
|
3
|
-
describe('safeCall', () => {
|
|
4
|
-
test('calls function and returns value', () => {
|
|
5
|
-
function fn(a, b) {
|
|
6
|
-
fn.called = true;
|
|
7
|
-
fn.args = [a, b];
|
|
8
|
-
return a + b;
|
|
9
|
-
}
|
|
10
|
-
const res = safeCall(fn, 2, 3);
|
|
11
|
-
expect(fn.called).toBe(true);
|
|
12
|
-
expect(fn.args).toEqual([2, 3]);
|
|
13
|
-
expect(res).toBe(5);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
test('returns undefined when fn is not a function', () => {
|
|
17
|
-
const res = safeCall(undefined, 1, 2);
|
|
18
|
-
expect(res).toBeUndefined();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test('catches errors thrown by function and does not rethrow', () => {
|
|
22
|
-
const bad = () => { throw new Error('boom'); };
|
|
23
|
-
expect(() => safeCall(bad)).not.toThrow();
|
|
24
|
-
});
|
|
25
|
-
});
|