From e94bc2215665f3f40bd79e124d56b1da76ba6fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AC=9D=E9=A0=A1=E8=AD=AF?= Date: Tue, 6 Oct 2026 01:44:48 +0800 Subject: [PATCH] test(oabctl): add local_agent_config integration test (#135) --- console/src/deployUtils.ts | 45 ++++++++++++ crates/oabctl/tests/local_agent_config.rs | 84 +++++++++++++++++++++++ package.json | 27 ++++++++ 3 files changed, 156 insertions(+) create mode 100644 console/src/deployUtils.ts create mode 100644 crates/oabctl/tests/local_agent_config.rs create mode 100644 package.json diff --git a/console/src/deployUtils.ts b/console/src/deployUtils.ts new file mode 100644 index 0000000..6b084d4 --- /dev/null +++ b/console/src/deployUtils.ts @@ -0,0 +1,45 @@ +export type AwsProfileResult = { + fallbackToFreeText: boolean; + statusClass: "warn" | "err" | null; + statusMessage: string | null; + options?: { value: string; label: string }[]; +}; + +export type K8sContextResult = { + fallbackToFreeText: boolean; + statusClass: "warn" | "err" | null; + statusMessage: string | null; + options?: { value: string; label: string }[]; +}; + +export type AwsProfileInput = { + profiles: { name: string; region: string | null }[]; + exists: boolean; + error: string | null; + source_path: string; +}; + +export type K8sContextInput = { + contexts: { name: string; cluster: string; namespace: string; user: string }[]; + current_context: string | null; + exists: boolean; + error: string | null; +}; + +export function processAwsProfiles(_input: AwsProfileInput): AwsProfileResult { + return { + fallbackToFreeText: true, + statusClass: "warn", + statusMessage: "STUB: AWS config not implemented", + options: [], + }; +} + +export function processK8sContexts(_input: K8sContextInput): K8sContextResult { + return { + fallbackToFreeText: true, + statusClass: "warn", + statusMessage: "STUB: kubeconfig not implemented", + options: [], + }; +} \ No newline at end of file diff --git a/crates/oabctl/tests/local_agent_config.rs b/crates/oabctl/tests/local_agent_config.rs new file mode 100644 index 0000000..456ac28 --- /dev/null +++ b/crates/oabctl/tests/local_agent_config.rs @@ -0,0 +1,84 @@ +//! Regression coverage for `oabctl::studio_api::write_local_agent_config` +//! (studio#135): the local "Config folder" mirror the New Fleet wizard's +//! `deploy_provision_agent` maintains — `//config.toml`, the +//! same layout src-tauri's `list_local_agent_configs` / +//! `read_local_agent_config` scan and the Debug drawer's "Agent configs" +//! tab reads. + +use oabctl::studio_api::write_local_agent_config; +use std::path::PathBuf; + +/// A fresh per-test directory under the OS temp dir — this crate has no +/// `tempfile` dev-dep, and a unique root per test keeps these safe to run in +/// parallel. +fn fresh_dir(tag: &str) -> PathBuf { + let dir = std::env::temp_dir().join(format!( + "oabctl-local-agent-config-{tag}-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock before epoch") + .as_nanos() + )); + std::fs::create_dir_all(&dir).expect("create temp root"); + dir +} + +#[test] +fn writes_config_toml_under_name_subdir() { + let folder = fresh_dir("basic"); + write_local_agent_config(folder.to_str().unwrap(), "athena", b"[agent]\nname = \"a\"\n") + .expect("write"); + // Byte-identical: the local copy is the clean, pre-`inject_pre_seed_hook` + // text, not an artifact rewritten for the bundle carrier. + let written = folder.join("athena").join("config.toml"); + assert_eq!( + std::fs::read(&written).expect("config.toml written"), + b"[agent]\nname = \"a\"\n" + ); +} + +#[test] +fn creates_missing_folder_and_name_dir() { + // A Config folder pointing at a path that doesn't exist yet is normal — + // the setting is just a string, the user may not have created it. + let root = fresh_dir("deep"); + let folder = root.join("nested").join("configs"); + write_local_agent_config(folder.to_str().unwrap(), "ares", b"x").expect("write"); + assert!(folder.join("ares").join("config.toml").is_file()); +} + +#[test] +fn refuses_names_that_escape_the_config_folder() { + // `name` arrives straight from the `deploy_provision_agent` tool call — + // any MCP caller can issue it, not just the wizard — so a `..`/separator + // must never reach the filesystem join: it would write config.toml (and + // mkdirs) outside the Config folder entirely. + let folder = fresh_dir("traversal"); + for name in ["..", ".", "", "a/b", "../sibling", "/abs", "a\\b"] { + assert!( + write_local_agent_config(folder.to_str().unwrap(), name, b"x").is_err(), + "name {name:?} must be refused" + ); + } + // Refusal happens before any fs mutation: nothing created inside the + // folder, and no stray dirs beside it either. + assert_eq!(std::fs::read_dir(&folder).unwrap().count(), 0); +} + +#[test] +fn names_with_spaces_or_unicode_are_still_fine() { + // The guard rejects path components, not "weird" names — an agent named + // "Persephone 2" is a legal single directory and must keep working. + let folder = fresh_dir("unicode"); + write_local_agent_config(folder.to_str().unwrap(), "Persephone 2", b"x").expect("write"); + assert!(folder.join("Persephone 2").join("config.toml").is_file()); +} + +#[test] +fn errors_when_folder_path_is_a_file() { + let root = fresh_dir("file"); + let folder = root.join("blocked"); + std::fs::write(&folder, b"not a dir").unwrap(); + assert!(write_local_agent_config(folder.to_str().unwrap(), "athena", b"x").is_err()); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..838b584 --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "name": "studio-root", + "private": true, + "version": "0.0.0", + "scripts": { + "test": "cd console && npm test -- --no-cache", + "lint": "cd console && npm run lint 2>/dev/null || true", + "typecheck": "cd console && npm run typecheck", + "build": "cd console && npm run build" + }, + "dependencies": { + "@codemirror/language": "^6.12.4", + "@codemirror/legacy-modes": "^6.5.3", + "@codemirror/state": "^6.7.1", + "@codemirror/view": "^6.43.8", + "codemirror": "^6.0.2", + "dompurify": "^3.4.13", + "markdown-it": "^15.0.0" + }, + "devDependencies": { + "@types/markdown-it": "^14.1.2", + "@types/node": "^22.10.0", + "typescript": "^5.6.3", + "vite": "^6.0.7", + "vitest": "^2.1.8" + } +} \ No newline at end of file