import { describe, expect, it } from "vitest";
import type { ContainerInfo, DockerRawContainerInspectInfo } from "../types.js";
import {
formatContainerExecMarkdown,
formatContainersMarkdown,
formatInspectMarkdown,
formatLogsMarkdown,
formatMultiStatsMarkdown,
formatStatsMarkdown,
} from "./container.js";
describe("formatContainersMarkdown - STYLE.md compliance", () => {
const mockContainers: ContainerInfo[] = [
{
id: "abc123",
name: "nginx",
image: "nginx:latest",
state: "running",
status: "Up 2 hours",
hostName: "squirts",
ports: [{ hostPort: 8080, containerPort: 80, protocol: "tcp" }],
labels: {},
created: "2024-01-01T00:00:00Z",
},
{
id: "def456",
name: "stopped-container",
image: "redis:alpine",
state: "exited",
status: "Exited (0) 5 minutes ago",
hostName: "squirts",
ports: [],
labels: {},
created: "2024-01-01T00:00:00Z",
},
];
it("should NOT contain emoji (π’π‘π΄)", () => {
const output = formatContainersMarkdown(mockContainers, 2, 0, false);
expect(output).not.toContain("π’");
expect(output).not.toContain("π‘");
expect(output).not.toContain("π΄");
});
it("should use canonical symbols (βββ)", () => {
const output = formatContainersMarkdown(mockContainers, 2, 0, false);
// Should contain β for running and β for stopped
expect(output).toContain("β");
expect(output).toContain("β");
});
it("should NOT have markdown ## prefix in title", () => {
const output = formatContainersMarkdown(mockContainers, 2, 0, false);
const lines = output.split("\n");
expect(lines[0]).not.toMatch(/^##/);
});
it("should include summary line with counts", () => {
const output = formatContainersMarkdown(mockContainers, 2, 0, false);
// Should have "Showing N of M containers"
expect(output).toMatch(/Showing \d+ of \d+ containers/);
});
it("should include legend for mixed states", () => {
const output = formatContainersMarkdown(mockContainers, 2, 0, false);
// Should include legend when multiple states present
expect(output).toMatch(/Legend:/);
});
it("should use aligned table structure (not list format)", () => {
const output = formatContainersMarkdown(mockContainers, 2, 0, false);
// Should have space-aligned table headers per STYLE.md Section 5.1
expect(output).toContain(" Container");
expect(output).toContain(" -------------------------");
});
it("should include host information in table output", () => {
const output = formatContainersMarkdown(mockContainers, 2, 0, false);
expect(output).toContain("Host");
expect(output).toContain("squirts");
});
it("should apply severity-first sorting (exited/stopped before running)", () => {
const output = formatContainersMarkdown(mockContainers, 2, 0, false);
const lines = output.split("\n");
// stopped-container should appear before nginx
const stoppedIdx = lines.findIndex((l) => l.includes("stopped-container"));
const runningIdx = lines.findIndex((l) => l.includes("nginx"));
expect(stoppedIdx).toBeLessThan(runningIdx);
});
it("should use standard pagination footer (not italic)", () => {
const output = formatContainersMarkdown(mockContainers.slice(0, 1), 10, 0, true);
// Should use "Showing XβY of Z | next_offset: N" format
expect(output).toMatch(/Showing \d+β\d+ of \d+ \| next_offset: \d+/);
expect(output).not.toMatch(/\*More results available/);
});
});
describe("formatStatsMarkdown - STYLE.md compliance", () => {
const testStats = [
{
containerName: "nginx",
cpuPercent: 12.5,
memoryUsage: 1073741824,
memoryLimit: 2147483648,
memoryPercent: 50.0,
networkRx: 1024,
networkTx: 2048,
blockRead: 4096,
blockWrite: 8192,
},
];
it("should NOT have markdown ## prefix in title", () => {
const result = formatStatsMarkdown(testStats, "squirts");
const lines = result.split("\n");
expect(lines[0]).not.toMatch(/^##/);
});
it("should include freshness timestamp", () => {
const result = formatStatsMarkdown(testStats, "squirts");
expect(result).toMatch(/As of \(EST\): \d{2}:\d{2}:\d{2} \| \d{2}\/\d{2}\/\d{4}/);
});
it("should have timestamp after title and before table", () => {
const result = formatStatsMarkdown(testStats, "squirts");
const lines = result.split("\n");
const titleIdx = lines.findIndex((l) => l.includes("Stats:"));
const timestampIdx = lines.findIndex((l) => /^As of \([^)]+\):/.test(l));
const tableIdx = lines.findIndex((l) => l.includes("| Metric"));
expect(timestampIdx).toBeGreaterThan(titleIdx);
expect(timestampIdx).toBeLessThan(tableIdx);
});
it("should include warning symbol for CPU usage >90%", () => {
const highCpuStats = [{ ...testStats[0], cpuPercent: 95 }];
const result = formatStatsMarkdown(highCpuStats, "squirts");
expect(result).toContain("95.0% β ");
});
it("should include warning symbol for memory usage >90%", () => {
const highMemStats = [{ ...testStats[0], memoryPercent: 94 }];
const result = formatStatsMarkdown(highMemStats, "squirts");
expect(result).toContain("94.0% β ");
});
it("should NOT include warning for normal usage levels", () => {
const result = formatStatsMarkdown(testStats, "squirts");
// testStats has 12.5% CPU and 50% memory - no warnings
expect(result).not.toContain("β ");
});
});
describe("formatMultiStatsMarkdown - Timestamp (STYLE.md 3.6)", () => {
const testStats = [
{
stats: {
containerName: "nginx",
cpuPercent: 12.5,
memoryUsage: 1073741824,
memoryLimit: 2147483648,
memoryPercent: 50.0,
networkRx: 1024,
networkTx: 2048,
blockRead: 4096,
blockWrite: 8192,
},
host: "squirts",
},
{
stats: {
containerName: "redis",
cpuPercent: 5.0,
memoryUsage: 536870912,
memoryLimit: 2147483648,
memoryPercent: 25.0,
networkRx: 512,
networkTx: 1024,
blockRead: 2048,
blockWrite: 4096,
},
host: "tootie",
},
];
it("should NOT have markdown ## prefix in title", () => {
const result = formatMultiStatsMarkdown(testStats);
const lines = result.split("\n");
expect(lines[0]).not.toMatch(/^##/);
});
it("should include summary with total container count", () => {
const result = formatMultiStatsMarkdown(testStats);
// Should have summary like "Containers: 2" or "Showing 2 containers"
expect(result).toMatch(/Containers: 2|Showing 2 containers/);
});
it("should include freshness timestamp", () => {
const result = formatMultiStatsMarkdown(testStats);
expect(result).toMatch(/As of \(EST\): \d{2}:\d{2}:\d{2} \| \d{2}\/\d{2}\/\d{4}/);
});
it("should have timestamp after title and before table", () => {
const result = formatMultiStatsMarkdown(testStats);
const lines = result.split("\n");
const titleIdx = lines.findIndex((l) => l.includes("Container Resource Usage"));
const timestampIdx = lines.findIndex((l) => /^As of \([^)]+\):/.test(l));
const tableIdx = lines.findIndex((l) => l.includes("| Container"));
expect(timestampIdx).toBeGreaterThan(titleIdx);
expect(timestampIdx).toBeLessThan(tableIdx);
});
it("should apply severity-first sorting (highest CPU/MEM first)", () => {
const mixedStats = [
{
stats: {
containerName: "low-resource",
cpuPercent: 5.0,
memoryPercent: 10.0,
memoryUsage: 100000000,
memoryLimit: 1000000000,
networkRx: 0,
networkTx: 0,
blockRead: 0,
blockWrite: 0,
},
host: "host1",
},
{
stats: {
containerName: "high-cpu",
cpuPercent: 95.0,
memoryPercent: 20.0,
memoryUsage: 200000000,
memoryLimit: 1000000000,
networkRx: 0,
networkTx: 0,
blockRead: 0,
blockWrite: 0,
},
host: "host2",
},
{
stats: {
containerName: "high-mem",
cpuPercent: 10.0,
memoryPercent: 94.0,
memoryUsage: 940000000,
memoryLimit: 1000000000,
networkRx: 0,
networkTx: 0,
blockRead: 0,
blockWrite: 0,
},
host: "host3",
},
];
const result = formatMultiStatsMarkdown(mixedStats);
const lines = result.split("\n");
const dataLines = lines.filter((l) => l.startsWith("| ") && !l.startsWith("| Container"));
// high-cpu or high-mem should appear before low-resource
const highCpuIdx = dataLines.findIndex((l) => l.includes("high-cpu"));
const highMemIdx = dataLines.findIndex((l) => l.includes("high-mem"));
const lowIdx = dataLines.findIndex((l) => l.includes("low-resource"));
expect(highCpuIdx).toBeLessThan(lowIdx);
expect(highMemIdx).toBeLessThan(lowIdx);
});
it("should include warning symbols for CPU>90% in table", () => {
const highCpuStats = [
{
stats: {
containerName: "high-cpu",
cpuPercent: 95.0,
memoryPercent: 50.0,
memoryUsage: 500000000,
memoryLimit: 1000000000,
networkRx: 0,
networkTx: 0,
blockRead: 0,
blockWrite: 0,
},
host: "host1",
},
];
const result = formatMultiStatsMarkdown(highCpuStats);
// Should show β after CPU percentage
expect(result).toContain("95.0% β ");
});
it("should include warning symbols for memory>90% in table", () => {
const highMemStats = [
{
stats: {
containerName: "high-mem",
cpuPercent: 50.0,
memoryPercent: 94.0,
memoryUsage: 940000000,
memoryLimit: 1000000000,
networkRx: 0,
networkTx: 0,
blockRead: 0,
blockWrite: 0,
},
host: "host1",
},
];
const result = formatMultiStatsMarkdown(highMemStats);
// Should show β after memory percentage
expect(result).toContain("94.0% β ");
});
it("should NOT include warning for normal usage levels", () => {
const result = formatMultiStatsMarkdown(testStats);
// testStats has 12.5% CPU and 50%/25% memory - no warnings
expect(result).not.toContain("β ");
});
});
describe("formatContainerExecMarkdown", () => {
it("should include preview format for large exec output", () => {
const output = Array.from({ length: 20 }, (_, i) => `line ${i + 1}`).join("\n");
const result = formatContainerExecMarkdown("nginx", "squirts", "cat /tmp/log", output, 0);
expect(result).toContain("Output lines: 20 (previewed)");
expect(result).toContain("Preview (first 5):");
expect(result).toContain("Preview (last 5):");
expect(result).not.toContain("line 10");
});
});
describe("formatLogsMarkdown - STYLE.md 7.5 preview format", () => {
it("should NOT have markdown ## prefix in title", () => {
const logs = [{ stream: "stdout" as const, message: "line 1" }];
const result = formatLogsMarkdown(logs, "nginx", "squirts", 10, false, false);
const lines = result.split("\n");
expect(lines[0]).not.toMatch(/^##/);
});
it("should include summary with counts and flags", () => {
const logs = [{ stream: "stdout" as const, message: "line 1" }];
const result = formatLogsMarkdown(logs, "nginx", "squirts", 10, false, false);
expect(result).toMatch(/Lines returned: 1 \(requested: 10\) \| truncated: no \| follow: no/);
});
it("should include freshness timestamp for volatile log data", () => {
const logs = [{ stream: "stdout" as const, message: "line 1" }];
const result = formatLogsMarkdown(logs, "nginx", "squirts", 10, false, false);
expect(result).toMatch(/As of \(EST\): \d{2}:\d{2}:\d{2} \| \d{2}\/\d{2}\/\d{4}/);
});
it("should show all lines when β€10 lines", () => {
const logs = Array.from({ length: 8 }, (_, i) => ({
stream: "stdout" as const,
message: `line ${i + 1}`,
}));
const result = formatLogsMarkdown(logs, "nginx", "squirts", 10, false, false);
// All 8 lines should be present
expect(result).toContain("line 1");
expect(result).toContain("line 8");
expect(result).not.toContain("Preview");
});
it("should use preview format (first 5 + last 5) when >10 lines", () => {
const logs = Array.from({ length: 20 }, (_, i) => ({
stream: "stdout" as const,
message: `line ${i + 1}`,
}));
const result = formatLogsMarkdown(logs, "nginx", "squirts", 20, false, false);
// Preview format per STYLE.md 7.5
expect(result).toContain("Preview (first 5):");
expect(result).toContain("Preview (last 5):");
expect(result).toContain("line 1");
expect(result).toContain("line 5");
expect(result).toContain("line 16");
expect(result).toContain("line 20");
// Middle lines should NOT be present
expect(result).not.toContain("line 10");
});
it("should label stdout and stderr streams", () => {
const logs = [
{ stream: "stdout" as const, message: "normal output" },
{ stream: "stderr" as const, message: "error output" },
];
const result = formatLogsMarkdown(logs, "nginx", "squirts", 10, false, false);
expect(result).toContain("[stdout] normal output");
expect(result).toContain("[stderr] error output");
});
});
describe("formatInspectMarkdown - STYLE.md compliance", () => {
const mockInspect: DockerRawContainerInspectInfo = {
Id: "abc123",
Name: "/nginx",
State: {
Status: "running",
Running: true,
Paused: false,
Restarting: false,
OOMKilled: false,
Dead: false,
Pid: 12345,
ExitCode: 0,
Error: "",
StartedAt: "2024-01-01T10:00:00Z",
FinishedAt: "0001-01-01T00:00:00Z",
},
Config: {
Image: "nginx:latest",
Cmd: ["nginx", "-g", "daemon off;"],
WorkingDir: "/usr/share/nginx/html",
Env: ["PATH=/usr/bin", "NGINX_VERSION=1.25.3"],
},
RestartCount: 0,
Mounts: [
{
Type: "bind",
Source: "/data/config",
Destination: "/etc/nginx",
Mode: "ro",
RW: false,
Propagation: "rprivate",
},
],
NetworkSettings: {
Ports: {
"80/tcp": [{ HostIp: "0.0.0.0", HostPort: "8080" }],
},
Networks: {
bridge: {},
},
},
};
it("should NOT have markdown ## prefix in title", () => {
const result = formatInspectMarkdown(mockInspect, "squirts");
const lines = result.split("\n");
expect(lines[0]).not.toMatch(/^##/);
});
it("should NOT have markdown ### prefixes in section headers", () => {
const result = formatInspectMarkdown(mockInspect, "squirts");
// Should not have ### State, ### Configuration, etc.
expect(result).not.toMatch(/^###/m);
});
it("should use canonical β for port mapping notation", () => {
const result = formatInspectMarkdown(mockInspect, "squirts");
// Already uses β but verify it's present
expect(result).toContain("β");
});
it("should include container name and host in title", () => {
const result = formatInspectMarkdown(mockInspect, "squirts");
const lines = result.split("\n");
// Should have "Container: nginx (squirts)" or similar
expect(lines[0]).toContain("nginx");
expect(lines[0]).toContain("squirts");
});
});