Files
sencho/frontend/src/lib/dossierMarkdown.test.ts
T
Anso 2eafee3594 fix(networking): treat host-network services as host-exposed in summaries (#1430)
The exposure summaries derived a stack's exposure solely from the declared
published-port list, so a service running with network_mode: host (which
publishes every container port on the host but declares no ports:) was
under-reported as less exposed than it actually is.

Capture network_mode in the lightweight dependency parser, add an
isHostNetwork predicate, and treat a host-network service as exposed and
publishing across the Fleet networking summary, the Stack Dossier export, and
the Networking panel, matching how the Compose Doctor already flags host
networking.
2026-06-24 19:50:45 -04:00

95 lines
4.0 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { buildStackDossierMarkdown, EMPTY_DOSSIER_FIELDS, type StackDossierFields } from './dossierMarkdown';
import type { AnatomyMarkdownInput } from './anatomyMarkdown';
const anatomy: AnatomyMarkdownInput = {
stackName: 'plex',
services: ['plex'],
ports: { plex: [{ host: '32400', container: '32400', proto: 'tcp' }] },
volumes: {},
restart: 'unless-stopped',
envFile: '.env',
envVarCount: 4,
missingVars: ['CLAIM_TOKEN'],
networkName: 'plex_default',
gitSource: null,
};
const fields = (over: Partial<StackDossierFields> = {}): StackDossierFields => ({ ...EMPTY_DOSSIER_FIELDS, ...over });
describe('buildStackDossierMarkdown', () => {
it('returns only the anatomy markdown when no operator notes are set', () => {
const md = buildStackDossierMarkdown(anatomy, fields());
expect(md).toContain('# plex');
expect(md).toContain('## Services');
expect(md).not.toContain('## Operator notes');
expect(md).not.toContain('## Network exposure');
});
it('appends the network exposure section when a summary is provided', () => {
const md = buildStackDossierMarkdown(anatomy, fields(), {
stackIntent: 'internal',
networks: [{ name: 'plex_default', external: false, internal: false }],
services: [{ name: 'plex', intent: null, ports: ['32400/tcp (all interfaces)'], hostNetwork: false }],
});
expect(md).toContain('## Network exposure');
expect(md).toContain('**Stack intent:** internal');
expect(md).toContain('32400/tcp (all interfaces)');
});
it('appends an Operator notes section with the filled fields', () => {
const md = buildStackDossierMarkdown(anatomy, fields({
purpose: 'Media server',
owner: 'home',
static_ip: '10.0.10.4',
backup_notes: 'rsync config nightly',
}));
expect(md).toContain('## Operator notes');
expect(md).toContain('- **Purpose:** Media server');
expect(md).toContain('- **Owner:** home');
expect(md).toContain('- **Static IP:** 10.0.10.4');
expect(md).toContain('### Backup\nrsync config nightly');
});
it('omits empty operator fields', () => {
const md = buildStackDossierMarkdown(anatomy, fields({ purpose: 'only this' }));
expect(md).toContain('- **Purpose:** only this');
expect(md).not.toContain('- **Owner:**');
expect(md).not.toContain('### Firewall');
});
it('keeps the generated anatomy facts in the combined export', () => {
const md = buildStackDossierMarkdown(anatomy, fields({ purpose: 'p' }));
expect(md).toContain('| plex | 32400 | 32400 | tcp |');
expect(md).toContain('- Variables: 4');
expect(md).toContain('- Missing: `CLAIM_TOKEN`');
});
it('preserves multi-line access URLs as a block', () => {
const md = buildStackDossierMarkdown(anatomy, fields({ access_urls: 'https://a.example\nhttps://b.example' }));
expect(md).toContain('### Access URLs\nhttps://a.example\nhttps://b.example');
});
it('collapses stray newlines in a single-line field into one bullet', () => {
const md = buildStackDossierMarkdown(anatomy, fields({ purpose: 'line one\nline two' }));
expect(md).toContain('- **Purpose:** line one line two');
});
it('the generated facts never emit a .env assignment, only variable names and counts', () => {
const md = buildStackDossierMarkdown(anatomy, fields({ custom_notes: 'see runbook' }));
expect(md).toContain('- Variables: 4');
expect(md).toContain('- Missing: `CLAIM_TOKEN`');
expect(md).not.toMatch(/CLAIM_TOKEN\s*=/);
});
it('exports operator notes verbatim (user-authored content is not redacted)', () => {
const md = buildStackDossierMarkdown(anatomy, fields({ custom_notes: 'DB_PASSWORD=hunter2' }));
expect(md).toContain('DB_PASSWORD=hunter2');
});
it('is deterministic across distinct but equal inputs', () => {
const f = fields({ purpose: 'x', firewall_notes: 'y' });
expect(buildStackDossierMarkdown(anatomy, f)).toBe(buildStackDossierMarkdown(anatomy, { ...f }));
});
});