import { describe, expect, it } from "vitest";
import type {
DockerDiskUsage,
DockerNetworkInfo,
DockerVolumeInfo,
ImageInfo,
PruneResult,
} from "../types.js";
import {
type DockerInfoResult,
formatDockerDfMarkdown,
formatDockerInfoMarkdown,
formatImagesMarkdown,
formatNetworksMarkdown,
formatPruneMarkdown,
formatVolumesMarkdown,
} from "./docker.js";
describe("formatDockerInfoMarkdown", () => {
describe("symbol usage", () => {
it("should use canonical ✗ symbol for errors, not emoji", () => {
const errorResult: DockerInfoResult = {
status: "error",
errorMessage: "Connection refused",
};
const output = formatDockerInfoMarkdown([{ host: "test-host", info: errorResult }]);
// Should use canonical ✗ symbol
expect(output).toContain("✗ Error:");
// Should NOT use emoji ❌
expect(output).not.toContain("❌");
});
});
describe("title format", () => {
it("should use plain text title without markdown ## when single host", () => {
const successResult: DockerInfoResult = {
status: "success",
dockerVersion: "24.0.6",
apiVersion: "1.43",
os: "linux",
arch: "x86_64",
kernelVersion: "6.1.0",
cpus: 8,
memoryBytes: 16000000000,
storageDriver: "overlay2",
rootDir: "/var/lib/docker",
containersRunning: 5,
containersTotal: 10,
images: 20,
};
const output = formatDockerInfoMarkdown([{ host: "squirts", info: successResult }]);
// Should follow STYLE.md 3.1: "Entity on host" format (plain text)
expect(output).toMatch(/^Docker System Info on squirts\n/);
// Should NOT have markdown ## prefix
expect(output).not.toContain("## Docker System Info");
});
it("should use (all) suffix when multiple hosts", () => {
const successResult: DockerInfoResult = {
status: "success",
dockerVersion: "24.0.6",
apiVersion: "1.43",
os: "linux",
arch: "x86_64",
kernelVersion: "6.1.0",
cpus: 8,
memoryBytes: 16000000000,
storageDriver: "overlay2",
rootDir: "/var/lib/docker",
containersRunning: 5,
containersTotal: 10,
images: 20,
};
const output = formatDockerInfoMarkdown([
{ host: "host1", info: successResult },
{ host: "host2", info: successResult },
]);
// Should follow STYLE.md 3.1: "Entity (all)" format when multiple
expect(output).toMatch(/^Docker System Info \(all\)\n/);
expect(output).not.toContain("## Docker System Info");
});
});
describe("summary line", () => {
it("should include Hosts: N | OK: N | Fail: N summary per STYLE.md 3.2", () => {
const successResult: DockerInfoResult = {
status: "success",
dockerVersion: "24.0.6",
apiVersion: "1.43",
os: "linux",
arch: "x86_64",
kernelVersion: "6.1.0",
cpus: 8,
memoryBytes: 16000000000,
storageDriver: "overlay2",
rootDir: "/var/lib/docker",
containersRunning: 5,
containersTotal: 10,
images: 20,
};
const errorResult: DockerInfoResult = {
status: "error",
errorMessage: "Connection refused",
};
const output = formatDockerInfoMarkdown([
{ host: "host1", info: successResult },
{ host: "host2", info: errorResult },
{ host: "host3", info: successResult },
]);
// Should include summary with pipe-separated counts
expect(output).toContain("Hosts: 3 | OK: 2 | Fail: 1");
});
});
});
describe("formatDockerDfMarkdown", () => {
const mockUsage: DockerDiskUsage = {
images: { total: 10, active: 5, size: 1000000000, reclaimable: 500000000 },
containers: { total: 8, running: 4, size: 200000000, reclaimable: 100000000 },
volumes: { total: 15, active: 10, size: 3000000000, reclaimable: 1000000000 },
buildCache: { total: 20, size: 500000000, reclaimable: 300000000 },
totalSize: 4700000000,
totalReclaimable: 1900000000,
};
describe("title format", () => {
it("should use plain text title with host when single host", () => {
const output = formatDockerDfMarkdown([{ host: "squirts", usage: mockUsage }]);
expect(output).toMatch(/^Docker Disk Usage on squirts\n/);
expect(output).not.toContain("## Docker Disk Usage");
});
it("should use (all) suffix when multiple hosts", () => {
const output = formatDockerDfMarkdown([
{ host: "host1", usage: mockUsage },
{ host: "host2", usage: mockUsage },
]);
expect(output).toMatch(/^Docker Disk Usage \(all\)\n/);
expect(output).not.toContain("## Docker Disk Usage");
});
});
describe("summary line", () => {
it("should include aggregate totals across hosts", () => {
const output = formatDockerDfMarkdown([
{ host: "host1", usage: mockUsage },
{ host: "host2", usage: mockUsage },
]);
// Should aggregate total size and reclaimable across both hosts
// Each host has 4.7GB (4700000000) total and 1.9GB (1900000000) reclaimable
// 2 hosts = 9.4GB (9400000000) total = 8.8GB displayed, 3.8GB (3800000000) reclaimable = 3.5GB displayed
expect(output).toContain("Total: 8.8 GB | Reclaimable: 3.5 GB");
});
});
});
describe("formatImagesMarkdown", () => {
const mockImages: ImageInfo[] = [
{
id: "abc123",
tags: ["nginx:latest", "nginx:1.25"],
size: 100000000,
created: 1234567890,
hostName: "host1",
containers: 2,
},
];
it("should use plain text title with host when host provided", () => {
const output = formatImagesMarkdown(mockImages, 10, 0, "squirts");
expect(output).toMatch(/^Docker Images on squirts\n/);
expect(output).not.toContain("## Docker Images");
});
it("should use plain text title without host when not provided", () => {
const output = formatImagesMarkdown(mockImages, 10, 0);
expect(output).toMatch(/^Docker Images\n/);
expect(output).not.toContain("## Docker Images");
});
});
describe("formatNetworksMarkdown", () => {
const mockNetworks: DockerNetworkInfo[] = [
{
id: "net123",
name: "bridge",
driver: "bridge",
scope: "local",
hostName: "host1",
},
];
it("should use plain text title with host when host provided", () => {
const output = formatNetworksMarkdown(mockNetworks, 5, 0, "squirts");
expect(output).toMatch(/^Docker Networks on squirts\n/);
expect(output).not.toContain("## Docker Networks");
});
it("should use plain text title without host when not provided", () => {
const output = formatNetworksMarkdown(mockNetworks, 5, 0);
expect(output).toMatch(/^Docker Networks\n/);
expect(output).not.toContain("## Docker Networks");
});
});
describe("formatVolumesMarkdown", () => {
const mockVolumes: DockerVolumeInfo[] = [
{
name: "vol1",
driver: "local",
scope: "local",
mountpoint: "/var/lib/docker/volumes/vol1",
hostName: "host1",
},
];
it("should use plain text title with host when host provided", () => {
const output = formatVolumesMarkdown(mockVolumes, 8, 0, "squirts");
expect(output).toMatch(/^Docker Volumes on squirts\n/);
expect(output).not.toContain("## Docker Volumes");
});
it("should use plain text title without host when not provided", () => {
const output = formatVolumesMarkdown(mockVolumes, 8, 0);
expect(output).toMatch(/^Docker Volumes\n/);
expect(output).not.toContain("## Docker Volumes");
});
});
describe("formatPruneMarkdown", () => {
const mockPruneResults: PruneResult[] = [
{ type: "containers", itemsDeleted: 3, spaceReclaimed: 500000000 },
{ type: "networks", itemsDeleted: 0, spaceReclaimed: 0 },
{ type: "build cache", itemsDeleted: 5, spaceReclaimed: 1300000000 },
];
describe("title format", () => {
it("should use plain text title with host when single host", () => {
const output = formatPruneMarkdown([{ host: "squirts", results: mockPruneResults }]);
expect(output).toMatch(/^Docker Prune on squirts\n/);
expect(output).not.toContain("## Prune Results");
});
it("should use (all) suffix when multiple hosts", () => {
const output = formatPruneMarkdown([
{ host: "host1", results: mockPruneResults },
{ host: "host2", results: mockPruneResults },
]);
expect(output).toMatch(/^Docker Prune \(all\)\n/);
expect(output).not.toContain("## Prune Results");
});
});
describe("RECLAIMED format per STYLE.md 7.8", () => {
it("should use RECLAIMED section format instead of table", () => {
const output = formatPruneMarkdown([{ host: "squirts", results: mockPruneResults }]);
// Should have RECLAIMED: section header
expect(output).toContain("RECLAIMED:");
// Should have indented list format (not table)
expect(output).toContain(" containers: ");
expect(output).toContain(" networks: ");
expect(output).toContain(" build cache: ");
// Should NOT have table format
expect(output).not.toContain("| Type |");
expect(output).not.toContain("|------|");
});
});
});