import { describe, expect, it } from "vitest";
import type { DockerNetworkInfo } from "../types.js";
import {
formatContainersMarkdown,
formatHostStatusMarkdown,
formatLogsMarkdown,
formatNetworksMarkdown,
formatScoutDiffMarkdown,
formatScoutExecMarkdown,
formatScoutFindMarkdown,
formatScoutListMarkdown,
formatScoutReadMarkdown,
formatScoutTransferMarkdown,
formatScoutTreeMarkdown,
formatStatsMarkdown,
formatVolumesMarkdown,
truncateIfNeeded,
} from "./index.js";
describe("truncateIfNeeded", () => {
it("should return text unchanged if under limit", () => {
const text = "short text";
expect(truncateIfNeeded(text)).toBe(text);
});
it("should truncate text exceeding CHARACTER_LIMIT", () => {
const longText = "x".repeat(50001);
const result = truncateIfNeeded(longText);
expect(result.length).toBeLessThan(longText.length);
expect(result).toContain("truncated");
});
});
describe("formatContainersMarkdown", () => {
it("should return 'No containers found' for empty array", () => {
const result = formatContainersMarkdown([], 0, 0, false);
expect(result).toContain("No containers found");
});
it("should format container list with canonical symbols", () => {
const containers = [
{
id: "abc123",
name: "test-container",
image: "nginx:latest",
state: "running" as const,
status: "Up 2 hours",
hostName: "tootie",
ports: [],
labels: {},
created: "2024-01-01T00:00:00Z",
},
];
const result = formatContainersMarkdown(containers, 1, 0, false);
expect(result).toContain("●"); // Canonical running symbol per STYLE.md 4.1
expect(result).toContain("test-container");
});
it("should show standard pagination footer when hasMore is true", () => {
const containers = [
{
id: "abc123",
name: "test",
image: "nginx",
state: "running" as const,
status: "Up",
hostName: "tootie",
ports: [],
labels: {},
created: "2024-01-01T00:00:00Z",
},
];
const result = formatContainersMarkdown(containers, 10, 0, true);
// Standard pagination footer per STYLE.md Section 12
expect(result).toContain("Showing 1–1 of 10 | next_offset: 1");
});
});
describe("formatContainersMarkdown - Request Echo (STYLE.md 3.5)", () => {
const testContainers = [
{
id: "abc123",
name: "web-nginx",
image: "nginx:latest",
state: "running" as const,
status: "Up 2 hours",
hostName: "tootie",
ports: [],
labels: {},
created: "2024-01-01T00:00:00Z",
},
{
id: "def456",
name: "api-service",
image: "node:18",
state: "running" as const,
status: "Up 1 hour",
hostName: "tootie",
ports: [],
labels: {},
created: "2024-01-01T01:00:00Z",
},
];
it("should include filter echo when filters are present", () => {
const result = formatContainersMarkdown(testContainers, 2, 0, false, {
state: "running",
});
expect(result).toContain("Filters: state=running");
});
it("should omit filter echo when no filters provided", () => {
const result = formatContainersMarkdown(testContainers, 2, 0, false);
expect(result).not.toContain("Filters:");
});
it("should format filter line between summary and body", () => {
const result = formatContainersMarkdown(testContainers, 2, 0, false, { state: "running" });
const lines = result.split("\n");
const summaryIdx = lines.findIndex((l) => l.includes("Showing"));
const filterIdx = lines.findIndex((l) => l.includes("Filters:"));
const containerIdx = lines.findIndex((l) => l.includes("web-nginx"));
// Filter line appears after summary, before container data
expect(filterIdx).toBeGreaterThan(summaryIdx);
expect(filterIdx).toBeLessThan(containerIdx);
// Verify filter line appears right after summary (headerIdx + 2)
expect(lines[summaryIdx + 1]).toContain("Filters:");
});
it("should handle multiple filter values", () => {
const result = formatContainersMarkdown(testContainers, 2, 0, false, {
state: "running",
host: "tootie",
limit: 10,
});
expect(result).toContain("Filters:");
expect(result).toContain("state=running");
expect(result).toContain("host=tootie");
expect(result).toContain("limit=10");
});
it("should handle boolean filter values", () => {
const result = formatContainersMarkdown(testContainers, 2, 0, false, {
includeExited: true,
});
expect(result).toContain("Filters: includeExited=true");
});
});
describe("formatLogsMarkdown", () => {
it("should return 'No logs found' for empty array", () => {
const result = formatLogsMarkdown([], "test", "host", 10, false, false);
expect(result).toContain("No logs found");
});
it("should format log entries with stream labels", () => {
const logs = [{ stream: "stdout" as const, message: "Test log message" }];
const result = formatLogsMarkdown(logs, "container", "host", 10, false, false);
expect(result).toContain("[stdout] Test log message");
});
});
describe("formatStatsMarkdown", () => {
it("should return 'No stats available' for empty array", () => {
const result = formatStatsMarkdown([], "test-host");
expect(result).toContain("No stats available");
});
it("should format stats for a single container", () => {
const stats = [
{
containerName: "nginx",
cpuPercent: 12.5,
memoryUsage: 1073741824,
memoryLimit: 2147483648,
memoryPercent: 50.0,
networkRx: 1024,
networkTx: 2048,
blockRead: 4096,
blockWrite: 8192,
},
];
const result = formatStatsMarkdown(stats, "test-host");
expect(result).toContain("nginx");
expect(result).toContain("test-host");
expect(result).toContain("12.5%");
expect(result).toContain("CPU");
expect(result).toContain("Memory");
});
});
describe("formatHostStatusMarkdown", () => {
it("should show online status with canonical symbol", () => {
const status = [
{
name: "tootie",
connected: true,
containerCount: 10,
runningCount: 8,
},
];
const result = formatHostStatusMarkdown(status);
expect(result).toContain("●"); // Canonical online symbol per STYLE.md 4.1
expect(result).toContain("Online");
expect(result).toContain("tootie");
});
it("should show offline status with canonical symbol", () => {
const status = [
{
name: "offline-host",
connected: false,
containerCount: 0,
runningCount: 0,
error: "Connection refused",
},
];
const result = formatHostStatusMarkdown(status);
expect(result).toContain("○"); // Canonical offline symbol per STYLE.md 4.1
expect(result).toContain("Offline");
});
});
describe("scout formatters", () => {
describe("formatScoutReadMarkdown", () => {
it("formats file content with path header", () => {
const result = formatScoutReadMarkdown(
"tootie",
"/etc/hosts",
"127.0.0.1 localhost",
100,
false
);
expect(result).toContain("tootie:/etc/hosts");
expect(result).toContain("127.0.0.1 localhost");
});
it("shows truncation notice when truncated", () => {
const result = formatScoutReadMarkdown(
"tootie",
"/var/log/big.log",
"partial content...",
1000000,
true
);
expect(result).toContain("truncated");
});
});
describe("formatScoutListMarkdown", () => {
it("formats directory listing", () => {
const listing = "total 4\ndrwxr-xr-x 2 root root 4096 Jan 1 00:00 test";
const result = formatScoutListMarkdown("tootie", "/var/log", listing);
expect(result).toContain("tootie:/var/log");
expect(result).toContain("total 4");
});
});
describe("formatScoutTreeMarkdown", () => {
it("formats tree output", () => {
const tree = ".\n├── dir1\n└── file.txt";
const result = formatScoutTreeMarkdown("tootie", "/home", tree, 3);
expect(result).toContain("tootie:/home");
expect(result).toContain("├── dir1");
});
});
describe("formatScoutExecMarkdown", () => {
it("formats command result", () => {
const result = formatScoutExecMarkdown("tootie", "/tmp", "ls -la", "file1\nfile2", 0);
expect(result).toContain("ls -la");
expect(result).toContain("file1");
expect(result).toContain("**Exit:** 0");
});
});
describe("formatScoutFindMarkdown", () => {
it("formats find results", () => {
const files = "/var/log/syslog\n/var/log/auth.log";
const result = formatScoutFindMarkdown("tootie", "/var", "*.log", files);
expect(result).toContain("*.log");
expect(result).toContain("/var/log/syslog");
});
});
describe("formatScoutTransferMarkdown", () => {
it("formats transfer result", () => {
const result = formatScoutTransferMarkdown(
"tootie",
"/tmp/file.txt",
"shart",
"/backup/file.txt",
1024
);
expect(result).toContain("tootie:/tmp/file.txt");
expect(result).toContain("shart:/backup/file.txt");
expect(result).toContain("1.0 KB");
});
it("includes warning if present", () => {
const result = formatScoutTransferMarkdown(
"tootie",
"/tmp/file.txt",
"shart",
"/etc/config",
512,
"Warning: system path"
);
expect(result).toContain("Warning");
});
});
describe("formatScoutDiffMarkdown", () => {
it("formats diff output", () => {
const diff = "--- a/hosts\n+++ b/hosts\n@@ -1 +1 @@\n-old\n+new";
const result = formatScoutDiffMarkdown("tootie", "/etc/hosts", "shart", "/etc/hosts", diff);
expect(result).toContain("tootie:/etc/hosts");
expect(result).toContain("shart:/etc/hosts");
expect(result).toContain("---");
});
});
});
describe("docker formatters", () => {
describe("formatNetworksMarkdown", () => {
it("should return 'No networks found' for empty array", () => {
const result = formatNetworksMarkdown([], 0, 0);
expect(result).toContain("No networks found");
});
it("should format networks list grouped by host", () => {
const networks = [
{
id: "net1",
name: "bridge",
driver: "bridge",
scope: "local",
hostName: "tootie",
},
{
id: "net2",
name: "host",
driver: "host",
scope: "local",
hostName: "tootie",
},
{
id: "net3",
name: "overlay",
driver: "overlay",
scope: "swarm",
hostName: "shart",
},
];
const result = formatNetworksMarkdown(networks, 3, 0);
expect(result).toMatch(/^Docker Networks\n/);
expect(result).toContain("Showing 3 of 3 networks");
expect(result).toContain("### tootie");
expect(result).toContain("### shart");
expect(result).toContain("bridge (bridge, local)");
expect(result).toContain("overlay (overlay, swarm)");
});
it("should respect pagination", () => {
const allNetworks = [
{
id: "net1",
name: "net1",
driver: "bridge",
scope: "local",
hostName: "host1",
},
{
id: "net2",
name: "net2",
driver: "bridge",
scope: "local",
hostName: "host1",
},
{
id: "net3",
name: "net3",
driver: "bridge",
scope: "local",
hostName: "host1",
},
];
// Simulate pagination at call site (offset=1, limit=1)
const paginatedNetworks = allNetworks.slice(1, 2);
const result = formatNetworksMarkdown(paginatedNetworks, 3, 1);
expect(result).toContain("Showing 1 of 3 networks");
expect(result).toContain("net2");
expect(result).not.toContain("net1");
expect(result).not.toContain("net3");
});
it("should handle out-of-bounds offset gracefully", () => {
// When pagination happens at call site with offset >= total count,
// an empty array is passed to the formatter (e.g., allNetworks.slice(5, 15) with only 2 items)
const paginatedNetworks: DockerNetworkInfo[] = [];
const result = formatNetworksMarkdown(paginatedNetworks, 2, 5);
expect(result).toContain("No networks found");
});
it("should preserve input order and NOT reorder pre-paginated data", () => {
// Handler now sorts BEFORE pagination, so formatter receives sorted data
// Formatter must preserve this order, even if it spans multiple hosts
const preSortedPaginatedNetworks = [
// Handler sorted by hostName, then paginated - this slice has zulu before alpha
{ id: "net6", name: "net6", driver: "bridge", scope: "local", hostName: "zulu" },
{ id: "net1", name: "net1", driver: "bridge", scope: "local", hostName: "alpha" },
];
const result = formatNetworksMarkdown(preSortedPaginatedNetworks, 10, 5);
// Formatter must preserve input order (zulu before alpha)
const zuluIndex = result.indexOf("### zulu");
const alphaIndex = result.indexOf("### alpha");
expect(zuluIndex).toBeGreaterThan(-1);
expect(alphaIndex).toBeGreaterThan(-1);
expect(zuluIndex).toBeLessThan(alphaIndex); // zulu should appear BEFORE alpha in output
});
});
describe("formatVolumesMarkdown", () => {
it("should return 'No volumes found' for empty array", () => {
const result = formatVolumesMarkdown([], 0, 0);
expect(result).toContain("No volumes found");
});
it("should format volumes list grouped by host", () => {
const volumes = [
{
name: "vol1",
driver: "local",
scope: "local",
hostName: "tootie",
},
{
name: "vol2",
driver: "local",
scope: "local",
hostName: "tootie",
},
{
name: "vol3",
driver: "nfs",
scope: "global",
hostName: "shart",
},
];
const result = formatVolumesMarkdown(volumes, 3, 0);
expect(result).toMatch(/^Docker Volumes\n/);
expect(result).toContain("Showing 3 of 3 volumes");
expect(result).toContain("### tootie");
expect(result).toContain("### shart");
expect(result).toContain("vol1 (local, local)");
expect(result).toContain("vol3 (nfs, global)");
});
it("should respect pagination", () => {
const allVolumes = [
{
name: "vol1",
driver: "local",
scope: "local",
hostName: "host1",
},
{
name: "vol2",
driver: "local",
scope: "local",
hostName: "host1",
},
{
name: "vol3",
driver: "local",
scope: "local",
hostName: "host1",
},
];
// Simulate pagination at call site (offset=1, limit=1)
const paginatedVolumes = allVolumes.slice(1, 2);
const result = formatVolumesMarkdown(paginatedVolumes, 3, 1);
expect(result).toContain("Showing 1 of 3 volumes");
expect(result).toContain("vol2");
expect(result).not.toContain("vol1");
expect(result).not.toContain("vol3");
});
it("should handle out-of-bounds offset with empty volumes array", () => {
// totalCount=2 but offset=5 (beyond available items)
const result = formatVolumesMarkdown([], 2, 5);
expect(result).toContain("No volumes found");
});
it("should preserve host header order for pre-sorted paginated volumes", () => {
// Pre-sorted volumes spanning multiple hosts
const volumes = [
{
name: "vol1",
driver: "local",
scope: "local",
hostName: "alpha",
},
{
name: "vol2",
driver: "local",
scope: "local",
hostName: "alpha",
},
{
name: "vol3",
driver: "nfs",
scope: "global",
hostName: "bravo",
},
{
name: "vol4",
driver: "nfs",
scope: "global",
hostName: "bravo",
},
{
name: "vol5",
driver: "local",
scope: "local",
hostName: "charlie",
},
];
const result = formatVolumesMarkdown(volumes, 5, 0);
const alphaIndex = result.indexOf("### alpha");
const bravoIndex = result.indexOf("### bravo");
const charlieIndex = result.indexOf("### charlie");
expect(alphaIndex).toBeGreaterThan(-1);
expect(bravoIndex).toBeGreaterThan(-1);
expect(charlieIndex).toBeGreaterThan(-1);
expect(alphaIndex).toBeLessThan(bravoIndex);
expect(bravoIndex).toBeLessThan(charlieIndex);
});
});
});
describe("getTimestamp (STYLE.md 3.6)", () => {
it("should return timestamp in correct format", async () => {
const { getTimestamp } = await import("./utils.js");
const result = getTimestamp();
expect(result).toMatch(/^As of \([^)]+\): \d{2}:\d{2}:\d{2} \| \d{2}\/\d{2}\/\d{4}$/);
});
it("should include timezone label by default", async () => {
const { getTimestamp } = await import("./utils.js");
const result = getTimestamp();
expect(result).toMatch(/^As of \([^)]+\):/);
});
it("should support custom timezone parameter", async () => {
const { getTimestamp } = await import("./utils.js");
const result = getTimestamp("UTC");
expect(result).toMatch(/^As of \([^)]+\): \d{2}:\d{2}:\d{2} \| \d{2}\/\d{2}\/\d{4}$/);
expect(result).toContain("(UTC)");
});
});