fabro/crates/arc-devcontainer/tests/integration.rs
Bryan Helmkamp 5e337b4cb3 Add arc-devcontainer crate for parsing and resolving devcontainer.json
Standalone crate that reads devcontainer.json (with JSONC support), fetches
OCI Features via oras, and produces a resolved config containing a generated
Dockerfile, lifecycle hooks, environment variables, and forwarded ports.

Supports image, Dockerfile, and Docker Compose modes with devcontainer
variable substitution. No coupling to arc-workflows or DaytonaSandbox.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 22:38:12 -05:00

102 lines
3.6 KiB
Rust

use arc_devcontainer::{Command, DevcontainerResolver};
use std::path::PathBuf;
fn fixture_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(name)
}
#[tokio::test]
async fn resolve_image_only() {
let config = DevcontainerResolver::resolve(&fixture_path("image-only"))
.await
.unwrap();
assert!(config.dockerfile.contains("FROM mcr.microsoft.com/devcontainers/base:ubuntu"));
assert_eq!(config.remote_user.as_deref(), Some("vscode"));
assert_eq!(config.forwarded_ports, vec![3000, 8080]);
assert_eq!(config.environment.get("EDITOR").map(String::as_str), Some("code"));
assert_eq!(config.workspace_folder, "/workspaces/image-only");
assert!(config.compose_file.is_none());
assert!(config.compose_service.is_none());
assert_eq!(config.post_create_commands.len(), 1);
assert!(matches!(&config.post_create_commands[0], Command::Shell(s) if s == "echo hello"));
}
#[tokio::test]
async fn resolve_dockerfile_mode() {
let config = DevcontainerResolver::resolve(&fixture_path("dockerfile-mode"))
.await
.unwrap();
// Should read the actual Dockerfile content
assert!(config.dockerfile.contains("FROM node:20"));
assert!(config.dockerfile.contains("apt-get update"));
assert_eq!(config.remote_user.as_deref(), Some("developer"));
assert_eq!(config.forwarded_ports, vec![4000]);
assert_eq!(config.post_create_commands.len(), 1);
assert!(matches!(&config.post_create_commands[0], Command::Shell(s) if s == "npm install"));
}
#[tokio::test]
async fn resolve_compose_mode() {
let config = DevcontainerResolver::resolve(&fixture_path("compose-mode"))
.await
.unwrap();
// In compose mode, the dockerfile is derived from the compose service's image
assert!(config.dockerfile.contains("FROM node:20"));
assert_eq!(config.workspace_folder, "/workspace");
assert_eq!(config.remote_user.as_deref(), Some("node"));
assert!(config.compose_file.is_some());
assert_eq!(config.compose_service.as_deref(), Some("app"));
// Ports come from compose + remoteEnv merged
assert_eq!(config.forwarded_ports, vec![3000, 9229]);
// Environment merged from compose + remoteEnv
assert_eq!(config.environment.get("NODE_ENV").map(String::as_str), Some("development"));
assert_eq!(config.environment.get("DEBUG").map(String::as_str), Some("true"));
}
#[tokio::test]
async fn resolve_variables() {
let config = DevcontainerResolver::resolve(&fixture_path("variables"))
.await
.unwrap();
assert_eq!(config.workspace_folder, "/workspaces/variables");
assert_eq!(
config.environment.get("PROJECT_ROOT").map(String::as_str),
Some("/workspaces/variables")
);
assert_eq!(
config.environment.get("PROJECT_NAME").map(String::as_str),
Some("variables")
);
}
#[tokio::test]
async fn resolve_not_found() {
let result = DevcontainerResolver::resolve(&fixture_path("nonexistent")).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("no devcontainer.json found"));
}
#[tokio::test]
async fn generated_dockerfile_is_well_formed() {
let config = DevcontainerResolver::resolve(&fixture_path("image-only"))
.await
.unwrap();
// Should start with the generated header
assert!(config.dockerfile.contains("# Generated by arc-devcontainer"));
// Should have the base image
assert!(config.dockerfile.contains("FROM"));
// Should end with a newline
assert!(config.dockerfile.ends_with('\n'));
}