kastell 2.2.3 → 2.2.5
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/.claude-plugin/marketplace.json +18 -18
- package/.claude-plugin/plugin.json +45 -39
- package/CHANGELOG.md +1294 -1266
- package/LICENSE +201 -201
- package/NOTICE +5 -5
- package/README.md +1 -1
- package/README.tr.md +1 -1
- package/bin/kastell +2 -2
- package/bin/kastell-mcp +5 -5
- package/dist/adapters/coolify.js +92 -92
- package/dist/adapters/dokploy.js +99 -99
- package/dist/core/audit/formatters/badge.js +20 -20
- package/dist/core/completions.js +631 -631
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +25 -31
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/serverExplain.d.ts.map +1 -1
- package/dist/mcp/tools/serverExplain.js.map +1 -1
- package/dist/mcp/tools/serverFleet.d.ts.map +1 -1
- package/dist/mcp/tools/serverFleet.js.map +1 -1
- package/dist/mcp/tools/serverInfo.d.ts +1 -1
- package/dist/mcp/tools/serverInfo.js +1 -1
- package/dist/mcp/tools/serverPlugin.d.ts.map +1 -1
- package/dist/mcp/tools/serverPlugin.js.map +1 -1
- package/dist/mcp-bundle.mjs +101015 -0
- package/dist/utils/cloudInit.js +58 -58
- package/dist/utils/version.d.ts.map +1 -1
- package/dist/utils/version.js +19 -4
- package/dist/utils/version.js.map +1 -1
- package/kastell-plugin/.claude-plugin/plugin.json +20 -20
- package/kastell-plugin/.mcp.json +15 -8
- package/kastell-plugin/README.md +113 -113
- package/kastell-plugin/agents/kastell-auditor.md +77 -77
- package/kastell-plugin/agents/scripts/bucket_mapper.sh +101 -101
- package/kastell-plugin/agents/scripts/trend_report.sh +91 -91
- package/kastell-plugin/hooks/destroy-block.cjs +31 -31
- package/kastell-plugin/hooks/hooks.json +57 -57
- package/kastell-plugin/hooks/pre-commit-audit-guard.cjs +75 -75
- package/kastell-plugin/hooks/session-audit.cjs +86 -86
- package/kastell-plugin/hooks/session-log.cjs +56 -56
- package/kastell-plugin/hooks/stop-quality-check.cjs +72 -72
- package/kastell-plugin/skills/kastell-careful/SKILL.md +64 -64
- package/kastell-plugin/skills/kastell-ops/SKILL.md +139 -139
- package/kastell-plugin/skills/kastell-ops/references/commands.md +45 -45
- package/kastell-plugin/skills/kastell-ops/references/mcp-tools.md +50 -50
- package/kastell-plugin/skills/kastell-ops/references/patterns.md +145 -145
- package/kastell-plugin/skills/kastell-ops/references/pitfalls.md +136 -136
- package/kastell-plugin/skills/kastell-ops/scripts/check_coverage.sh +101 -101
- package/kastell-plugin/skills/kastell-ops/scripts/fleet_report.sh +73 -73
- package/kastell-plugin/skills/kastell-ops/scripts/parse_audit.sh +76 -76
- package/kastell-plugin/skills/kastell-research/SKILL.md +90 -90
- package/kastell-plugin/skills/kastell-scaffold/SKILL.md +104 -104
- package/kastell-plugin/skills/kastell-scaffold/references/template-audit-check.md +150 -150
- package/kastell-plugin/skills/kastell-scaffold/references/template-command.md +80 -80
- package/kastell-plugin/skills/kastell-scaffold/references/template-mcp-tool.md +72 -72
- package/kastell-plugin/skills/kastell-scaffold/references/template-provider.md +67 -67
- package/kastell-plugin/skills/kastell-scaffold/scripts/scaffold.sh +180 -180
- package/kastell-plugin/skills/kastell-scaffold/templates/check-test.ts.tpl +27 -27
- package/kastell-plugin/skills/kastell-scaffold/templates/check.ts.tpl +50 -50
- package/kastell-plugin/skills/kastell-scaffold/templates/command-core.ts.tpl +18 -18
- package/kastell-plugin/skills/kastell-scaffold/templates/command-test.ts.tpl +17 -17
- package/kastell-plugin/skills/kastell-scaffold/templates/command.ts.tpl +25 -25
- package/kastell-plugin/skills/kastell-scaffold/templates/mcp-tool-test.ts.tpl +30 -30
- package/kastell-plugin/skills/kastell-scaffold/templates/mcp-tool.ts.tpl +29 -29
- package/kastell-plugin/skills/kastell-scaffold/templates/provider-test.ts.tpl +34 -34
- package/kastell-plugin/skills/kastell-scaffold/templates/provider.ts.tpl +32 -32
- package/package.json +125 -122
- package/dist/commands/interactive.d.ts +0 -11
- package/dist/commands/interactive.d.ts.map +0 -1
- package/dist/commands/interactive.js +0 -1079
- package/dist/commands/interactive.js.map +0 -1
- package/dist/core/lock.d.ts +0 -66
- package/dist/core/lock.d.ts.map +0 -1
- package/dist/core/lock.js +0 -556
- package/dist/core/lock.js.map +0 -1
|
@@ -1,145 +1,145 @@
|
|
|
1
|
-
# Kastell Patterns
|
|
2
|
-
|
|
3
|
-
## Do / Don't
|
|
4
|
-
|
|
5
|
-
| Do | Don't |
|
|
6
|
-
|-----------------------------------------------------------------------|--------------------------------------------------------------------|
|
|
7
|
-
| Put all business logic in `src/core/` | Put logic in `src/commands/` — commands are thin wrappers only |
|
|
8
|
-
| Use `getAdapter(platform)` from `factory.ts` | Import `CoolifyAdapter` / `DokployAdapter` directly in commands |
|
|
9
|
-
| Use `adapter.port`, `adapter.defaultLogService`, `adapter.platformPorts` | Hardcode port numbers (8000, 3000) in command files |
|
|
10
|
-
| Use `withProviderErrorHandling` HOF for provider operations | Write per-command try/catch around provider API calls |
|
|
11
|
-
| Use `assertValidIp()` before every SSH operation | Call `sshExec` without IP validation |
|
|
12
|
-
| Use `sanitizedEnv` for subprocess calls | Pass raw `process.env` to child processes |
|
|
13
|
-
| Use `sanitizeResponseData()` for API error responses | Leak raw API error objects to the user |
|
|
14
|
-
| Use `createMockAdapter()` from `tests/helpers/mockAdapter.ts` | Write inline `{ healthCheck: jest.fn() }` objects in tests |
|
|
15
|
-
| Batch SSH commands (fast config + slow probes) in audit operations | Call `sshExec` multiple times fetching overlapping data |
|
|
16
|
-
| Return plain data objects from `src/core/` functions | Import `chalk` or `ora` in `src/core/` files |
|
|
17
|
-
| Use `jest.resetAllMocks()` with `describe.each` | Use `clearAllMocks()` with `describe.each` (causes cross-test bleed) |
|
|
18
|
-
|
|
19
|
-
## Testing Patterns
|
|
20
|
-
|
|
21
|
-
### Pattern 1: Mock Adapter Factory
|
|
22
|
-
|
|
23
|
-
Use `createMockAdapter()` from `tests/helpers/mockAdapter.ts`. Never write inline mock adapter objects.
|
|
24
|
-
|
|
25
|
-
```typescript
|
|
26
|
-
import { createMockAdapter } from '../../tests/helpers/mockAdapter.js';
|
|
27
|
-
|
|
28
|
-
// Basic usage — gets correct defaults for the platform
|
|
29
|
-
const adapter = createMockAdapter({ name: 'coolify' });
|
|
30
|
-
// adapter.port === 8000, adapter.defaultLogService === 'coolify'
|
|
31
|
-
|
|
32
|
-
// Override specific methods
|
|
33
|
-
const adapter = createMockAdapter({
|
|
34
|
-
name: 'dokploy',
|
|
35
|
-
overrides: {
|
|
36
|
-
healthCheck: jest.fn(async () => ({ status: 'not reachable' as const })),
|
|
37
|
-
},
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// In the test — inject via jest.mock or direct parameter
|
|
41
|
-
jest.mock('../../src/adapters/factory.js', () => ({
|
|
42
|
-
getAdapter: jest.fn(() => adapter),
|
|
43
|
-
resolvePlatform: jest.fn(() => 'coolify'),
|
|
44
|
-
}));
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### Pattern 2: SSH Mock
|
|
48
|
-
|
|
49
|
-
Mock `sshExec` at the module level. Use `mockResolvedValueOnce` for sequential calls.
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
import { sshExec } from '../../src/utils/ssh.js';
|
|
53
|
-
jest.mock('../../src/utils/ssh.js');
|
|
54
|
-
const mockSshExec = sshExec as jest.MockedFunction<typeof sshExec>;
|
|
55
|
-
|
|
56
|
-
// Single call
|
|
57
|
-
mockSshExec.mockResolvedValueOnce({ code: 0, stdout: 'active', stderr: '' });
|
|
58
|
-
|
|
59
|
-
// Multiple sequential calls (batch grouping pattern)
|
|
60
|
-
mockSshExec
|
|
61
|
-
.mockResolvedValueOnce({ code: 0, stdout: 'ufw active', stderr: '' }) // fast config
|
|
62
|
-
.mockResolvedValueOnce({ code: 0, stdout: '{"load": 0.5}', stderr: '' }); // slow probe
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Pattern 3: MCP Handler Test
|
|
66
|
-
|
|
67
|
-
Import the handler directly and call it with the expected parameters. Assert on `result.content[0].text`.
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
import { handleServerAudit } from '../serverAudit.js';
|
|
71
|
-
|
|
72
|
-
// Mock the core dependency
|
|
73
|
-
jest.mock('../../src/core/audit/runner.js');
|
|
74
|
-
import { runAudit } from '../../src/core/audit/runner.js';
|
|
75
|
-
const mockRunAudit = runAudit as jest.MockedFunction<typeof runAudit>;
|
|
76
|
-
|
|
77
|
-
describe('handleServerAudit', () => {
|
|
78
|
-
beforeEach(() => jest.resetAllMocks());
|
|
79
|
-
|
|
80
|
-
it('returns audit results', async () => {
|
|
81
|
-
mockRunAudit.mockResolvedValueOnce({ score: 85, checks: [] });
|
|
82
|
-
|
|
83
|
-
const result = await handleServerAudit({
|
|
84
|
-
server: 'test-server',
|
|
85
|
-
category: 'ssh',
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
expect(result.isError).toBeUndefined();
|
|
89
|
-
expect(result.content[0].text).toContain('85');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('returns error on failure', async () => {
|
|
93
|
-
mockRunAudit.mockRejectedValueOnce(new Error('SSH timeout'));
|
|
94
|
-
|
|
95
|
-
const result = await handleServerAudit({ server: 'test-server' });
|
|
96
|
-
|
|
97
|
-
expect(result.isError).toBe(true);
|
|
98
|
-
expect(result.content[0].text).toContain('SSH timeout');
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### Pattern 4: Command Test (mock core, not low-level deps)
|
|
104
|
-
|
|
105
|
-
Test commands by mocking the core module — not `sshExec`, not providers. This verifies delegation.
|
|
106
|
-
|
|
107
|
-
```typescript
|
|
108
|
-
jest.mock('../../src/core/backup.js');
|
|
109
|
-
import { backupServer } from '../../src/core/backup.js';
|
|
110
|
-
const mockBackupServer = backupServer as jest.MockedFunction<typeof backupServer>;
|
|
111
|
-
|
|
112
|
-
describe('backup command', () => {
|
|
113
|
-
beforeEach(() => jest.resetAllMocks());
|
|
114
|
-
|
|
115
|
-
it('delegates to backupServer core function', async () => {
|
|
116
|
-
mockBackupServer.mockResolvedValueOnce({
|
|
117
|
-
success: true,
|
|
118
|
-
backupPath: '/home/user/.kastell/backups/myserver',
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
// Execute command action directly (Commander action callback)
|
|
122
|
-
await backupAction({ server: 'myserver' });
|
|
123
|
-
|
|
124
|
-
expect(mockBackupServer).toHaveBeenCalledWith('myserver');
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## Core Function Signature Conventions
|
|
130
|
-
|
|
131
|
-
Core functions return typed result objects, never throw to the caller:
|
|
132
|
-
|
|
133
|
-
```typescript
|
|
134
|
-
// Pattern: return { success, data?, error?, hint? }
|
|
135
|
-
export async function exampleCore(server: string): Promise<ExampleResult> {
|
|
136
|
-
try {
|
|
137
|
-
const record = getServerRecord(server); // throws if not found
|
|
138
|
-
assertValidIp(record.ip); // throws if invalid
|
|
139
|
-
// ... business logic ...
|
|
140
|
-
return { success: true, data: result };
|
|
141
|
-
} catch (err) {
|
|
142
|
-
return { success: false, error: String(err) };
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
```
|
|
1
|
+
# Kastell Patterns
|
|
2
|
+
|
|
3
|
+
## Do / Don't
|
|
4
|
+
|
|
5
|
+
| Do | Don't |
|
|
6
|
+
|-----------------------------------------------------------------------|--------------------------------------------------------------------|
|
|
7
|
+
| Put all business logic in `src/core/` | Put logic in `src/commands/` — commands are thin wrappers only |
|
|
8
|
+
| Use `getAdapter(platform)` from `factory.ts` | Import `CoolifyAdapter` / `DokployAdapter` directly in commands |
|
|
9
|
+
| Use `adapter.port`, `adapter.defaultLogService`, `adapter.platformPorts` | Hardcode port numbers (8000, 3000) in command files |
|
|
10
|
+
| Use `withProviderErrorHandling` HOF for provider operations | Write per-command try/catch around provider API calls |
|
|
11
|
+
| Use `assertValidIp()` before every SSH operation | Call `sshExec` without IP validation |
|
|
12
|
+
| Use `sanitizedEnv` for subprocess calls | Pass raw `process.env` to child processes |
|
|
13
|
+
| Use `sanitizeResponseData()` for API error responses | Leak raw API error objects to the user |
|
|
14
|
+
| Use `createMockAdapter()` from `tests/helpers/mockAdapter.ts` | Write inline `{ healthCheck: jest.fn() }` objects in tests |
|
|
15
|
+
| Batch SSH commands (fast config + slow probes) in audit operations | Call `sshExec` multiple times fetching overlapping data |
|
|
16
|
+
| Return plain data objects from `src/core/` functions | Import `chalk` or `ora` in `src/core/` files |
|
|
17
|
+
| Use `jest.resetAllMocks()` with `describe.each` | Use `clearAllMocks()` with `describe.each` (causes cross-test bleed) |
|
|
18
|
+
|
|
19
|
+
## Testing Patterns
|
|
20
|
+
|
|
21
|
+
### Pattern 1: Mock Adapter Factory
|
|
22
|
+
|
|
23
|
+
Use `createMockAdapter()` from `tests/helpers/mockAdapter.ts`. Never write inline mock adapter objects.
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { createMockAdapter } from '../../tests/helpers/mockAdapter.js';
|
|
27
|
+
|
|
28
|
+
// Basic usage — gets correct defaults for the platform
|
|
29
|
+
const adapter = createMockAdapter({ name: 'coolify' });
|
|
30
|
+
// adapter.port === 8000, adapter.defaultLogService === 'coolify'
|
|
31
|
+
|
|
32
|
+
// Override specific methods
|
|
33
|
+
const adapter = createMockAdapter({
|
|
34
|
+
name: 'dokploy',
|
|
35
|
+
overrides: {
|
|
36
|
+
healthCheck: jest.fn(async () => ({ status: 'not reachable' as const })),
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// In the test — inject via jest.mock or direct parameter
|
|
41
|
+
jest.mock('../../src/adapters/factory.js', () => ({
|
|
42
|
+
getAdapter: jest.fn(() => adapter),
|
|
43
|
+
resolvePlatform: jest.fn(() => 'coolify'),
|
|
44
|
+
}));
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Pattern 2: SSH Mock
|
|
48
|
+
|
|
49
|
+
Mock `sshExec` at the module level. Use `mockResolvedValueOnce` for sequential calls.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { sshExec } from '../../src/utils/ssh.js';
|
|
53
|
+
jest.mock('../../src/utils/ssh.js');
|
|
54
|
+
const mockSshExec = sshExec as jest.MockedFunction<typeof sshExec>;
|
|
55
|
+
|
|
56
|
+
// Single call
|
|
57
|
+
mockSshExec.mockResolvedValueOnce({ code: 0, stdout: 'active', stderr: '' });
|
|
58
|
+
|
|
59
|
+
// Multiple sequential calls (batch grouping pattern)
|
|
60
|
+
mockSshExec
|
|
61
|
+
.mockResolvedValueOnce({ code: 0, stdout: 'ufw active', stderr: '' }) // fast config
|
|
62
|
+
.mockResolvedValueOnce({ code: 0, stdout: '{"load": 0.5}', stderr: '' }); // slow probe
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Pattern 3: MCP Handler Test
|
|
66
|
+
|
|
67
|
+
Import the handler directly and call it with the expected parameters. Assert on `result.content[0].text`.
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { handleServerAudit } from '../serverAudit.js';
|
|
71
|
+
|
|
72
|
+
// Mock the core dependency
|
|
73
|
+
jest.mock('../../src/core/audit/runner.js');
|
|
74
|
+
import { runAudit } from '../../src/core/audit/runner.js';
|
|
75
|
+
const mockRunAudit = runAudit as jest.MockedFunction<typeof runAudit>;
|
|
76
|
+
|
|
77
|
+
describe('handleServerAudit', () => {
|
|
78
|
+
beforeEach(() => jest.resetAllMocks());
|
|
79
|
+
|
|
80
|
+
it('returns audit results', async () => {
|
|
81
|
+
mockRunAudit.mockResolvedValueOnce({ score: 85, checks: [] });
|
|
82
|
+
|
|
83
|
+
const result = await handleServerAudit({
|
|
84
|
+
server: 'test-server',
|
|
85
|
+
category: 'ssh',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
expect(result.isError).toBeUndefined();
|
|
89
|
+
expect(result.content[0].text).toContain('85');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('returns error on failure', async () => {
|
|
93
|
+
mockRunAudit.mockRejectedValueOnce(new Error('SSH timeout'));
|
|
94
|
+
|
|
95
|
+
const result = await handleServerAudit({ server: 'test-server' });
|
|
96
|
+
|
|
97
|
+
expect(result.isError).toBe(true);
|
|
98
|
+
expect(result.content[0].text).toContain('SSH timeout');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Pattern 4: Command Test (mock core, not low-level deps)
|
|
104
|
+
|
|
105
|
+
Test commands by mocking the core module — not `sshExec`, not providers. This verifies delegation.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
jest.mock('../../src/core/backup.js');
|
|
109
|
+
import { backupServer } from '../../src/core/backup.js';
|
|
110
|
+
const mockBackupServer = backupServer as jest.MockedFunction<typeof backupServer>;
|
|
111
|
+
|
|
112
|
+
describe('backup command', () => {
|
|
113
|
+
beforeEach(() => jest.resetAllMocks());
|
|
114
|
+
|
|
115
|
+
it('delegates to backupServer core function', async () => {
|
|
116
|
+
mockBackupServer.mockResolvedValueOnce({
|
|
117
|
+
success: true,
|
|
118
|
+
backupPath: '/home/user/.kastell/backups/myserver',
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Execute command action directly (Commander action callback)
|
|
122
|
+
await backupAction({ server: 'myserver' });
|
|
123
|
+
|
|
124
|
+
expect(mockBackupServer).toHaveBeenCalledWith('myserver');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Core Function Signature Conventions
|
|
130
|
+
|
|
131
|
+
Core functions return typed result objects, never throw to the caller:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
// Pattern: return { success, data?, error?, hint? }
|
|
135
|
+
export async function exampleCore(server: string): Promise<ExampleResult> {
|
|
136
|
+
try {
|
|
137
|
+
const record = getServerRecord(server); // throws if not found
|
|
138
|
+
assertValidIp(record.ip); // throws if invalid
|
|
139
|
+
// ... business logic ...
|
|
140
|
+
return { success: true, data: result };
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return { success: false, error: String(err) };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
# Kastell Pitfalls
|
|
2
|
-
|
|
3
|
-
Known traps, symptoms, and fixes. Severity: HIGH (blocks correct behavior), MEDIUM (causes test/maintenance pain), LOW (minor inconsistency).
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## 1. Adapter Bypass (HIGH)
|
|
8
|
-
|
|
9
|
-
**Symptom:** `if (server.platform === 'coolify') { port = 8000 } else { port = 3000 }` in `src/commands/`
|
|
10
|
-
|
|
11
|
-
**Root cause:** Command accesses platform properties directly instead of using the adapter.
|
|
12
|
-
|
|
13
|
-
**Fix:** Use `getAdapter(platform)` from `factory.ts` and read `adapter.port`, `adapter.defaultLogService`, `adapter.platformPorts`.
|
|
14
|
-
|
|
15
|
-
```typescript
|
|
16
|
-
// WRONG
|
|
17
|
-
const port = server.platform === 'coolify' ? 8000 : 3000;
|
|
18
|
-
|
|
19
|
-
// CORRECT
|
|
20
|
-
import { getAdapter } from '../../src/adapters/factory.js';
|
|
21
|
-
const adapter = getAdapter(server.platform);
|
|
22
|
-
const port = adapter.port;
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## 2. Business Logic in Commands (HIGH)
|
|
28
|
-
|
|
29
|
-
**Symptom:** Complex calculations, API calls, try/catch blocks, or SSH calls in `src/commands/*.ts`.
|
|
30
|
-
|
|
31
|
-
**Root cause:** Logic was not extracted to `src/core/`.
|
|
32
|
-
|
|
33
|
-
**Fix:** Extract all logic to a new `src/core/<name>.ts` function. Command only calls core and displays the result.
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## 3. UI in Core (HIGH)
|
|
38
|
-
|
|
39
|
-
**Symptom:** `chalk.green(...)`, `ora('...').start()`, or `console.log()` in `src/core/*.ts`.
|
|
40
|
-
|
|
41
|
-
**Root cause:** Core function was handling display instead of returning data.
|
|
42
|
-
|
|
43
|
-
**Fix:** Core functions return plain data objects. The command layer (or MCP handler) handles display using chalk/ora.
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
## 4. Direct Adapter Import (MEDIUM)
|
|
48
|
-
|
|
49
|
-
**Symptom:** `import { CoolifyAdapter } from '../../src/adapters/coolify.js'` in a command or core file.
|
|
50
|
-
|
|
51
|
-
**Root cause:** Bypasses the factory cache and breaks the abstraction boundary.
|
|
52
|
-
|
|
53
|
-
**Fix:** Always use `getAdapter(platform)` from `src/adapters/factory.ts`.
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## 5. Inline Adapter Mocks in Tests (MEDIUM)
|
|
58
|
-
|
|
59
|
-
**Symptom:** `const adapter = { healthCheck: jest.fn(), port: 8000, ... }` scattered across test files.
|
|
60
|
-
|
|
61
|
-
**Root cause:** Not using the centralized mock factory.
|
|
62
|
-
|
|
63
|
-
**Fix:** Use `createMockAdapter()` from `tests/helpers/mockAdapter.ts`. When the `PlatformAdapter` interface gains new methods, only `mockAdapter.ts` needs updating.
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## 6. SSH Batch Grouping (MEDIUM)
|
|
68
|
-
|
|
69
|
-
**Symptom:** Audit or health commands call `sshExec` 4-6 times sequentially, each fetching partially overlapping data.
|
|
70
|
-
|
|
71
|
-
**Root cause:** Each check independently fetches data instead of using shared batched results.
|
|
72
|
-
|
|
73
|
-
**Fix:** Batch fast config commands together (single SSH call), batch slow probe commands together. Use head limits appropriate to the data volume (e.g., `head -50` for audit log checks).
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## 7. Jest requireActual Crash (MEDIUM)
|
|
78
|
-
|
|
79
|
-
**Symptom:** Tests crash on Node v24+ with an error related to `jest.requireActual`.
|
|
80
|
-
|
|
81
|
-
**Root cause:** `jest.requireActual` behavior changed in Node v24.
|
|
82
|
-
|
|
83
|
-
**Fix:** Use inline `jest.fn()` mocks instead of `jest.requireActual`. For module-level mocks, use `jest.mock()` with a factory function.
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## 8. Module-Level Side Effects (MEDIUM)
|
|
88
|
-
|
|
89
|
-
**Symptom:** Test imports a module that registers listeners or modifies globals at load time, causing unexpected behavior when other tests run.
|
|
90
|
-
|
|
91
|
-
**Root cause:** Module has top-level side effects (e.g., `process.on('SIGINT', ...)` at module scope).
|
|
92
|
-
|
|
93
|
-
**Fix:** Mock the module in ALL test files that import it, not just the direct test file. Side effects occur at import time.
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## 9. Hardcoded Port Numbers (LOW)
|
|
98
|
-
|
|
99
|
-
**Symptom:** `8000` or `3000` literals appear in `src/commands/` or `src/core/` files.
|
|
100
|
-
|
|
101
|
-
**Root cause:** Port copied from constants instead of read from adapter.
|
|
102
|
-
|
|
103
|
-
**Fix:** Use `adapter.port` for platform HTTP port, `adapter.platformPorts` for firewall protection list.
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## 10. PROVIDER_REGISTRY Mismatch (LOW)
|
|
108
|
-
|
|
109
|
-
**Symptom:** New provider works via direct code path but fails CLI validation, completion, or `--provider` flag parsing.
|
|
110
|
-
|
|
111
|
-
**Root cause:** Provider added to `src/providers/` but not added to `PROVIDER_REGISTRY` in `src/constants.ts`.
|
|
112
|
-
|
|
113
|
-
**Fix:** Always add to `PROVIDER_REGISTRY` first — it is the single source of truth for provider enumeration, validation, and display.
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## 11. SSH Timeout Too Short (LOW)
|
|
118
|
-
|
|
119
|
-
**Symptom:** Long-running commands (lock, audit, update) fail silently or with cryptic timeout errors.
|
|
120
|
-
|
|
121
|
-
**Root cause:** Default SSH timeout (30s) is insufficient for operations like platform update (~3 minutes).
|
|
122
|
-
|
|
123
|
-
**Fix:** Use 180s timeout for slow operations:
|
|
124
|
-
```typescript
|
|
125
|
-
await sshExec(ip, command, { timeout: 180_000 });
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## 12. describe.each + clearAllMocks (LOW)
|
|
131
|
-
|
|
132
|
-
**Symptom:** Tests pass individually but fail when the full suite runs. Mock call counts are wrong in later tests.
|
|
133
|
-
|
|
134
|
-
**Root cause:** `jest.clearAllMocks()` clears call history but does not reset mock implementations. `describe.each` reuses the same mock instance across parameterized runs.
|
|
135
|
-
|
|
136
|
-
**Fix:** Use `jest.resetAllMocks()` in `beforeEach` when using `describe.each`. This resets both call history and implementations.
|
|
1
|
+
# Kastell Pitfalls
|
|
2
|
+
|
|
3
|
+
Known traps, symptoms, and fixes. Severity: HIGH (blocks correct behavior), MEDIUM (causes test/maintenance pain), LOW (minor inconsistency).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Adapter Bypass (HIGH)
|
|
8
|
+
|
|
9
|
+
**Symptom:** `if (server.platform === 'coolify') { port = 8000 } else { port = 3000 }` in `src/commands/`
|
|
10
|
+
|
|
11
|
+
**Root cause:** Command accesses platform properties directly instead of using the adapter.
|
|
12
|
+
|
|
13
|
+
**Fix:** Use `getAdapter(platform)` from `factory.ts` and read `adapter.port`, `adapter.defaultLogService`, `adapter.platformPorts`.
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
// WRONG
|
|
17
|
+
const port = server.platform === 'coolify' ? 8000 : 3000;
|
|
18
|
+
|
|
19
|
+
// CORRECT
|
|
20
|
+
import { getAdapter } from '../../src/adapters/factory.js';
|
|
21
|
+
const adapter = getAdapter(server.platform);
|
|
22
|
+
const port = adapter.port;
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 2. Business Logic in Commands (HIGH)
|
|
28
|
+
|
|
29
|
+
**Symptom:** Complex calculations, API calls, try/catch blocks, or SSH calls in `src/commands/*.ts`.
|
|
30
|
+
|
|
31
|
+
**Root cause:** Logic was not extracted to `src/core/`.
|
|
32
|
+
|
|
33
|
+
**Fix:** Extract all logic to a new `src/core/<name>.ts` function. Command only calls core and displays the result.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 3. UI in Core (HIGH)
|
|
38
|
+
|
|
39
|
+
**Symptom:** `chalk.green(...)`, `ora('...').start()`, or `console.log()` in `src/core/*.ts`.
|
|
40
|
+
|
|
41
|
+
**Root cause:** Core function was handling display instead of returning data.
|
|
42
|
+
|
|
43
|
+
**Fix:** Core functions return plain data objects. The command layer (or MCP handler) handles display using chalk/ora.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## 4. Direct Adapter Import (MEDIUM)
|
|
48
|
+
|
|
49
|
+
**Symptom:** `import { CoolifyAdapter } from '../../src/adapters/coolify.js'` in a command or core file.
|
|
50
|
+
|
|
51
|
+
**Root cause:** Bypasses the factory cache and breaks the abstraction boundary.
|
|
52
|
+
|
|
53
|
+
**Fix:** Always use `getAdapter(platform)` from `src/adapters/factory.ts`.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 5. Inline Adapter Mocks in Tests (MEDIUM)
|
|
58
|
+
|
|
59
|
+
**Symptom:** `const adapter = { healthCheck: jest.fn(), port: 8000, ... }` scattered across test files.
|
|
60
|
+
|
|
61
|
+
**Root cause:** Not using the centralized mock factory.
|
|
62
|
+
|
|
63
|
+
**Fix:** Use `createMockAdapter()` from `tests/helpers/mockAdapter.ts`. When the `PlatformAdapter` interface gains new methods, only `mockAdapter.ts` needs updating.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## 6. SSH Batch Grouping (MEDIUM)
|
|
68
|
+
|
|
69
|
+
**Symptom:** Audit or health commands call `sshExec` 4-6 times sequentially, each fetching partially overlapping data.
|
|
70
|
+
|
|
71
|
+
**Root cause:** Each check independently fetches data instead of using shared batched results.
|
|
72
|
+
|
|
73
|
+
**Fix:** Batch fast config commands together (single SSH call), batch slow probe commands together. Use head limits appropriate to the data volume (e.g., `head -50` for audit log checks).
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 7. Jest requireActual Crash (MEDIUM)
|
|
78
|
+
|
|
79
|
+
**Symptom:** Tests crash on Node v24+ with an error related to `jest.requireActual`.
|
|
80
|
+
|
|
81
|
+
**Root cause:** `jest.requireActual` behavior changed in Node v24.
|
|
82
|
+
|
|
83
|
+
**Fix:** Use inline `jest.fn()` mocks instead of `jest.requireActual`. For module-level mocks, use `jest.mock()` with a factory function.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## 8. Module-Level Side Effects (MEDIUM)
|
|
88
|
+
|
|
89
|
+
**Symptom:** Test imports a module that registers listeners or modifies globals at load time, causing unexpected behavior when other tests run.
|
|
90
|
+
|
|
91
|
+
**Root cause:** Module has top-level side effects (e.g., `process.on('SIGINT', ...)` at module scope).
|
|
92
|
+
|
|
93
|
+
**Fix:** Mock the module in ALL test files that import it, not just the direct test file. Side effects occur at import time.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 9. Hardcoded Port Numbers (LOW)
|
|
98
|
+
|
|
99
|
+
**Symptom:** `8000` or `3000` literals appear in `src/commands/` or `src/core/` files.
|
|
100
|
+
|
|
101
|
+
**Root cause:** Port copied from constants instead of read from adapter.
|
|
102
|
+
|
|
103
|
+
**Fix:** Use `adapter.port` for platform HTTP port, `adapter.platformPorts` for firewall protection list.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 10. PROVIDER_REGISTRY Mismatch (LOW)
|
|
108
|
+
|
|
109
|
+
**Symptom:** New provider works via direct code path but fails CLI validation, completion, or `--provider` flag parsing.
|
|
110
|
+
|
|
111
|
+
**Root cause:** Provider added to `src/providers/` but not added to `PROVIDER_REGISTRY` in `src/constants.ts`.
|
|
112
|
+
|
|
113
|
+
**Fix:** Always add to `PROVIDER_REGISTRY` first — it is the single source of truth for provider enumeration, validation, and display.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## 11. SSH Timeout Too Short (LOW)
|
|
118
|
+
|
|
119
|
+
**Symptom:** Long-running commands (lock, audit, update) fail silently or with cryptic timeout errors.
|
|
120
|
+
|
|
121
|
+
**Root cause:** Default SSH timeout (30s) is insufficient for operations like platform update (~3 minutes).
|
|
122
|
+
|
|
123
|
+
**Fix:** Use 180s timeout for slow operations:
|
|
124
|
+
```typescript
|
|
125
|
+
await sshExec(ip, command, { timeout: 180_000 });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## 12. describe.each + clearAllMocks (LOW)
|
|
131
|
+
|
|
132
|
+
**Symptom:** Tests pass individually but fail when the full suite runs. Mock call counts are wrong in later tests.
|
|
133
|
+
|
|
134
|
+
**Root cause:** `jest.clearAllMocks()` clears call history but does not reset mock implementations. `describe.each` reuses the same mock instance across parameterized runs.
|
|
135
|
+
|
|
136
|
+
**Fix:** Use `jest.resetAllMocks()` in `beforeEach` when using `describe.each`. This resets both call history and implementations.
|