diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 053181e..32ee982 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: # Default tests use captured CLI fixtures and tolerate a missing binary. # The opt-in live smoke test stays ignored here; it needs a running - # container service with virtualization. See docs/container-1.4.1.md. + # container service with virtualization. See docs/container-1.5.0.md. - name: cargo test run: cargo test --locked working-directory: src-tauri diff --git a/README.md b/README.md index aa59f12..1eb7f90 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ app does, you could have typed at the prompt yourself. |---|---| | **Mac** | Apple silicon. `container` does not run on Intel. | | **macOS** | 26 or newer. Apple does not support `container` below that. | -| **`container` CLI** | 1.4.1 recommended and tested. | +| **`container` CLI** | 1.5.0 recommended and tested. 1.4.1 is also supported. | | **To build** | Node.js 20+ and Rust via [rustup](https://rustup.rs). | Get the CLI from [apple/container releases](https://github.com/apple/container/releases), @@ -229,7 +229,7 @@ A banner across the top tells you whether the container system is running and of start or stop it. If the status check fails, it shows the reason and offers a retry. Compatibility notes and the live test procedure are in -[docs/container-1.4.1.md](docs/container-1.4.1.md). +[docs/container-1.5.0.md](docs/container-1.5.0.md). ## Development diff --git a/docs/container-1.4.1.md b/docs/container-1.5.0.md similarity index 63% rename from docs/container-1.4.1.md rename to docs/container-1.5.0.md index b97af94..e935f74 100644 --- a/docs/container-1.4.1.md +++ b/docs/container-1.5.0.md @@ -1,11 +1,28 @@ -# Container 1.4.1 compatibility +# Container 1.5.0 compatibility The desktop app invokes the installed CLI; it does not embed Apple's Swift packages. -Use `container --version` to confirm the CLI version. Version 1.4.1 is the tested and -recommended version. Legacy JSON shapes remain covered where supported, but older CLI -releases have not been rerun through the live suite for this update. +Use `container --version` to confirm the CLI version. Version 1.5.0 is the tested and +recommended version; 1.4.1 remains supported. Legacy JSON shapes remain covered where +supported, but releases before 1.4.1 have not been rerun through the live suite. -## Changes +## 1.5.0 + +No app code changes were needed. The JSON the app reads from `system status`, `ls`, +`stats`, `image ls`, `machine ls`, `volume ls`, `network ls`, and `builder status` has +the same structure as 1.4.1. Unknown commands still exit nonzero (now 64 with a longer +message), so the error path is unchanged. + +Apple's [1.5.0 release notes](https://github.com/apple/container/releases/tag/1.5.0) +list one breaking CLI change, the removal of `container k8s start`. The app does not +use the Kubernetes plugin. The other changes (commands running with an empty image +entrypoint, egress after localhost DNS changes, a kubeconfig security fix) need no app +changes. + +The captured `image ls` output lists every platform variant of the multi-arch Alpine +index, including `unknown/unknown` attestation entries. The app still shows the arm64 +variant's size. + +## 1.4.1 - Recognize tmpfs and named-volume mount types before checking host bind directories. Both legacy blank tmpfs sources and the current `"tmpfs"` source work. @@ -42,22 +59,23 @@ cargo test --locked cargo build --locked ``` -Fixtures under `src/test/fixtures/container-1.4.1/` were captured from the installed -1.4.1 CLI on Apple silicon. Personal paths and disposable resource names are replaced +Fixtures under `src/test/fixtures/container-1.5.0/` and +`src/test/fixtures/container-1.4.1/` were captured from the installed CLI of each +version on Apple silicon. The frontend and Rust fixture tests run against both. Personal paths and disposable resource names are replaced with test values. They cover system status, containers, stats, images, machines, volumes, networks, and an existing stopped builder. Container and machine fixtures come from disposable Alpine resources. The fixture tests run without the CLI in CI. ## Live smoke test -Use an Apple silicon Mac with container 1.4.1, virtualization support, network access +Use an Apple silicon Mac with container 1.5.0 or 1.4.1, virtualization support, network access for image pulls, and enough memory for the test container, machine, and builder. After building the frontend: ```sh container system start cd src-tauri -cargo test --locked --test cli_compatibility live_1_4_1_smoke -- --ignored --nocapture +cargo test --locked --test cli_compatibility live_smoke -- --ignored --nocapture ``` The test exercises the app's Rust command functions for pull, container lifecycle, @@ -77,17 +95,24 @@ and the settings preview and recreation flow using a disposable container. ## Upstream machine initialization limitation -In 1.4.1, the first noninteractive `machine run` immediately after creating a machine -can fail with `Operation not supported by device` or `Inappropriate ioctl for device`. +In 1.4.1 and 1.5.0, the first noninteractive `machine run` immediately after creating a +machine can fail with `Operation not supported by device` or `Inappropriate ioctl for device`. The CLI's [boot helper](https://github.com/apple/container/blob/1.4.1/Sources/ContainerCommands/Machine/MachineHelpers.swift) requests a terminal when its snapshot still reports first-time user setup as incomplete. Stopping the newly created machine before its first shell command lets the next boot -load the persisted setup state. This workaround passed the live test; the smoke test -includes that stop explicitly. The app surfaces CLI errors and does not automatically +load the persisted setup state. The problem still occurs in 1.5.0. This workaround +passed the live test; the smoke test includes that stop explicitly. The app surfaces CLI errors and does not automatically retry shell commands, which could execute user work twice. ## Validation record +Validated September 30, 2026 on Apple silicon, macOS 27.0, container client/server 1.5.0: +303 frontend tests, 117 default Rust tests, formatting, Clippy with warnings denied, +frontend/native builds, and the opt-in live smoke test passed. The live test includes +the machine initialization workaround above. + +Earlier record for 1.4.1: + Validated September 17, 2026 on Apple silicon, macOS 27.0, container client/server 1.4.1: 295 frontend tests, 117 default Rust tests, formatting, Clippy with warnings denied, frontend/native builds, and the opt-in live smoke test passed. The live test includes diff --git a/src-tauri/src/builder.rs b/src-tauri/src/builder.rs index 2c59be5..20627af 100644 --- a/src-tauri/src/builder.rs +++ b/src-tauri/src/builder.rs @@ -90,16 +90,17 @@ mod tests { use serde_json::json; #[test] - fn reads_captured_1_4_1_builder_status() { - let value = serde_json::from_str(include_str!( - "../../src/test/fixtures/container-1.4.1/builder.json" - )) - .unwrap(); - let state = parse_state(&value); - assert!(state.exists); - assert!(!state.running); - assert_eq!(state.cpus, Some(2)); - assert_eq!(state.memory_mb, Some(2048)); + fn reads_captured_builder_status() { + for payload in [ + include_str!("../../src/test/fixtures/container-1.4.1/builder.json"), + include_str!("../../src/test/fixtures/container-1.5.0/builder.json"), + ] { + let state = parse_state(&serde_json::from_str(payload).unwrap()); + assert!(state.exists); + assert!(!state.running); + assert_eq!(state.cpus, Some(2)); + assert_eq!(state.memory_mb, Some(2048)); + } } fn builder(state: &str) -> Value { diff --git a/src-tauri/src/cli.rs b/src-tauri/src/cli.rs index 2cbea0a..30d3f3f 100644 --- a/src-tauri/src/cli.rs +++ b/src-tauri/src/cli.rs @@ -214,11 +214,21 @@ mod tests { #[test] fn expanded_running_status_is_supported() { - let payload = include_str!("../../src/test/fixtures/container-1.4.1/system-running.json"); - let result = parse_output(&["system", "status"], &output(0, payload, "")).unwrap(); - assert_eq!(result["status"], "running"); - assert_eq!(result["client"]["version"], "1.4.1"); - assert_eq!(result["server"]["version"], "1.4.1"); + for (version, payload) in [ + ( + "1.4.1", + include_str!("../../src/test/fixtures/container-1.4.1/system-running.json"), + ), + ( + "1.5.0", + include_str!("../../src/test/fixtures/container-1.5.0/system-running.json"), + ), + ] { + let result = parse_output(&["system", "status"], &output(0, payload, "")).unwrap(); + assert_eq!(result["status"], "running"); + assert_eq!(result["client"]["version"], version); + assert_eq!(result["server"]["version"], version); + } } #[test] diff --git a/src-tauri/tests/cli_compatibility.rs b/src-tauri/tests/cli_compatibility.rs index 39a9ba6..49db49d 100644 --- a/src-tauri/tests/cli_compatibility.rs +++ b/src-tauri/tests/cli_compatibility.rs @@ -1,38 +1,59 @@ use applecontainerdesktop_lib::{builder, cli::run_cmd, containers::*, recreate}; use serde_json::{json, Value}; +/// CLI versions with captured fixtures and a passing live smoke test. +const SUPPORTED_VERSIONS: [&str; 2] = ["1.4.1", "1.5.0"]; + #[test] -fn recreation_preserves_captured_1_4_1_configuration() { - let raw: Value = serde_json::from_str(include_str!( - "../../src/test/fixtures/container-1.4.1/containers.json" - )) - .unwrap(); - let plan = recreate::build_run_args(recreate::config_of(&raw).unwrap(), &json!({"cpus": "2"})); - for pair in [ - ["--cpus", "2"], - ["--memory", "536870912"], - ["--tmpfs", "/scratch"], - ["-v", "/tmp/acd-compat/bind:/bind:ro"], - ["-v", "compat-test:/data"], - ["-p", "127.0.0.1:18741:8080"], - ["-e", "COMPAT_TEST=preserved"], - ["-l", "compat.version=1.4.1"], - ["--network", "compat-test,mtu=1280"], - ["--entrypoint", "/bin/sh"], - ["-w", "/tmp"], +fn recreation_preserves_captured_configuration() { + for (version, payload) in [ + ( + "1.4.1", + include_str!("../../src/test/fixtures/container-1.4.1/containers.json"), + ), + ( + "1.5.0", + include_str!("../../src/test/fixtures/container-1.5.0/containers.json"), + ), ] { - assert!(plan.args.windows(2).any(|w| w == pair), "missing {pair:?}"); + let raw: Value = serde_json::from_str(payload).unwrap(); + let plan = + recreate::build_run_args(recreate::config_of(&raw).unwrap(), &json!({"cpus": "2"})); + let label = format!("compat.version={version}"); + for pair in [ + ["--cpus", "2"], + ["--memory", "536870912"], + ["--tmpfs", "/scratch"], + ["-v", "/tmp/acd-compat/bind:/bind:ro"], + ["-v", "compat-test:/data"], + ["-p", "127.0.0.1:18741:8080"], + ["-e", "COMPAT_TEST=preserved"], + ["-l", &label], + ["--network", "compat-test,mtu=1280"], + ["--entrypoint", "/bin/sh"], + ["-w", "/tmp"], + ] { + assert!( + plan.args.windows(2).any(|w| w == pair), + "{version}: missing {pair:?}" + ); + } + assert!(plan.unsupported.is_empty(), "{version}"); } - assert!(plan.unsupported.is_empty()); } -/// Opt-in: needs a running container 1.4.1 service and Apple silicon. Creates -/// only uniquely named disposable resources; never prunes existing resources. +/// Opt-in: needs a running container service at a supported version and Apple +/// silicon. Creates only uniquely named disposable resources; never prunes +/// existing resources. #[test] -#[ignore = "requires a live container service; see docs/container-1.4.1.md"] -fn live_1_4_1_smoke() { +#[ignore = "requires a live container service; see docs/container-1.5.0.md"] +fn live_smoke() { let version = run_cmd(&["--version"]).unwrap(); - assert!(version.as_str().unwrap().contains("version 1.4.1 ")); + let version = SUPPORTED_VERSIONS + .into_iter() + .find(|v| version.as_str().unwrap().contains(&format!("version {v} "))) + .unwrap_or_else(|| panic!("unsupported CLI: {version}")); + eprintln!("Testing container {version}"); assert_eq!(check_system_status().unwrap()["status"], "running"); let name = format!( "acd-smoke-{}", @@ -84,7 +105,7 @@ fn live_1_4_1_smoke() { "-e", "COMPAT_TEST=preserved", "-l", - "compat.version=1.4.1", + &format!("compat.version={version}"), "-w", "/tmp", "--entrypoint", @@ -199,7 +220,7 @@ fn live_1_4_1_smoke() { .unwrap(); assert_eq!(vm["cpus"], 2); assert_eq!(vm["memory"], 2147483648_u64); - // 1.4.1 can retain an uninitialized snapshot after first-time user setup. + // 1.4.1 and 1.5.0 can retain an uninitialized snapshot after first-time user setup. // A fresh boot loads the persisted setup state. Without this, machine run // may try to initialize with a TTY despite a noninteractive command. // Keep this upstream limitation explicit; never replay user commands. diff --git a/src/test/compatibility.test.ts b/src/test/compatibility.test.ts index a2cf403..dd77e8a 100644 --- a/src/test/compatibility.test.ts +++ b/src/test/compatibility.test.ts @@ -1,51 +1,54 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { invoke } from "@tauri-apps/api/core"; import { checkSystemStatus, listContainers, listImages, listMachines, listNetworks, listVolumes } from "../api"; -import containers from "./fixtures/container-1.4.1/containers.json"; -import images from "./fixtures/container-1.4.1/images.json"; -import machines from "./fixtures/container-1.4.1/machines.json"; -import networks from "./fixtures/container-1.4.1/networks.json"; -import volumes from "./fixtures/container-1.4.1/volumes.json"; -import status from "./fixtures/container-1.4.1/system-running.json"; const mock = vi.mocked(invoke); -describe("captured container 1.4.1 JSON", () => { +// Each captured CLI version lives in fixtures/container-/. +const fixtures = import.meta.glob("./fixtures/container-*/*.json", { eager: true, import: "default" }); +const versions = [...new Set(Object.keys(fixtures).map(path => path.split("/")[2].slice("container-".length)))]; +const fixture = (version: string, name: string) => fixtures[`./fixtures/container-${version}/${name}.json`]; + +it("covers every supported CLI version", () => { + expect(versions).toEqual(expect.arrayContaining(["1.4.1", "1.5.0"])); +}); + +describe.each(versions)("captured container %s JSON", version => { beforeEach(() => vi.clearAllMocks()); it("reads container state and published ports", async () => { - mock.mockResolvedValue(containers); + mock.mockResolvedValue(fixture(version, "containers")); expect((await listContainers())[0]).toMatchObject({ id: "compat-test", status: "running", image: "docker.io/library/alpine:latest", ports: "18741→8080", }); }); it("reads native image size and reference", async () => { - mock.mockResolvedValue(images); + mock.mockResolvedValue(fixture(version, "images")); expect((await listImages())[0]).toMatchObject({ reference: "docker.io/library/alpine:latest", size: "4 MB" }); }); it("reads flat machine resource fields", async () => { - mock.mockResolvedValue(machines); + mock.mockResolvedValue(fixture(version, "machines")); expect((await listMachines())[0]).toMatchObject({ name: "compat-test-vm", cpus: 2, memoryMB: 2048, isDefault: true, status: "running", }); }); it("reads volume capacity and source", async () => { - mock.mockResolvedValue(volumes); + mock.mockResolvedValue(fixture(version, "volumes")); expect((await listVolumes())[0]).toMatchObject({ name: "compat-test", provisioned: "1.1 GB", format: "ext4" }); }); it("reads network configuration and addressing", async () => { - mock.mockResolvedValue(networks); + mock.mockResolvedValue(fixture(version, "networks")); expect((await listNetworks()).find(n => n.name === "default")).toMatchObject({ mode: "nat", subnet: "192.168.64.0/24", gateway: "192.168.64.1", }); }); it("accepts the expanded system status payload", async () => { - mock.mockResolvedValue(status); + mock.mockResolvedValue(fixture(version, "system-running")); expect((await checkSystemStatus()).status).toBe("running"); }); }); diff --git a/src/test/fixtures/container-1.5.0/builder.json b/src/test/fixtures/container-1.5.0/builder.json new file mode 100644 index 0000000..7fc0522 --- /dev/null +++ b/src/test/fixtures/container-1.5.0/builder.json @@ -0,0 +1,100 @@ +[ + { + "configuration": { + "capAdd": [ + "ALL" + ], + "capDrop": [], + "creationDate": "2026-09-29T03:30:41Z", + "dns": { + "nameservers": [], + "options": [], + "searchDomains": [] + }, + "id": "buildkit", + "image": { + "descriptor": { + "digest": "sha256:b5b3f7fa81e662db6929f1ad66d835d151a1b03f682cfe5f9fcb17fa46d6bcc9", + "mediaType": "application/vnd.oci.image.index.v1+json", + "size": 856 + }, + "reference": "ghcr.io/apple/container-builder-shim/builder:0.13.1" + }, + "initProcess": { + "arguments": [ + "--debug", + "--vsock" + ], + "environment": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "BUILDKIT_SETUP_CGROUPV2_ROOT=1" + ], + "executable": "/usr/local/bin/container-builder-shim", + "rlimits": [], + "supplementalGroups": [], + "terminal": false, + "user": { + "id": { + "gid": 0, + "uid": 0 + } + }, + "workingDirectory": "/" + }, + "labels": { + "com.apple.container.plugin": "builder", + "com.apple.container.resource.role": "builder" + }, + "mounts": [ + { + "destination": "/run", + "options": [], + "source": "", + "type": { + "tmpfs": {} + } + }, + { + "destination": "/var/lib/container-builder-shim/exports", + "options": [], + "source": "/Users/test/Library/Application Support/com.apple.container/builder/", + "type": { + "virtiofs": {} + } + } + ], + "networks": [ + { + "network": "default", + "options": { + "hostname": "buildkit" + } + } + ], + "platform": { + "architecture": "arm64", + "os": "linux", + "variant": "v8" + }, + "publishedPorts": [], + "publishedSockets": [], + "readOnly": false, + "resources": { + "cpuOverhead": 1, + "cpus": 2, + "memoryInBytes": 2147483648 + }, + "rosetta": true, + "runtimeHandler": "container-runtime-linux", + "ssh": false, + "sysctls": {}, + "useInit": false, + "virtualization": false + }, + "id": "buildkit", + "status": { + "networks": [], + "state": "stopped" + } + } +] diff --git a/src/test/fixtures/container-1.5.0/containers.json b/src/test/fixtures/container-1.5.0/containers.json new file mode 100644 index 0000000..cf51e8d --- /dev/null +++ b/src/test/fixtures/container-1.5.0/containers.json @@ -0,0 +1,136 @@ +[ + { + "configuration": { + "capAdd": [], + "capDrop": [], + "creationDate": "2026-09-30T22:56:20Z", + "dns": { + "nameservers": [], + "options": [], + "searchDomains": [] + }, + "id": "compat-test", + "image": { + "descriptor": { + "digest": "sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b", + "mediaType": "application/vnd.oci.image.index.v1+json", + "size": 9218 + }, + "reference": "docker.io/library/alpine:latest" + }, + "initProcess": { + "arguments": [ + "-c", + "echo ready; sleep 3600" + ], + "environment": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "COMPAT_TEST=preserved" + ], + "executable": "/bin/sh", + "rlimits": [], + "supplementalGroups": [], + "terminal": false, + "user": { + "id": { + "gid": 0, + "uid": 0 + } + }, + "workingDirectory": "/tmp" + }, + "labels": { + "compat.version": "1.5.0" + }, + "mounts": [ + { + "destination": "/scratch", + "options": [], + "source": "tmpfs", + "type": { + "tmpfs": {} + } + }, + { + "destination": "/bind", + "options": [ + "ro" + ], + "source": "/tmp/acd-compat/bind", + "type": { + "virtiofs": {} + } + }, + { + "destination": "/data", + "options": [], + "source": "/Users/test/Library/Application Support/com.apple.container/volumes/compat-test/volume.img", + "type": { + "volume": { + "cache": { + "on": {} + }, + "format": "ext4", + "name": "compat-test", + "sync": { + "fsync": {} + } + } + } + } + ], + "networks": [ + { + "network": "compat-test", + "options": { + "hostname": "compat-test", + "mtu": 1280 + } + } + ], + "platform": { + "architecture": "arm64", + "os": "linux" + }, + "publishedPorts": [ + { + "containerPort": 8080, + "count": 1, + "hostAddress": "127.0.0.1", + "hostPort": 18741, + "proto": "tcp" + } + ], + "publishedSockets": [], + "readOnly": false, + "resources": { + "cpuOverhead": 1, + "cpus": 1, + "memoryInBytes": 536870912 + }, + "rosetta": false, + "runtimeHandler": "container-runtime-linux", + "ssh": false, + "sysctls": {}, + "useInit": false, + "virtualization": false + }, + "id": "compat-test", + "status": { + "networks": [ + { + "hostname": "compat-test", + "ipv4Address": "192.168.65.2/24", + "ipv4Gateway": "192.168.65.1", + "ipv6Address": "fd53:4374:163a:7204:f4b7:5aff:fec2:c241/64", + "macAddress": "f6:b7:5a:c2:c2:41", + "mtu": 1280, + "network": "compat-test", + "variant": "reserved" + } + ], + "startedDate": "2026-09-30T22:56:21Z", + "state": "running" + } + } +] diff --git a/src/test/fixtures/container-1.5.0/images.json b/src/test/fixtures/container-1.5.0/images.json new file mode 100644 index 0000000..9e331a4 --- /dev/null +++ b/src/test/fixtures/container-1.5.0/images.json @@ -0,0 +1,510 @@ +[ + { + "configuration": { + "creationDate": "2026-06-16T00:00:15Z", + "descriptor": { + "digest": "sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b", + "mediaType": "application/vnd.oci.image.index.v1+json", + "size": 9218 + }, + "name": "docker.io/library/alpine:latest" + }, + "id": "28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b", + "variants": [ + { + "config": { + "architecture": "amd64", + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "WorkingDir": "/" + }, + "created": "2026-06-16T00:01:29.967161902Z", + "history": [ + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:01:29.967161902Z", + "created_by": "ADD alpine-minirootfs-3.24.1-x86_64.tar.gz / # buildkit" + }, + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:01:29.967161902Z", + "created_by": "CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "diff_ids": [ + "sha256:34884abbe92863fce933ed7c39c0e045631af0ed86d5cc0dfbdf9fdca426ce3c" + ], + "type": "layers" + } + }, + "digest": "sha256:79ff19e9084a00eece421b2523fb93e22d730e2c0e525905de047e848e56d95f", + "platform": { + "architecture": "amd64", + "os": "linux" + }, + "size": 3848024 + }, + { + "config": { + "architecture": "unknown", + "config": {}, + "os": "unknown", + "rootfs": { + "diff_ids": [ + "sha256:f5124fb579e27b9769a8ce0158cb650168246572098b5095ecbf8e605488afb6", + "sha256:56dceff11b3396976a334a95c6a2816e051a060123c719b35e4fc5104a962f7f" + ], + "type": "layers" + } + }, + "digest": "sha256:f21951a6120df0f5f9329311202f7869e7b70120f4748632d58753cff662b126", + "platform": { + "architecture": "unknown", + "os": "unknown" + }, + "size": 86390 + }, + { + "config": { + "architecture": "arm", + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "WorkingDir": "/" + }, + "created": "2026-06-16T00:00:25.329026823Z", + "history": [ + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:00:25.329026823Z", + "created_by": "ADD alpine-minirootfs-3.24.1-armhf.tar.gz / # buildkit" + }, + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:00:25.329026823Z", + "created_by": "CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "diff_ids": [ + "sha256:78ed6782dc1bda42a805a54b6a8cf65a497ca4e409bc5bfe00e38ec85bdf826d" + ], + "type": "layers" + }, + "variant": "v6" + }, + "digest": "sha256:bc301c70d7e7b929e5d3251d08ecc2e2894c22580958f79298052ed73ada4be0", + "platform": { + "architecture": "arm", + "os": "linux", + "variant": "v6" + }, + "size": 3555096 + }, + { + "config": { + "architecture": "unknown", + "config": {}, + "os": "unknown", + "rootfs": { + "diff_ids": [ + "sha256:f5124fb579e27b9769a8ce0158cb650168246572098b5095ecbf8e605488afb6", + "sha256:56dceff11b3396976a334a95c6a2816e051a060123c719b35e4fc5104a962f7f" + ], + "type": "layers" + } + }, + "digest": "sha256:9ebfa72c35ca370f63e20c92dfa3ac11b67f753b193cc345aa2b23f89241227c", + "platform": { + "architecture": "unknown", + "os": "unknown" + }, + "size": 86118 + }, + { + "config": { + "architecture": "arm", + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "WorkingDir": "/" + }, + "created": "2026-06-16T00:00:26.526765088Z", + "history": [ + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:00:26.526765088Z", + "created_by": "ADD alpine-minirootfs-3.24.1-armv7.tar.gz / # buildkit" + }, + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:00:26.526765088Z", + "created_by": "CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "diff_ids": [ + "sha256:1e30d7d82ee088ef08d915b4394ec1aa51a54d871dcd0770dc7503bee836e489" + ], + "type": "layers" + }, + "variant": "v7" + }, + "digest": "sha256:48bf253520b161ff0f7bd9c6b2aded4126fa8ee2bc29386580b2a8a0322a1742", + "platform": { + "architecture": "arm", + "os": "linux", + "variant": "v7" + }, + "size": 3262261 + }, + { + "config": { + "architecture": "unknown", + "config": {}, + "os": "unknown", + "rootfs": { + "diff_ids": [ + "sha256:f5124fb579e27b9769a8ce0158cb650168246572098b5095ecbf8e605488afb6", + "sha256:56dceff11b3396976a334a95c6a2816e051a060123c719b35e4fc5104a962f7f" + ], + "type": "layers" + } + }, + "digest": "sha256:b47a7a1760540077a1aefc00795604cd7e1b2acf556312118e10806f9a58d072", + "platform": { + "architecture": "unknown", + "os": "unknown" + }, + "size": 86390 + }, + { + "config": { + "architecture": "arm64", + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "WorkingDir": "/" + }, + "created": "2026-06-16T00:01:20.474100947Z", + "history": [ + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:01:20.474100947Z", + "created_by": "ADD alpine-minirootfs-3.24.1-aarch64.tar.gz / # buildkit" + }, + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:01:20.474100947Z", + "created_by": "CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "diff_ids": [ + "sha256:b2848c02ac6ff53d265469b5b30f649f335e546a83330cd8916d54e65e640409" + ], + "type": "layers" + }, + "variant": "v8" + }, + "digest": "sha256:e7a1a92a5bfeee40966aea60f0796b0e7917cc35591542701834f03a68fa3d18", + "platform": { + "architecture": "arm64", + "os": "linux", + "variant": "v8" + }, + "size": 4184689 + }, + { + "config": { + "architecture": "unknown", + "config": {}, + "os": "unknown", + "rootfs": { + "diff_ids": [ + "sha256:f5124fb579e27b9769a8ce0158cb650168246572098b5095ecbf8e605488afb6", + "sha256:56dceff11b3396976a334a95c6a2816e051a060123c719b35e4fc5104a962f7f" + ], + "type": "layers" + } + }, + "digest": "sha256:d9dc32c63a23ac682a41ab2eae01051d2a4fbe472eefd109faf97be63a5216e5", + "platform": { + "architecture": "unknown", + "os": "unknown" + }, + "size": 86390 + }, + { + "config": { + "architecture": "386", + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "WorkingDir": "/" + }, + "created": "2026-06-16T00:01:19.360099979Z", + "history": [ + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:01:19.360099979Z", + "created_by": "ADD alpine-minirootfs-3.24.1-x86.tar.gz / # buildkit" + }, + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:01:19.360099979Z", + "created_by": "CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "diff_ids": [ + "sha256:20868c90c269dd93125a9ddea2bec8b326ccfc8229d85a7456ee028a165b29b1" + ], + "type": "layers" + } + }, + "digest": "sha256:6f5908cdf811d574b30ec394e405ef74ee293bed5af1620a5187d604604a90a8", + "platform": { + "architecture": "386", + "os": "linux" + }, + "size": 3671765 + }, + { + "config": { + "architecture": "unknown", + "config": {}, + "os": "unknown", + "rootfs": { + "diff_ids": [ + "sha256:f5124fb579e27b9769a8ce0158cb650168246572098b5095ecbf8e605488afb6", + "sha256:56dceff11b3396976a334a95c6a2816e051a060123c719b35e4fc5104a962f7f" + ], + "type": "layers" + } + }, + "digest": "sha256:bf1792d3b17c3493e465c13b8357c5f60c2acc36cb69aab0813d4e7f1995ac2b", + "platform": { + "architecture": "unknown", + "os": "unknown" + }, + "size": 86390 + }, + { + "config": { + "architecture": "ppc64le", + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "WorkingDir": "/" + }, + "created": "2026-06-16T00:00:15.017984356Z", + "history": [ + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:00:15.017984356Z", + "created_by": "ADD alpine-minirootfs-3.24.1-ppc64le.tar.gz / # buildkit" + }, + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:00:15.017984356Z", + "created_by": "CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "diff_ids": [ + "sha256:8c55296752dfcaf17e3fa825036cfe5050c55402025b508314048e741c9a5f16" + ], + "type": "layers" + } + }, + "digest": "sha256:a30366c2d2645b131e92b797b39357db29f197966c660db2767f143353000d6f", + "platform": { + "architecture": "ppc64le", + "os": "linux" + }, + "size": 3815039 + }, + { + "config": { + "architecture": "unknown", + "config": {}, + "os": "unknown", + "rootfs": { + "diff_ids": [ + "sha256:f5124fb579e27b9769a8ce0158cb650168246572098b5095ecbf8e605488afb6", + "sha256:56dceff11b3396976a334a95c6a2816e051a060123c719b35e4fc5104a962f7f" + ], + "type": "layers" + } + }, + "digest": "sha256:7a21929832908e662272c6e3333f68c662e3854ad4956e49d995251dcd9ccbfe", + "platform": { + "architecture": "unknown", + "os": "unknown" + }, + "size": 86390 + }, + { + "config": { + "architecture": "riscv64", + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "WorkingDir": "/" + }, + "created": "2026-06-16T05:59:15.56408535Z", + "history": [ + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T05:59:15.56408535Z", + "created_by": "ADD alpine-minirootfs-3.24.1-riscv64.tar.gz / # buildkit" + }, + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T05:59:15.56408535Z", + "created_by": "CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "diff_ids": [ + "sha256:d806e99d43aeb11f79a974b2623809f37ead271ba59a954bd643342d3ae87e71" + ], + "type": "layers" + } + }, + "digest": "sha256:20a26477b54fb521bc8f633ea0af1f8f9b3dac5661739f857b381b117226919b", + "platform": { + "architecture": "riscv64", + "os": "linux" + }, + "size": 3575994 + }, + { + "config": { + "architecture": "unknown", + "config": {}, + "os": "unknown", + "rootfs": { + "diff_ids": [ + "sha256:f5124fb579e27b9769a8ce0158cb650168246572098b5095ecbf8e605488afb6", + "sha256:56dceff11b3396976a334a95c6a2816e051a060123c719b35e4fc5104a962f7f" + ], + "type": "layers" + } + }, + "digest": "sha256:83b3fe59c5ee68a257a6e179c792b9bd5b0aceb02ceb2813f14eef469a020c00", + "platform": { + "architecture": "unknown", + "os": "unknown" + }, + "size": 86390 + }, + { + "config": { + "architecture": "s390x", + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "WorkingDir": "/" + }, + "created": "2026-06-16T00:00:21.879382071Z", + "history": [ + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:00:21.879382071Z", + "created_by": "ADD alpine-minirootfs-3.24.1-s390x.tar.gz / # buildkit" + }, + { + "comment": "buildkit.dockerfile.v0", + "created": "2026-06-16T00:00:21.879382071Z", + "created_by": "CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "diff_ids": [ + "sha256:ff89eb082f59748c49390592d343caad75d5a0f38be634c4a5ff87b55add7c8d" + ], + "type": "layers" + } + }, + "digest": "sha256:4eea4e45f63fc6b38e5f653120aaf8a7c19043706f6d76070821495371e3017b", + "platform": { + "architecture": "s390x", + "os": "linux" + }, + "size": 3710951 + }, + { + "config": { + "architecture": "unknown", + "config": {}, + "os": "unknown", + "rootfs": { + "diff_ids": [ + "sha256:f5124fb579e27b9769a8ce0158cb650168246572098b5095ecbf8e605488afb6", + "sha256:56dceff11b3396976a334a95c6a2816e051a060123c719b35e4fc5104a962f7f" + ], + "type": "layers" + } + }, + "digest": "sha256:4a411cfbe9fc8bbe39859f835711f280d91a856a28a6ca01ef82ca298170c727", + "platform": { + "architecture": "unknown", + "os": "unknown" + }, + "size": 86390 + } + ] + } +] diff --git a/src/test/fixtures/container-1.5.0/machines.json b/src/test/fixtures/container-1.5.0/machines.json new file mode 100644 index 0000000..1a55a42 --- /dev/null +++ b/src/test/fixtures/container-1.5.0/machines.json @@ -0,0 +1,12 @@ +[ + { + "status": "running", + "id": "compat-test-vm", + "createdDate": "2026-09-30T22:56:30Z", + "ipAddress": "192.168.64.2", + "default": true, + "cpus": 2, + "memory": 2147483648, + "diskSize": 1169326080 + } +] diff --git a/src/test/fixtures/container-1.5.0/networks.json b/src/test/fixtures/container-1.5.0/networks.json new file mode 100644 index 0000000..486ffe0 --- /dev/null +++ b/src/test/fixtures/container-1.5.0/networks.json @@ -0,0 +1,36 @@ +[ + { + "configuration": { + "creationDate": "2026-09-30T22:56:20Z", + "labels": {}, + "mode": "nat", + "name": "compat-test", + "options": {}, + "plugin": "container-network-vmnet" + }, + "id": "compat-test", + "status": { + "ipv4Gateway": "192.168.65.1", + "ipv4Subnet": "192.168.65.0/24", + "ipv6Subnet": "fd53:4374:163a:7204::/64" + } + }, + { + "configuration": { + "creationDate": "2026-09-29T15:54:07Z", + "labels": { + "com.apple.container.resource.role": "builtin" + }, + "mode": "nat", + "name": "default", + "options": {}, + "plugin": "container-network-vmnet" + }, + "id": "default", + "status": { + "ipv4Gateway": "192.168.64.1", + "ipv4Subnet": "192.168.64.0/24", + "ipv6Subnet": "fd84:d933:bb9e:f441::/64" + } + } +] diff --git a/src/test/fixtures/container-1.5.0/stats.json b/src/test/fixtures/container-1.5.0/stats.json new file mode 100644 index 0000000..7b8d893 --- /dev/null +++ b/src/test/fixtures/container-1.5.0/stats.json @@ -0,0 +1,13 @@ +[ + { + "blockReadBytes": 4113408, + "blockWriteBytes": 8192, + "cpuUsageUsec": 2970, + "id": "compat-test", + "memoryLimitBytes": 536870912, + "memoryUsageBytes": 4517888, + "networkRxBytes": 44119, + "networkTxBytes": 602, + "numProcesses": 1 + } +] diff --git a/src/test/fixtures/container-1.5.0/system-running.json b/src/test/fixtures/container-1.5.0/system-running.json new file mode 100644 index 0000000..24edb5d --- /dev/null +++ b/src/test/fixtures/container-1.5.0/system-running.json @@ -0,0 +1,29 @@ +{ + "client": { + "appName": "container", + "build": "release", + "commit": "unspecified", + "version": "1.5.0" + }, + "host": { + "architecture": "arm64", + "cpus": 14, + "operatingSystem": "Version 27.0 (Build 26A428)" + }, + "paths": { + "appRoot": "/Users/test/Library/Application Support/com.apple.container/", + "installRoot": "/opt/homebrew/Cellar/container/1.5.0/" + }, + "resources": { + "containersRunning": 1, + "containersTotal": 6, + "images": 24 + }, + "server": { + "appName": "container-apiserver", + "build": "release", + "commit": "unspecified", + "version": "1.5.0" + }, + "status": "running" +} diff --git a/src/test/fixtures/container-1.5.0/volumes.json b/src/test/fixtures/container-1.5.0/volumes.json new file mode 100644 index 0000000..d506f06 --- /dev/null +++ b/src/test/fixtures/container-1.5.0/volumes.json @@ -0,0 +1,17 @@ +[ + { + "configuration": { + "creationDate": "2026-09-30T22:56:20Z", + "driver": "local", + "format": "ext4", + "labels": {}, + "name": "compat-test", + "options": { + "size": "1G" + }, + "sizeInBytes": 1073741824, + "source": "/Users/test/Library/Application Support/com.apple.container/volumes/compat-test/volume.img" + }, + "id": "compat-test" + } +] diff --git a/src/test/lib/stats.test.ts b/src/test/lib/stats.test.ts index 82a7653..2e826cc 100644 --- a/src/test/lib/stats.test.ts +++ b/src/test/lib/stats.test.ts @@ -1,11 +1,15 @@ import { describe, expect, it } from "vitest"; import { displayStats } from "../../lib/stats"; -import stats from "../fixtures/container-1.4.1/stats.json"; +import stats141 from "../fixtures/container-1.4.1/stats.json"; +import stats150 from "../fixtures/container-1.5.0/stats.json"; -describe("1.4.1 resource counters", () => { - it("shows memory from a real sample without inventing a CPU percentage", () => { +describe("1.4.1 and 1.5.0 resource counters", () => { + it.each([ + ["1.4.1", stats141, "3.78 MiB / 512.00 MiB"], + ["1.5.0", stats150, "4.31 MiB / 512.00 MiB"], + ])("shows memory from a real %s sample without inventing a CPU percentage", (_, stats, memory) => { const result = displayStats({ stats: stats[0], time: 1000 }); - expect(result.memory).toBe("3.78 MiB / 512.00 MiB"); + expect(result.memory).toBe(memory); expect(result.cpu).toBeUndefined(); });