tlc-claude-code 1.6.4 → 1.7.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.
Files changed (81) hide show
  1. package/dashboard/dist/components/ContainerSecurityPane.d.ts +45 -0
  2. package/dashboard/dist/components/ContainerSecurityPane.js +44 -0
  3. package/dashboard/dist/components/ContainerSecurityPane.test.d.ts +1 -0
  4. package/dashboard/dist/components/ContainerSecurityPane.test.js +153 -0
  5. package/package.json +1 -1
  6. package/server/lib/access-control.test.js +1 -1
  7. package/server/lib/agents-cancel-command.test.js +1 -1
  8. package/server/lib/agents-get-command.test.js +1 -1
  9. package/server/lib/agents-list-command.test.js +1 -1
  10. package/server/lib/agents-logs-command.test.js +1 -1
  11. package/server/lib/agents-retry-command.test.js +1 -1
  12. package/server/lib/budget-limits.test.js +2 -2
  13. package/server/lib/code-gate/bypass-logger.js +129 -0
  14. package/server/lib/code-gate/bypass-logger.test.js +142 -0
  15. package/server/lib/code-gate/gate-command.js +114 -0
  16. package/server/lib/code-gate/gate-command.test.js +111 -0
  17. package/server/lib/code-gate/gate-config.js +163 -0
  18. package/server/lib/code-gate/gate-config.test.js +181 -0
  19. package/server/lib/code-gate/gate-engine.js +193 -0
  20. package/server/lib/code-gate/gate-engine.test.js +258 -0
  21. package/server/lib/code-gate/gate-reporter.js +123 -0
  22. package/server/lib/code-gate/gate-reporter.test.js +159 -0
  23. package/server/lib/code-gate/hooks-generator.js +149 -0
  24. package/server/lib/code-gate/hooks-generator.test.js +142 -0
  25. package/server/lib/code-gate/llm-reviewer.js +176 -0
  26. package/server/lib/code-gate/llm-reviewer.test.js +161 -0
  27. package/server/lib/code-gate/push-gate.js +133 -0
  28. package/server/lib/code-gate/push-gate.test.js +190 -0
  29. package/server/lib/code-gate/rules/architecture-rules.js +228 -0
  30. package/server/lib/code-gate/rules/architecture-rules.test.js +155 -0
  31. package/server/lib/code-gate/rules/client-rules.js +120 -0
  32. package/server/lib/code-gate/rules/client-rules.test.js +121 -0
  33. package/server/lib/code-gate/rules/config-rules.js +140 -0
  34. package/server/lib/code-gate/rules/config-rules.test.js +103 -0
  35. package/server/lib/code-gate/rules/database-rules.js +158 -0
  36. package/server/lib/code-gate/rules/database-rules.test.js +119 -0
  37. package/server/lib/code-gate/rules/docker-rules.js +201 -0
  38. package/server/lib/code-gate/rules/docker-rules.test.js +104 -0
  39. package/server/lib/code-gate/rules/quality-rules.js +304 -0
  40. package/server/lib/code-gate/rules/quality-rules.test.js +199 -0
  41. package/server/lib/code-gate/rules/security-rules.js +228 -0
  42. package/server/lib/code-gate/rules/security-rules.test.js +131 -0
  43. package/server/lib/code-gate/rules/structure-rules.js +155 -0
  44. package/server/lib/code-gate/rules/structure-rules.test.js +107 -0
  45. package/server/lib/code-gate/rules/test-rules.js +93 -0
  46. package/server/lib/code-gate/rules/test-rules.test.js +97 -0
  47. package/server/lib/code-gate/typescript-gate.js +128 -0
  48. package/server/lib/code-gate/typescript-gate.test.js +131 -0
  49. package/server/lib/code-generator.test.js +1 -1
  50. package/server/lib/cost-command.test.js +1 -1
  51. package/server/lib/cost-optimizer.test.js +1 -1
  52. package/server/lib/cost-projections.test.js +1 -1
  53. package/server/lib/cost-reports.test.js +1 -1
  54. package/server/lib/cost-tracker.test.js +1 -1
  55. package/server/lib/crypto-patterns.test.js +1 -1
  56. package/server/lib/design-command.test.js +1 -1
  57. package/server/lib/design-parser.test.js +1 -1
  58. package/server/lib/gemini-vision.test.js +1 -1
  59. package/server/lib/input-validator.test.js +1 -1
  60. package/server/lib/litellm-client.test.js +1 -1
  61. package/server/lib/litellm-command.test.js +1 -1
  62. package/server/lib/litellm-config.test.js +1 -1
  63. package/server/lib/model-pricing.test.js +1 -1
  64. package/server/lib/models-command.test.js +1 -1
  65. package/server/lib/optimize-command.test.js +1 -1
  66. package/server/lib/orchestration-integration.test.js +1 -1
  67. package/server/lib/output-encoder.test.js +1 -1
  68. package/server/lib/quality-evaluator.test.js +1 -1
  69. package/server/lib/quality-gate-command.test.js +1 -1
  70. package/server/lib/quality-gate-scorer.test.js +1 -1
  71. package/server/lib/quality-history.test.js +1 -1
  72. package/server/lib/quality-presets.test.js +1 -1
  73. package/server/lib/quality-retry.test.js +1 -1
  74. package/server/lib/quality-thresholds.test.js +1 -1
  75. package/server/lib/secure-auth.test.js +1 -1
  76. package/server/lib/secure-code-command.test.js +1 -1
  77. package/server/lib/secure-errors.test.js +1 -1
  78. package/server/lib/security/auth-security.test.js +4 -3
  79. package/server/lib/vision-command.test.js +1 -1
  80. package/server/lib/visual-command.test.js +1 -1
  81. package/server/lib/visual-testing.test.js +1 -1
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Database Rules
3
+ *
4
+ * Detects new Date() in ORM .set() blocks (should use sql`now()`)
5
+ * and inline billing math that should use shared calculation utilities.
6
+ *
7
+ * Derived from production bugs: timestamp drift, copy-paste billing errors.
8
+ * See: WALL_OF_SHAME.md Bug #29 (missing VAT), Lesson #3 (timestamps)
9
+ *
10
+ * @module code-gate/rules/database-rules
11
+ */
12
+
13
+ /**
14
+ * @param {string} filePath
15
+ * @returns {boolean}
16
+ */
17
+ function isTestFile(filePath) {
18
+ return /\.(test|spec)\.[jt]sx?$/.test(filePath) || filePath.includes('__tests__');
19
+ }
20
+
21
+ /**
22
+ * Detect new Date() inside ORM .set({}) blocks.
23
+ * Database timestamps should use sql`now()` for consistent DB-server time.
24
+ * new Date() uses app-server time which can drift.
25
+ *
26
+ * @param {string} filePath
27
+ * @param {string} content
28
+ * @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
29
+ */
30
+ function checkNewDateInSet(filePath, content) {
31
+ if (isTestFile(filePath)) return [];
32
+ const findings = [];
33
+ const lines = content.split('\n');
34
+
35
+ let inSetBlock = false;
36
+ let braceDepth = 0;
37
+
38
+ for (let i = 0; i < lines.length; i++) {
39
+ const line = lines[i];
40
+
41
+ // Detect .set({ or .set( { start
42
+ if (/\.set\s*\(\s*\{/.test(line)) {
43
+ inSetBlock = true;
44
+ braceDepth = 0;
45
+ // Count braces on this line
46
+ for (const ch of line) {
47
+ if (ch === '{') braceDepth++;
48
+ if (ch === '}') braceDepth--;
49
+ }
50
+ if (braceDepth <= 0) {
51
+ // Single-line .set({...}) — check this line
52
+ if (/new\s+Date\s*\(\s*\)/.test(line)) {
53
+ findings.push({
54
+ severity: 'block',
55
+ rule: 'no-new-date-in-set',
56
+ line: i + 1,
57
+ message: 'new Date() in .set() block — use sql`now()` for consistent DB timestamps',
58
+ fix: 'Replace new Date() with sql`now()` for database-server time consistency',
59
+ });
60
+ }
61
+ inSetBlock = false;
62
+ continue;
63
+ }
64
+ // Check the opening line too
65
+ if (/new\s+Date\s*\(\s*\)/.test(line)) {
66
+ findings.push({
67
+ severity: 'block',
68
+ rule: 'no-new-date-in-set',
69
+ line: i + 1,
70
+ message: 'new Date() in .set() block — use sql`now()` for consistent DB timestamps',
71
+ fix: 'Replace new Date() with sql`now()` for database-server time consistency',
72
+ });
73
+ }
74
+ continue;
75
+ }
76
+
77
+ if (inSetBlock) {
78
+ for (const ch of line) {
79
+ if (ch === '{') braceDepth++;
80
+ if (ch === '}') braceDepth--;
81
+ }
82
+
83
+ if (/new\s+Date\s*\(\s*\)/.test(line)) {
84
+ findings.push({
85
+ severity: 'block',
86
+ rule: 'no-new-date-in-set',
87
+ line: i + 1,
88
+ message: 'new Date() in .set() block — use sql`now()` for consistent DB timestamps',
89
+ fix: 'Replace new Date() with sql`now()` for database-server time consistency',
90
+ });
91
+ }
92
+
93
+ if (braceDepth <= 0) {
94
+ inSetBlock = false;
95
+ }
96
+ }
97
+ }
98
+
99
+ return findings;
100
+ }
101
+
102
+ /** Billing-related variable names that indicate inline math */
103
+ const BILLING_VARS = [
104
+ 'quantity', 'rate', 'price', 'cost', 'amount',
105
+ 'subtotal', 'discount', 'tax', 'vat', 'total',
106
+ 'lineTotal', 'grandTotal', 'unitPrice',
107
+ ];
108
+
109
+ /**
110
+ * Detect inline billing math that should use shared calculation utilities.
111
+ * When billing calculations are scattered across files, bugs like missing VAT
112
+ * or wrong discount logic slip through.
113
+ *
114
+ * @param {string} filePath
115
+ * @param {string} content
116
+ * @returns {Array}
117
+ */
118
+ function checkInlineBillingMath(filePath, content) {
119
+ if (isTestFile(filePath)) return [];
120
+
121
+ // Allow math in calculation utility files
122
+ const fileBase = filePath.toLowerCase();
123
+ if (fileBase.includes('calculation') || fileBase.includes('calc.') ||
124
+ fileBase.includes('calc/') || fileBase.includes('-calc')) {
125
+ return [];
126
+ }
127
+
128
+ const findings = [];
129
+ const lines = content.split('\n');
130
+
131
+ for (let i = 0; i < lines.length; i++) {
132
+ const line = lines[i];
133
+ const trimmed = line.trim();
134
+ if (trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
135
+
136
+ // Check for billing-variable arithmetic: quantity * rate, subtotal - discount, etc.
137
+ for (const varName of BILLING_VARS) {
138
+ const pattern = new RegExp(`\\b${varName}\\b\\s*[*+\\-]\\s*\\b(${BILLING_VARS.join('|')})\\b`);
139
+ if (pattern.test(line)) {
140
+ findings.push({
141
+ severity: 'warn',
142
+ rule: 'no-inline-billing-math',
143
+ line: i + 1,
144
+ message: 'Inline billing calculation — use shared calculation utility',
145
+ fix: 'Use calculateLineTotal(), calculateSubtotal(), or calculateGrandTotal() from billing-calculations module',
146
+ });
147
+ break; // One finding per line
148
+ }
149
+ }
150
+ }
151
+
152
+ return findings;
153
+ }
154
+
155
+ module.exports = {
156
+ checkNewDateInSet,
157
+ checkInlineBillingMath,
158
+ };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Database Rules Tests
3
+ *
4
+ * Detects new Date() in ORM .set() blocks and inline billing math.
5
+ */
6
+ import { describe, it, expect } from 'vitest';
7
+
8
+ const {
9
+ checkNewDateInSet,
10
+ checkInlineBillingMath,
11
+ } = require('./database-rules.js');
12
+
13
+ describe('Database Rules', () => {
14
+ describe('checkNewDateInSet', () => {
15
+ it('detects new Date() in .set({}) block', () => {
16
+ const code = `
17
+ db.update(leads).set({
18
+ status: "active",
19
+ updatedAt: new Date(),
20
+ });
21
+ `;
22
+ const findings = checkNewDateInSet('src/leads/leads.service.ts', code);
23
+ expect(findings).toHaveLength(1);
24
+ expect(findings[0].severity).toBe('block');
25
+ expect(findings[0].rule).toBe('no-new-date-in-set');
26
+ expect(findings[0].fix).toContain('sql');
27
+ });
28
+
29
+ it('detects new Date() in .set() with convertedAt', () => {
30
+ const code = `
31
+ db.update(leads).set({
32
+ convertedAt: new Date(),
33
+ updatedAt: new Date(),
34
+ });
35
+ `;
36
+ const findings = checkNewDateInSet('src/leads/leads.service.ts', code);
37
+ expect(findings.length).toBeGreaterThanOrEqual(1);
38
+ });
39
+
40
+ it('allows new Date() in logging context', () => {
41
+ const code = `
42
+ console.log('Started at', new Date());
43
+ const timestamp = new Date();
44
+ `;
45
+ const findings = checkNewDateInSet('src/utils/logger.ts', code);
46
+ expect(findings).toHaveLength(0);
47
+ });
48
+
49
+ it('allows sql`now()` in .set()', () => {
50
+ const code = `
51
+ db.update(leads).set({
52
+ status: "active",
53
+ updatedAt: sql\`now()\`,
54
+ });
55
+ `;
56
+ const findings = checkNewDateInSet('src/leads/leads.service.ts', code);
57
+ expect(findings).toHaveLength(0);
58
+ });
59
+
60
+ it('skips test files', () => {
61
+ const code = 'db.update(leads).set({ updatedAt: new Date() });';
62
+ const findings = checkNewDateInSet('src/leads/leads.test.ts', code);
63
+ expect(findings).toHaveLength(0);
64
+ });
65
+
66
+ it('detects across multiline .set() call', () => {
67
+ const code = [
68
+ 'await db',
69
+ ' .update(invoices)',
70
+ ' .set({',
71
+ ' paidAt: new Date(),',
72
+ ' status: "paid",',
73
+ ' });',
74
+ ].join('\n');
75
+ const findings = checkNewDateInSet('src/invoices/invoices.service.ts', code);
76
+ expect(findings).toHaveLength(1);
77
+ });
78
+ });
79
+
80
+ describe('checkInlineBillingMath', () => {
81
+ it('detects quantity * rate pattern', () => {
82
+ const code = 'const lineTotal = quantity * rate;';
83
+ const findings = checkInlineBillingMath('src/invoices/editor.tsx', code);
84
+ expect(findings).toHaveLength(1);
85
+ expect(findings[0].severity).toBe('warn');
86
+ expect(findings[0].rule).toBe('no-inline-billing-math');
87
+ });
88
+
89
+ it('detects subtotal - discount pattern', () => {
90
+ const code = 'const total = subtotal - discount + tax;';
91
+ const findings = checkInlineBillingMath('src/invoices/sheet.tsx', code);
92
+ expect(findings).toHaveLength(1);
93
+ });
94
+
95
+ it('allows math in calculation utility files', () => {
96
+ const code = 'const lineTotal = quantity * rate;';
97
+ const findings = checkInlineBillingMath('src/lib/billing-calculations.ts', code);
98
+ expect(findings).toHaveLength(0);
99
+ });
100
+
101
+ it('allows math in files with "calc" in name', () => {
102
+ const code = 'const total = subtotal - discount;';
103
+ const findings = checkInlineBillingMath('src/utils/price-calc.ts', code);
104
+ expect(findings).toHaveLength(0);
105
+ });
106
+
107
+ it('skips test files', () => {
108
+ const code = 'const lineTotal = quantity * rate;';
109
+ const findings = checkInlineBillingMath('src/invoices/editor.test.tsx', code);
110
+ expect(findings).toHaveLength(0);
111
+ });
112
+
113
+ it('allows non-billing arithmetic', () => {
114
+ const code = 'const area = width * height;';
115
+ const findings = checkInlineBillingMath('src/utils/geometry.ts', code);
116
+ expect(findings).toHaveLength(0);
117
+ });
118
+ });
119
+ });
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Docker Rules
3
+ *
4
+ * Detects dangerous Docker patterns that can cause data loss:
5
+ * external volumes, missing volume names, destructive commands.
6
+ *
7
+ * Derived from Bug #27 (AI wiped production database via docker volume).
8
+ * See: TLC-BEST-PRACTICES.md Section 7 (Docker & Infrastructure)
9
+ *
10
+ * @module code-gate/rules/docker-rules
11
+ */
12
+
13
+ /**
14
+ * File types where docker-compose rules apply.
15
+ * @param {string} filePath
16
+ * @returns {boolean}
17
+ */
18
+ function isDockerComposeFile(filePath) {
19
+ const base = filePath.toLowerCase();
20
+ return base.includes('docker-compose') || base.includes('compose.y');
21
+ }
22
+
23
+ /**
24
+ * File types where shell command rules apply.
25
+ * @param {string} filePath
26
+ * @returns {boolean}
27
+ */
28
+ function isShellOrCIFile(filePath) {
29
+ const base = filePath.toLowerCase();
30
+ return base.endsWith('.sh') || base.endsWith('.bash') ||
31
+ base.includes('.github/') || base.includes('.gitlab-ci') ||
32
+ base.includes('makefile') || base.includes('Makefile') ||
33
+ base.endsWith('.yml') || base.endsWith('.yaml');
34
+ }
35
+
36
+ /**
37
+ * Detect `external: true` in docker-compose volume definitions.
38
+ * External volumes create fragile cross-project dependencies
39
+ * and can silently reference wrong data.
40
+ *
41
+ * @param {string} filePath
42
+ * @param {string} content
43
+ * @returns {Array<{severity: string, rule: string, line: number, message: string, fix: string}>}
44
+ */
45
+ function checkExternalVolumes(filePath, content) {
46
+ if (!isDockerComposeFile(filePath)) return [];
47
+ const findings = [];
48
+ const lines = content.split('\n');
49
+
50
+ for (let i = 0; i < lines.length; i++) {
51
+ const line = lines[i];
52
+ if (/^\s*external\s*:\s*true/.test(line)) {
53
+ findings.push({
54
+ severity: 'block',
55
+ rule: 'no-external-volumes',
56
+ line: i + 1,
57
+ message: 'Docker volume with external: true — creates fragile cross-project dependency',
58
+ fix: 'Remove external: true and use explicit name: property instead',
59
+ });
60
+ }
61
+ }
62
+
63
+ return findings;
64
+ }
65
+
66
+ /**
67
+ * Detect docker-compose volumes without explicit `name:` property.
68
+ * Without explicit names, Docker generates project-prefixed names
69
+ * that can collide or be accidentally deleted.
70
+ *
71
+ * @param {string} filePath
72
+ * @param {string} content
73
+ * @returns {Array}
74
+ */
75
+ function checkMissingVolumeNames(filePath, content) {
76
+ if (!isDockerComposeFile(filePath)) return [];
77
+ const findings = [];
78
+ const lines = content.split('\n');
79
+
80
+ let inVolumesSection = false;
81
+ let currentVolume = null;
82
+ let currentVolumeHasName = false;
83
+ let currentVolumeLine = -1;
84
+
85
+ for (let i = 0; i < lines.length; i++) {
86
+ const line = lines[i];
87
+
88
+ // Detect top-level volumes: section
89
+ if (/^volumes\s*:/.test(line)) {
90
+ inVolumesSection = true;
91
+ continue;
92
+ }
93
+
94
+ // Exit volumes section on next top-level key
95
+ if (inVolumesSection && /^\S/.test(line) && !/^\s/.test(line) && !line.startsWith('#')) {
96
+ // Flush last volume
97
+ if (currentVolume && !currentVolumeHasName) {
98
+ findings.push({
99
+ severity: 'warn',
100
+ rule: 'require-volume-names',
101
+ line: currentVolumeLine + 1,
102
+ message: `Volume '${currentVolume}' has no explicit name: — data may be lost on project rename`,
103
+ fix: `Add name: property: name: ${currentVolume}`,
104
+ });
105
+ }
106
+ inVolumesSection = false;
107
+ currentVolume = null;
108
+ continue;
109
+ }
110
+
111
+ if (inVolumesSection) {
112
+ // Volume key (indented once, ends with colon)
113
+ const volumeKey = line.match(/^\s{2}(\w[\w-]*)\s*:/);
114
+ if (volumeKey) {
115
+ // Flush previous volume
116
+ if (currentVolume && !currentVolumeHasName) {
117
+ findings.push({
118
+ severity: 'warn',
119
+ rule: 'require-volume-names',
120
+ line: currentVolumeLine + 1,
121
+ message: `Volume '${currentVolume}' has no explicit name: — data may be lost on project rename`,
122
+ fix: `Add name: property: name: ${currentVolume}`,
123
+ });
124
+ }
125
+ currentVolume = volumeKey[1];
126
+ currentVolumeLine = i;
127
+ currentVolumeHasName = false;
128
+ }
129
+
130
+ // Check for name: property (indented further)
131
+ if (currentVolume && /^\s+name\s*:/.test(line)) {
132
+ currentVolumeHasName = true;
133
+ }
134
+ }
135
+ }
136
+
137
+ // Flush final volume
138
+ if (inVolumesSection && currentVolume && !currentVolumeHasName) {
139
+ findings.push({
140
+ severity: 'warn',
141
+ rule: 'require-volume-names',
142
+ line: currentVolumeLine + 1,
143
+ message: `Volume '${currentVolume}' has no explicit name: — data may be lost on project rename`,
144
+ fix: `Add name: property: name: ${currentVolume}`,
145
+ });
146
+ }
147
+
148
+ return findings;
149
+ }
150
+
151
+ /**
152
+ * Detect dangerous Docker commands in scripts and CI files.
153
+ * `docker compose down -v` removes data volumes.
154
+ * `docker volume rm` deletes volumes directly.
155
+ *
156
+ * @param {string} filePath
157
+ * @param {string} content
158
+ * @returns {Array}
159
+ */
160
+ function checkDangerousDockerCommands(filePath, content) {
161
+ if (!isShellOrCIFile(filePath)) return [];
162
+ const findings = [];
163
+ const lines = content.split('\n');
164
+
165
+ for (let i = 0; i < lines.length; i++) {
166
+ const line = lines[i];
167
+ const trimmed = line.trim();
168
+ if (trimmed.startsWith('#')) continue;
169
+
170
+ // docker compose down -v (or docker-compose down -v)
171
+ if (/docker[\s-]compose\s+down\s+.*-v/.test(line) ||
172
+ /docker[\s-]compose\s+down\s+-v/.test(line)) {
173
+ findings.push({
174
+ severity: 'block',
175
+ rule: 'no-dangerous-docker',
176
+ line: i + 1,
177
+ message: 'docker compose down -v removes data volumes — potential data loss',
178
+ fix: 'Use docker compose down (without -v) to preserve data volumes',
179
+ });
180
+ }
181
+
182
+ // docker volume rm
183
+ if (/docker\s+volume\s+rm\b/.test(line)) {
184
+ findings.push({
185
+ severity: 'block',
186
+ rule: 'no-dangerous-docker',
187
+ line: i + 1,
188
+ message: 'docker volume rm — irreversible data deletion',
189
+ fix: 'Verify this is intentional. Use docker volume inspect first to check contents',
190
+ });
191
+ }
192
+ }
193
+
194
+ return findings;
195
+ }
196
+
197
+ module.exports = {
198
+ checkExternalVolumes,
199
+ checkMissingVolumeNames,
200
+ checkDangerousDockerCommands,
201
+ };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Docker Rules Tests
3
+ *
4
+ * Detects dangerous Docker patterns: external volumes,
5
+ * missing volume names, and destructive commands.
6
+ */
7
+ import { describe, it, expect } from 'vitest';
8
+
9
+ const {
10
+ checkExternalVolumes,
11
+ checkMissingVolumeNames,
12
+ checkDangerousDockerCommands,
13
+ } = require('./docker-rules.js');
14
+
15
+ describe('Docker Rules', () => {
16
+ describe('checkExternalVolumes', () => {
17
+ it('detects external: true in docker-compose', () => {
18
+ const yaml = `
19
+ volumes:
20
+ postgres_data:
21
+ external: true
22
+ `;
23
+ const findings = checkExternalVolumes('docker-compose.yml', yaml);
24
+ expect(findings).toHaveLength(1);
25
+ expect(findings[0].severity).toBe('block');
26
+ expect(findings[0].rule).toBe('no-external-volumes');
27
+ });
28
+
29
+ it('passes without external flag', () => {
30
+ const yaml = `
31
+ volumes:
32
+ postgres_data:
33
+ name: myapp_postgres_data
34
+ `;
35
+ const findings = checkExternalVolumes('docker-compose.yml', yaml);
36
+ expect(findings).toHaveLength(0);
37
+ });
38
+
39
+ it('only checks docker-compose files', () => {
40
+ const yaml = 'external: true';
41
+ const findings = checkExternalVolumes('src/config.yml', yaml);
42
+ expect(findings).toHaveLength(0);
43
+ });
44
+ });
45
+
46
+ describe('checkMissingVolumeNames', () => {
47
+ it('detects volumes without name property', () => {
48
+ const yaml = `
49
+ volumes:
50
+ postgres_data:
51
+ driver: local
52
+ redis_data:
53
+ `;
54
+ const findings = checkMissingVolumeNames('docker-compose.yml', yaml);
55
+ expect(findings).toHaveLength(2); // both volumes lack name:
56
+ expect(findings[0].severity).toBe('warn');
57
+ expect(findings[0].rule).toBe('require-volume-names');
58
+ });
59
+
60
+ it('passes volume with explicit name', () => {
61
+ const yaml = `
62
+ volumes:
63
+ postgres_data:
64
+ name: myapp_postgres_data
65
+ `;
66
+ const findings = checkMissingVolumeNames('docker-compose.yml', yaml);
67
+ expect(findings).toHaveLength(0);
68
+ });
69
+
70
+ it('only checks docker-compose files', () => {
71
+ const yaml = 'volumes:\n data:\n driver: local';
72
+ const findings = checkMissingVolumeNames('src/config.ts', yaml);
73
+ expect(findings).toHaveLength(0);
74
+ });
75
+ });
76
+
77
+ describe('checkDangerousDockerCommands', () => {
78
+ it('detects docker compose down -v', () => {
79
+ const script = '#!/bin/bash\ndocker compose down -v\necho "Done"';
80
+ const findings = checkDangerousDockerCommands('scripts/reset.sh', script);
81
+ expect(findings).toHaveLength(1);
82
+ expect(findings[0].severity).toBe('block');
83
+ expect(findings[0].rule).toBe('no-dangerous-docker');
84
+ });
85
+
86
+ it('detects docker volume rm', () => {
87
+ const script = 'docker volume rm myapp_postgres_data';
88
+ const findings = checkDangerousDockerCommands('scripts/cleanup.sh', script);
89
+ expect(findings).toHaveLength(1);
90
+ });
91
+
92
+ it('allows docker compose down without -v', () => {
93
+ const script = 'docker compose down\necho "Stopped"';
94
+ const findings = checkDangerousDockerCommands('scripts/stop.sh', script);
95
+ expect(findings).toHaveLength(0);
96
+ });
97
+
98
+ it('checks shell scripts and CI files', () => {
99
+ const script = 'docker compose down -v';
100
+ const findings = checkDangerousDockerCommands('.github/workflows/ci.yml', script);
101
+ expect(findings).toHaveLength(1);
102
+ });
103
+ });
104
+ });