generate a compose file from any container - save as new or append to existing stack
This commit is contained in:
12 files changed
+1939
-4
No files matched your search
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Unit tests for the "Append to existing" merge helpers (#489): merging a generated
|
||||
* single-service compose into an existing stack compose, and computing the highlight range
|
||||
* of the added service. No browser needed - pure js-yaml logic.
|
||||
*
|
||||
* Run with: bun test tests/compose-merge.test.ts
|
||||
*/
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import yaml from 'js-yaml';
|
||||
import { mergeServiceIntoCompose, computeAddedRange } from '../src/lib/utils/compose-merge';
|
||||
|
||||
const BASE = `services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- '8080:80'
|
||||
`;
|
||||
|
||||
describe('mergeServiceIntoCompose', () => {
|
||||
test('adds a new service under services without touching the existing one', () => {
|
||||
const service = `services:
|
||||
redis:
|
||||
image: redis:7
|
||||
`;
|
||||
const { merged, key, renamed } = mergeServiceIntoCompose(BASE, service);
|
||||
expect(key).toBe('redis');
|
||||
expect(renamed).toBe(false);
|
||||
const doc = yaml.load(merged) as any;
|
||||
expect(Object.keys(doc.services).sort()).toEqual(['redis', 'web']);
|
||||
expect(doc.services.web.image).toBe('nginx:latest');
|
||||
expect(doc.services.redis.image).toBe('redis:7');
|
||||
});
|
||||
|
||||
test('auto-suffixes on a name clash instead of overwriting', () => {
|
||||
const service = `services:
|
||||
web:
|
||||
image: caddy:2
|
||||
`;
|
||||
const { merged, key, renamed } = mergeServiceIntoCompose(BASE, service);
|
||||
expect(key).toBe('web-2');
|
||||
expect(renamed).toBe(true);
|
||||
const doc = yaml.load(merged) as any;
|
||||
// original web is untouched, the added one is web-2
|
||||
expect(doc.services.web.image).toBe('nginx:latest');
|
||||
expect(doc.services['web-2'].image).toBe('caddy:2');
|
||||
});
|
||||
|
||||
test('picks the lowest free suffix when -2 is also taken', () => {
|
||||
const base = `services:
|
||||
web:
|
||||
image: nginx:latest
|
||||
web-2:
|
||||
image: nginx:1.25
|
||||
`;
|
||||
const service = `services:
|
||||
web:
|
||||
image: caddy:2
|
||||
`;
|
||||
const { key } = mergeServiceIntoCompose(base, service);
|
||||
expect(key).toBe('web-3');
|
||||
});
|
||||
|
||||
test('carries over top-level networks and volumes the service references', () => {
|
||||
const service = `services:
|
||||
db:
|
||||
image: postgres:16
|
||||
networks:
|
||||
- backend
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
backend:
|
||||
external: true
|
||||
volumes:
|
||||
pgdata:
|
||||
external: true
|
||||
`;
|
||||
const { merged } = mergeServiceIntoCompose(BASE, service);
|
||||
const doc = yaml.load(merged) as any;
|
||||
expect(doc.networks.backend).toEqual({ external: true });
|
||||
expect(doc.volumes.pgdata).toEqual({ external: true });
|
||||
});
|
||||
|
||||
test('handles an empty base compose (creates services)', () => {
|
||||
const service = `services:
|
||||
app:
|
||||
image: alpine
|
||||
`;
|
||||
const { merged, key } = mergeServiceIntoCompose('', service);
|
||||
expect(key).toBe('app');
|
||||
expect((yaml.load(merged) as any).services.app.image).toBe('alpine');
|
||||
});
|
||||
});
|
||||
|
||||
describe('computeAddedRange', () => {
|
||||
test('returns the exact block of the added service', () => {
|
||||
const service = `services:
|
||||
redis:
|
||||
image: redis:7
|
||||
`;
|
||||
const { merged, key } = mergeServiceIntoCompose(BASE, service);
|
||||
const [range] = computeAddedRange(merged, key);
|
||||
const lines = merged.split('\n');
|
||||
// the highlighted range must start at the ` redis:` line
|
||||
expect(lines[range.line - 1]).toMatch(/^\s+redis:/);
|
||||
// and every highlighted line must be inside the redis block (indent > 2) or the header
|
||||
for (let l = range.line; l <= range.endLine; l++) {
|
||||
expect(lines[l - 1].startsWith(' ')).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('does NOT over-highlight the trailing blank line when the service is last', () => {
|
||||
// merged doc ends with the added service and a trailing newline from yaml.dump.
|
||||
const service = `services:
|
||||
last:
|
||||
image: busybox
|
||||
`;
|
||||
const { merged, key } = mergeServiceIntoCompose(BASE, service);
|
||||
const [range] = computeAddedRange(merged, key);
|
||||
const lines = merged.split('\n');
|
||||
// endLine must be a real content line, not the trailing empty one.
|
||||
expect(lines[range.endLine - 1].trim()).not.toBe('');
|
||||
});
|
||||
|
||||
test('returns empty when the key is absent', () => {
|
||||
expect(computeAddedRange(BASE, 'nope')).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user