feat: use builtin profile by default

This commit is contained in:
2026-05-30 03:57:40 +09:00
parent ffe1785262
commit 625730cb0a
8 changed files with 315 additions and 283 deletions
+88 -1
View File
@@ -13,6 +13,7 @@ use serde::{Deserialize, Serialize};
use crate::{PodManifest, PodManifestConfig, ResolveError, paths};
const PROFILE_FORMAT_V1: &str = "insomnia.nix-profile.v1";
const BUILTIN_DEFAULT_PROFILE_NAME: &str = "default";
/// Registry source for discovered profiles.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
@@ -221,6 +222,24 @@ impl ProfileRegistry {
self.default = Some(default);
}
fn set_builtin_default_if_available(&mut self) {
if self.default.is_some() {
return;
}
if self
.select_named(
Some(ProfileRegistrySource::Builtin),
BUILTIN_DEFAULT_PROFILE_NAME,
)
.is_ok()
{
self.default = Some(ProfileDefault {
source: Some(ProfileRegistrySource::Builtin),
name: BUILTIN_DEFAULT_PROFILE_NAME.to_string(),
});
}
}
fn mark_default_flags(&mut self) {
let Some(default) = self.default.clone() else {
return;
@@ -282,6 +301,7 @@ impl ProfileDiscovery {
if let Some(path) = &self.project_config {
load_profile_registry_file(&mut registry, ProfileRegistrySource::Project, path)?;
}
registry.set_builtin_default_if_available();
registry.mark_default_flags();
Ok(registry)
}
@@ -323,12 +343,14 @@ pub struct ResolvedProfile {
#[derive(Debug, Clone)]
pub struct NixProfileResolver {
nix_bin: PathBuf,
workspace_base: Option<PathBuf>,
}
impl Default for NixProfileResolver {
fn default() -> Self {
Self {
nix_bin: PathBuf::from("nix"),
workspace_base: None,
}
}
}
@@ -341,9 +363,15 @@ impl NixProfileResolver {
pub fn with_nix_bin(nix_bin: impl Into<PathBuf>) -> Self {
Self {
nix_bin: nix_bin.into(),
workspace_base: None,
}
}
pub fn with_workspace_base(mut self, workspace_base: impl Into<PathBuf>) -> Self {
self.workspace_base = Some(workspace_base.into());
self
}
pub fn resolve(&self, selector: &ProfileSelector) -> Result<ResolvedProfile, ProfileError> {
match selector {
ProfileSelector::Path { path } => self.resolve_path(
@@ -351,6 +379,7 @@ impl NixProfileResolver {
ProfileSource::Path {
path: absolutize(path)?,
},
None,
),
ProfileSelector::Named { .. } | ProfileSelector::Default => {
let cwd = std::env::current_dir().map_err(|source| ProfileError::CommandIo {
@@ -359,6 +388,11 @@ impl NixProfileResolver {
})?;
let registry = ProfileDiscovery::for_cwd(&cwd).discover()?;
let entry = registry.select(selector)?.clone();
let artifact_base = if entry.source == ProfileRegistrySource::Builtin {
Some(absolutize(self.workspace_base.as_deref().unwrap_or(&cwd))?)
} else {
None
};
self.resolve_path(
&entry.path,
ProfileSource::Registry {
@@ -366,6 +400,7 @@ impl NixProfileResolver {
name: entry.name,
path: absolutize(&entry.path)?,
},
artifact_base,
)
}
}
@@ -375,15 +410,17 @@ impl NixProfileResolver {
&self,
path: &Path,
source: ProfileSource,
manifest_base_override: Option<PathBuf>,
) -> Result<ResolvedProfile, ProfileError> {
let absolute_path = absolutize(path)?;
let base_dir = absolute_path
let file_base_dir = absolute_path
.parent()
.map(Path::to_path_buf)
.ok_or_else(|| ProfileError::InvalidPath {
path: absolute_path.clone(),
message: "profile path has no parent directory".to_string(),
})?;
let base_dir = manifest_base_override.unwrap_or(file_base_dir);
let output = Command::new(&self.nix_bin)
.arg("eval")
@@ -948,6 +985,56 @@ mod tests {
);
}
#[test]
fn builtin_default_profile_is_registered_as_default() {
let registry = ProfileDiscovery::with_sources(paths::builtin_profiles_dir(), None, None)
.discover()
.unwrap();
let default = registry.default_entry().unwrap();
assert_eq!(default.source, ProfileRegistrySource::Builtin);
assert_eq!(default.name, BUILTIN_DEFAULT_PROFILE_NAME);
assert!(default.is_default);
assert!(default.path.ends_with("resources/nix/profiles/default.nix"));
}
#[cfg(unix)]
#[test]
fn builtin_profile_relative_paths_resolve_against_workspace_base() {
use std::os::unix::fs::PermissionsExt;
let tmp = TempDir::new().unwrap();
let workspace = tmp.path().join("workspace");
std::fs::create_dir_all(&workspace).unwrap();
let profile_path = tmp.path().join("default.nix");
std::fs::write(&profile_path, "{}").unwrap();
let nix_bin = tmp.path().join("fake-nix");
std::fs::write(
&nix_bin,
r#"#!/bin/sh
cat <<'JSON'
{"profile":{"format":"insomnia.nix-profile.v1","name":"default"},"manifest":{"pod":{"name":"default"},"model":{"scheme":"anthropic","model_id":"claude-sonnet-4-20250514"},"scope":{"allow":[{"target":".","permission":"write"}]}}}
JSON
"#,
)
.unwrap();
let mut perms = std::fs::metadata(&nix_bin).unwrap().permissions();
perms.set_mode(0o755);
std::fs::set_permissions(&nix_bin, perms).unwrap();
let resolved = NixProfileResolver::with_nix_bin(&nix_bin)
.resolve_path(
&profile_path,
ProfileSource::Registry {
source: ProfileRegistrySource::Builtin,
name: BUILTIN_DEFAULT_PROFILE_NAME.to_string(),
path: profile_path.clone(),
},
Some(workspace.clone()),
)
.unwrap();
assert_eq!(resolved.manifest.scope.allow[0].target, workspace);
}
#[test]
fn for_cwd_reads_profiles_toml_and_ignores_manifest_profiles() {
let tmp = TempDir::new().unwrap();
+118 -96
View File
@@ -4,13 +4,13 @@ use std::process::ExitCode;
use clap::Parser;
use manifest::{NixProfileResolver, PodManifest, PodManifestConfig, ProfileSelector, paths};
use pod::{Pod, PodController, PodFactory, PromptLoader};
use pod::{Pod, PodController, PromptLoader};
use session_store::{FsStore, PodMetadataStore, SegmentId, Store};
#[derive(Debug, Parser)]
#[command(
name = "insomnia-pod",
about = "Spawn a Pod process from manifest layers or a single manifest file"
about = "Spawn a Pod process from a Nix profile or a single manifest file"
)]
struct Cli {
/// Nix profile to evaluate. Accepts an explicit path, `path:<path>`, a
@@ -29,19 +29,20 @@ struct Cli {
#[arg(long, value_name = "NAME", requires = "profile", conflicts_with_all = ["pod", "session", "adopt"])]
profile_pod_name: Option<String>,
/// Manifest TOML to use directly, without loading user, project, or
/// overlay layers.
/// Manifest TOML to use directly as a one-file compatibility/debug input.
/// This bypasses profile discovery but still applies builtin defaults and
/// the same required-field validation boundary.
#[arg(long, value_name = "PATH", conflicts_with_all = ["project", "overlay"])]
manifest: Option<PathBuf>,
/// Start the project-manifest walk from this directory. When
/// omitted, the factory walks up from the current working
/// directory looking for `.insomnia/manifest.toml`.
/// Deprecated manifest-cascade project root flag. Ambient project/user
/// manifest discovery has been removed; configure/select a profile instead.
#[arg(long, value_name = "PATH")]
project: Option<PathBuf>,
/// Inline TOML string applied as the highest-priority overlay
/// layer. Example: `--overlay 'pod.name = "dbg"'`.
/// Inline TOML override applied to the implicit default profile. This is
/// retained for launchers that need to supply restore/session-time values;
/// it does not re-enable user/project manifest discovery.
#[arg(long, value_name = "TOML")]
overlay: Option<String>,
@@ -94,7 +95,7 @@ fn resolve_manifest_with_user_manifest_env(
fn resolve_manifest_with_user_manifest_env_and_profile_loader<F>(
cli: &Cli,
user_manifest_env: Option<OsString>,
_user_manifest_env: Option<OsString>,
load_profile_fn: F,
) -> Result<(PodManifest, PromptLoader), String>
where
@@ -105,29 +106,31 @@ where
return load_profile_fn(&selector, cli.profile_pod_name.as_deref());
}
let user_manifest = paths::user_manifest_path_from_env(user_manifest_env);
if let Some(path) = &cli.manifest {
if user_manifest.is_some() {
return Err(format!(
"--manifest cannot be used when {} is set",
paths::USER_MANIFEST_ENV
));
}
return load_single_manifest(path, cli.pod.as_deref());
}
let factory = build_factory_with_user_manifest_path(cli, user_manifest)?;
factory
.resolve()
.map_err(|e| format!("failed to resolve manifest cascade: {e}"))
if cli.project.is_some() {
return Err(
"--project is no longer supported; normal startup uses profile discovery/default, \
and --manifest <PATH> is the only one-file manifest mode"
.to_string(),
);
}
let selector = ProfileSelector::Default;
let (manifest, loader) = load_profile_fn(&selector, None)?;
let manifest = apply_cli_overrides_to_manifest(manifest, cli)?;
Ok((manifest, loader))
}
fn load_profile(
selector: &ProfileSelector,
pod_name_override: Option<&str>,
) -> Result<(PodManifest, PromptLoader), String> {
let resolver = NixProfileResolver::new();
let cwd = std::env::current_dir()
.map_err(|e| format!("failed to resolve current directory for profile: {e}"))?;
let resolver = NixProfileResolver::new().with_workspace_base(cwd);
let mut resolved = resolver.resolve(selector).map_err(|e| {
format!(
"failed to resolve profile {}: {e}",
@@ -146,28 +149,32 @@ fn load_single_manifest(
) -> Result<(PodManifest, PromptLoader), String> {
let toml = std::fs::read_to_string(path)
.map_err(|e| format!("failed to read manifest {}: {e}", path.display()))?;
let manifest = match pod_name_override {
Some(pod_name) => match PodManifest::from_toml(&toml) {
Ok(mut manifest) => {
manifest.pod.name = pod_name.to_string();
manifest
}
Err(_) => {
let base = PodManifestConfig::from_toml(&toml)
.map_err(|e| format!("failed to parse manifest {}: {e}", path.display()))?;
let overlay = PodManifestConfig::from_toml(&pod_name_overlay_toml(pod_name))
.expect("pod name overlay TOML is generated");
PodManifest::try_from(base.merge(overlay)).map_err(|e| {
format!(
"failed to resolve manifest {} with --pod: {e}",
path.display()
)
})?
}
},
None => PodManifest::from_toml(&toml)
.map_err(|e| format!("failed to parse manifest {}: {e}", path.display()))?,
let absolute_path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.map_err(|e| format!("failed to resolve current directory: {e}"))?
.join(path)
};
let base_dir = absolute_path.parent().ok_or_else(|| {
format!(
"manifest path {} has no parent directory",
absolute_path.display()
)
})?;
let mut config = PodManifestConfig::builtin_defaults().merge(
PodManifestConfig::from_toml(&toml)
.map_err(|e| format!("failed to parse manifest {}: {e}", path.display()))?
.resolve_paths(base_dir),
);
if let Some(pod_name) = pod_name_override {
config = config.merge(
PodManifestConfig::from_toml(&pod_name_overlay_toml(pod_name))
.expect("pod name overlay TOML is generated"),
);
}
let manifest = PodManifest::try_from(config)
.map_err(|e| format!("failed to resolve manifest {}: {e}", path.display()))?;
Ok((manifest, PromptLoader::builtins_only()))
}
@@ -179,43 +186,33 @@ fn pod_name_overlay_toml(pod_name: &str) -> String {
toml::to_string(&toml::Value::Table(root)).expect("pod name overlay serialisation cannot fail")
}
fn build_factory_with_user_manifest_path(
fn manifest_to_config(manifest: &PodManifest) -> Result<PodManifestConfig, String> {
let value = serde_json::to_value(manifest)
.map_err(|e| format!("failed to serialise resolved manifest for overlay: {e}"))?;
serde_json::from_value(value)
.map_err(|e| format!("failed to convert resolved manifest for overlay: {e}"))
}
fn apply_cli_overrides_to_manifest(
mut manifest: PodManifest,
cli: &Cli,
user_manifest: Option<PathBuf>,
) -> Result<PodFactory, String> {
let mut factory = PodFactory::new();
factory = match user_manifest {
Some(path) => factory
.with_user_manifest(path)
.map_err(|e| format!("failed to load user manifest: {e}"))?,
None => factory
.with_user_manifest_auto()
.map_err(|e| format!("failed to auto-load user manifest: {e}"))?,
};
factory = match &cli.project {
Some(path) => factory
.with_project_manifest_from(path)
.map_err(|e| format!("failed to load project manifest: {e}"))?,
None => factory
.with_project_manifest_auto()
.map_err(|e| format!("failed to auto-load project manifest: {e}"))?,
};
) -> Result<PodManifest, String> {
let profile = manifest.profile.clone();
if let Some(overlay) = cli.overlay.as_deref() {
factory = factory
.with_overlay_toml(overlay)
.map_err(|e| format!("failed to parse overlay TOML: {e}"))?;
let base_dir = std::env::current_dir()
.map_err(|e| format!("failed to resolve current directory for overlay: {e}"))?;
let overlay = PodManifestConfig::from_toml(overlay)
.map_err(|e| format!("failed to parse overlay TOML: {e}"))?
.resolve_paths(&base_dir);
let config = manifest_to_config(&manifest)?.merge(overlay);
manifest = PodManifest::try_from(config)
.map_err(|e| format!("failed to resolve default profile overlay: {e}"))?;
manifest.profile = profile;
}
if let Some(pod_name) = cli.pod.as_deref() {
factory = factory
.with_overlay_toml(&pod_name_overlay_toml(pod_name))
.map_err(|e| format!("failed to apply --pod overlay: {e}"))?;
manifest.pod.name = pod_name.to_string();
}
Ok(factory)
Ok(manifest)
}
#[tokio::main]
@@ -442,18 +439,20 @@ permission = "write"
}
#[test]
fn manifest_conflicts_with_user_manifest_env_when_env_is_non_empty() {
fn manifest_ignores_non_empty_user_manifest_env() {
let tmp = TempDir::new().unwrap();
let manifest = tmp.path().join("manifest.toml");
write(&manifest, &manifest_toml("single", tmp.path()));
let cli = Cli::try_parse_from(["insomnia-pod", "--manifest", manifest.to_str().unwrap()])
.unwrap();
let err = resolve_manifest_with_user_manifest_env(&cli, Some(OsString::from("user.toml")))
.unwrap_err();
let (manifest, loader) =
resolve_manifest_with_user_manifest_env(&cli, Some(OsString::from("user.toml")))
.unwrap();
assert!(err.contains("--manifest cannot be used"));
assert!(err.contains(paths::USER_MANIFEST_ENV));
assert_eq!(manifest.pod.name, "single");
assert!(loader.user_dir().is_none());
assert!(loader.workspace_dir().is_none());
}
#[test]
@@ -549,26 +548,38 @@ permission = "write"
}
#[test]
fn user_manifest_env_overrides_auto_user_manifest_path() {
fn normal_startup_uses_default_profile_and_ignores_user_manifest_env() {
let tmp = TempDir::new().unwrap();
let user_manifest = tmp.path().join("custom-user.toml");
write(&user_manifest, &manifest_toml("from-env", tmp.path()));
let no_project_root = tmp.path().join("no-project");
std::fs::create_dir_all(&no_project_root).unwrap();
let cli = Cli::try_parse_from([
"insomnia-pod",
"--project",
no_project_root.to_str().unwrap(),
])
.unwrap();
let cli = Cli::try_parse_from(["insomnia-pod"]).unwrap();
let mut called = false;
let (manifest, _loader) = resolve_manifest_with_user_manifest_env(
let (manifest, _loader) = resolve_manifest_with_user_manifest_env_and_profile_loader(
&cli,
Some(user_manifest.as_os_str().to_os_string()),
Some(OsString::from("ignored-user-manifest.toml")),
|selector, pod_name| {
called = true;
assert_eq!(selector, &ProfileSelector::Default);
assert_eq!(pod_name, None);
let manifest =
PodManifest::from_toml(&manifest_toml("from-default-profile", tmp.path()))
.unwrap();
Ok((manifest, PromptLoader::builtins_only()))
},
)
.unwrap();
assert_eq!(manifest.pod.name, "from-env");
assert!(called);
assert_eq!(manifest.pod.name, "from-default-profile");
}
#[test]
fn project_flag_no_longer_enables_ambient_manifest_cascade() {
let cli = Cli::try_parse_from(["insomnia-pod", "--project", "."]).unwrap();
let err = resolve_manifest_with_user_manifest_env_and_profile_loader(&cli, None, |_, _| {
panic!("default profile loader must not run when deprecated --project is present")
})
.unwrap_err();
assert!(err.contains("--project is no longer supported"));
}
#[test]
@@ -605,7 +616,17 @@ permission = "write"
let manifest = tmp.path().join("manifest.toml");
write(
&manifest,
&manifest_toml("unused", tmp.path()).replace("name = \"unused\"\n", ""),
r#"
[pod]
[model]
scheme = "anthropic"
model_id = "test-model"
[[scope.allow]]
target = "."
permission = "write"
"#,
);
let cli = Cli::try_parse_from([
"insomnia-pod",
@@ -619,6 +640,7 @@ permission = "write"
let (manifest, _loader) = resolve_manifest_with_user_manifest_env(&cli, None).unwrap();
assert_eq!(manifest.pod.name, "from-flag");
assert_eq!(manifest.scope.allow[0].target, tmp.path());
}
#[test]
+40 -155
View File
@@ -1,29 +1,23 @@
//! Inline-viewport "spawn Pod and attach" UX.
//!
//! Rendered at the user's current cursor position when `insomnia` is invoked
//! with no positional argument. Walks the cwd for a `.insomnia/manifest.toml`
//! to seed manifest defaults and `.insomnia/profiles.toml` to discover profile
//! choices, prompts for the Pod's name, and on confirmation
//! launches the `insomnia-pod` binary as an independent process with a freshly built
//! overlay (name + cwd scope when no project manifest exists). Once
//! the process reports its socket via the `INSOMNIA-READY` stderr line,
//! the dialog hands control back so main can switch the terminal to
//! alternate-screen mode.
//! with no positional argument. Discovers `.insomnia/profiles.toml` profile
//! choices plus bundled profiles, defaults to the builtin profile, prompts for
//! the Pod's name, and on confirmation launches the `insomnia-pod` binary as an
//! independent process. Once the process reports its socket via the
//! `INSOMNIA-READY` stderr line, the dialog hands control back so main can
//! switch the terminal to alternate-screen mode.
//!
//! The viewport's last frame stays in the terminal's scrollback so the
//! user has a record of what was spawned (or why a spawn failed).
use std::ffi::OsString;
use std::io;
use std::path::{Path, PathBuf};
use std::time::Duration;
use client::{SpawnConfig, spawn_pod};
use crossterm::event::{self, Event as TermEvent, KeyCode, KeyEventKind, KeyModifiers};
use manifest::{
PodManifestConfig, ProfileDiscovery, ScopeConfig, find_project_manifest_from, load_layer,
user_manifest_path, user_manifest_path_from_env,
};
use manifest::{ProfileDiscovery, ScopeConfig};
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use ratatui::layout::{Constraint, Layout};
@@ -97,7 +91,11 @@ pub async fn run(
profile: Option<String>,
) -> Result<SpawnOutcome, SpawnError> {
let defaults = load_spawn_defaults()?;
let mut profile_choices = defaults.profile_choices;
let mut profile_choices = if resume_from.is_some() {
Vec::new()
} else {
defaults.profile_choices
};
let profile_index = initial_profile_index(
&mut profile_choices,
profile.as_deref(),
@@ -232,42 +230,6 @@ struct ProfileChoice {
fn load_spawn_defaults() -> Result<SpawnDefaults, SpawnError> {
let cwd = std::env::current_dir().map_err(SpawnError::Io)?;
// Run the same merge pod itself uses, then read what's missing off the
// result. We only look at `scope.allow` here — `pod.name` is an
// instance-level identifier and is supplied by the dialog or `--pod`.
// TUI must pre-read the same user manifest path that the pod CLI will use,
// including a non-empty INSOMNIA_USER_MANIFEST override; empty values fall
// back to the auto-discovered path.
let user_layer = user_manifest_path_for_spawn(
std::env::var_os(manifest::paths::USER_MANIFEST_ENV),
user_manifest_path(),
)
.filter(|p| p.is_file())
.and_then(|p| load_layer(&p).ok());
let project_layer = find_project_manifest_from(&cwd).and_then(|p| load_layer(&p).ok());
let mut cascade = PodManifestConfig::builtin_defaults();
for layer in [user_layer.as_ref(), project_layer.as_ref()]
.into_iter()
.flatten()
{
cascade = cascade.merge(layer.clone());
}
let cascade_has_scope = !cascade.scope.allow.is_empty();
let scope_origin = match (
project_layer
.as_ref()
.is_some_and(|l| !l.scope.allow.is_empty()),
user_layer
.as_ref()
.is_some_and(|l| !l.scope.allow.is_empty()),
) {
(true, _) => ScopeOrigin::FromProject,
(false, true) => ScopeOrigin::FromUser,
(false, false) => ScopeOrigin::CwdDefault,
};
let default_name = cwd
.file_name()
.and_then(|s| s.to_str())
@@ -279,8 +241,8 @@ fn load_spawn_defaults() -> Result<SpawnDefaults, SpawnError> {
Ok(SpawnDefaults {
cwd,
cascade_has_scope,
scope_origin,
cascade_has_scope: true,
scope_origin: ScopeOrigin::FromProfile,
default_name,
default_profile_index,
profile_choices,
@@ -288,16 +250,11 @@ fn load_spawn_defaults() -> Result<SpawnDefaults, SpawnError> {
}
fn profile_choices_for_cwd(cwd: &Path) -> (Vec<ProfileChoice>, usize) {
let mut choices = vec![ProfileChoice {
selector: None,
label: "manifest cascade".to_string(),
is_default: false,
}];
let Ok(registry) = ProfileDiscovery::for_cwd(cwd).discover() else {
return (choices, 0);
return (Vec::new(), 0);
};
let mut choices = Vec::new();
for entry in registry.entries() {
let mut label = entry.qualified_name();
if entry.is_default {
@@ -343,13 +300,6 @@ fn initial_profile_index(
choices.len() - 1
}
fn user_manifest_path_for_spawn(
env_value: Option<OsString>,
default_user_manifest: Option<PathBuf>,
) -> Option<PathBuf> {
user_manifest_path_from_env(env_value).or(default_user_manifest)
}
fn form_for_pod_name(pod_name: String, defaults: SpawnDefaults) -> Form {
Form {
cwd: defaults.cwd,
@@ -362,11 +312,7 @@ fn form_for_pod_name(pod_name: String, defaults: SpawnDefaults) -> Form {
resume_from: None,
resume_by_pod_name: true,
resume_scope: None,
profile_choices: vec![ProfileChoice {
selector: None,
label: "manifest cascade".to_string(),
is_default: false,
}],
profile_choices: Vec::new(),
profile_index: 0,
}
}
@@ -519,16 +465,14 @@ enum MessageKind {
}
enum ScopeOrigin {
FromUser,
FromProject,
FromProfile,
CwdDefault,
}
struct Form {
cwd: PathBuf,
/// True when at least one cascade layer (user or project manifest)
/// already declares `scope.allow`. Drives whether the overlay
/// should add a cwd-write rule.
/// True when the launch source already supplies `scope.allow`.
/// Drives whether the compatibility overlay should add a cwd-write rule.
cascade_has_scope: bool,
/// Display label for the scope row in the dialog.
scope_origin: ScopeOrigin,
@@ -648,7 +592,7 @@ fn draw_form(f: &mut Frame<'_>, form: &Form) {
let layout = Layout::vertical([
Constraint::Length(1), // title
Constraint::Length(1), // name field
Constraint::Length(1), // context (manifest or scope default)
Constraint::Length(1), // context (profile or scope default)
Constraint::Length(1), // hint
Constraint::Length(1), // message
Constraint::Length(1), // spacer
@@ -716,15 +660,10 @@ fn context_line(form: &Form) -> Line<'_> {
}
match form.scope_origin {
ScopeOrigin::FromProject => Line::from(vec![
ScopeOrigin::FromProfile => Line::from(vec![
Span::raw(" "),
Span::styled("scope: ", Style::default().fg(Color::DarkGray)),
Span::styled("from project manifest", Style::default().fg(Color::Green)),
]),
ScopeOrigin::FromUser => Line::from(vec![
Span::raw(" "),
Span::styled("scope: ", Style::default().fg(Color::DarkGray)),
Span::styled("from user manifest", Style::default().fg(Color::Green)),
Span::styled("from default profile", Style::default().fg(Color::Green)),
]),
ScopeOrigin::CwdDefault => Line::from(vec![
Span::raw(" "),
@@ -767,7 +706,7 @@ mod tests {
cwd: PathBuf::from("/work/example"),
cascade_has_scope,
scope_origin: if cascade_has_scope {
ScopeOrigin::FromProject
ScopeOrigin::FromProfile
} else {
ScopeOrigin::CwdDefault
},
@@ -778,11 +717,7 @@ mod tests {
resume_from: None,
resume_by_pod_name: false,
resume_scope: None,
profile_choices: vec![ProfileChoice {
selector: None,
label: "manifest cascade".to_string(),
is_default: false,
}],
profile_choices: Vec::new(),
profile_index: 0,
}
}
@@ -792,14 +727,10 @@ mod tests {
let defaults = SpawnDefaults {
cwd: PathBuf::from("/work/example"),
cascade_has_scope: true,
scope_origin: ScopeOrigin::FromProject,
scope_origin: ScopeOrigin::FromProfile,
default_name: "ignored".to_string(),
default_profile_index: 0,
profile_choices: vec![ProfileChoice {
selector: None,
label: "manifest cascade".to_string(),
is_default: false,
}],
profile_choices: Vec::new(),
};
let f = form_for_pod_name("agent".to_string(), defaults);
@@ -860,46 +791,6 @@ mod tests {
assert_eq!(deny[0]["target"].as_str(), Some("/work/example/child"));
}
#[test]
fn cascade_merge_detects_scope_from_any_layer() {
let user = PodManifestConfig::from_toml(
r#"
[[scope.allow]]
target = "/from-user"
permission = "write"
"#,
)
.unwrap();
let mut cascade = PodManifestConfig::builtin_defaults();
cascade = cascade.merge(user);
assert!(!cascade.scope.allow.is_empty());
let empty_cascade = PodManifestConfig::builtin_defaults();
assert!(empty_cascade.scope.allow.is_empty());
}
#[test]
fn user_manifest_path_for_spawn_prefers_non_empty_env_override() {
assert_eq!(
user_manifest_path_for_spawn(
Some(OsString::from("/tmp/override.toml")),
Some(PathBuf::from("/default/manifest.toml")),
),
Some(PathBuf::from("/tmp/override.toml")),
);
}
#[test]
fn user_manifest_path_for_spawn_treats_empty_env_as_unset() {
assert_eq!(
user_manifest_path_for_spawn(
Some(OsString::from("")),
Some(PathBuf::from("/default/manifest.toml")),
),
Some(PathBuf::from("/default/manifest.toml")),
);
}
#[test]
fn profile_choices_use_project_registry_default() {
let temp = tempfile::tempdir().unwrap();
@@ -925,7 +816,7 @@ coder = "profiles/coder.nix"
}
#[test]
fn profile_choices_include_no_profile_source_labels_and_default_marker() {
fn profile_choices_include_builtin_and_project_default_marker() {
let temp = tempfile::tempdir().unwrap();
let project = temp.path().join("project");
let insomnia = project.join(".insomnia");
@@ -942,22 +833,17 @@ description = "Project coder"
.unwrap();
let (choices, default_index) = profile_choices_for_cwd(&project);
assert_eq!(choices[0].selector, None);
assert_eq!(choices[0].label, "manifest cascade");
assert_eq!(choices[0].selector.as_deref(), Some("builtin:default"));
assert_eq!(choices[0].label, "builtin:default");
assert_eq!(default_index, 1);
assert_eq!(choices[1].selector.as_deref(), Some("project:coder"));
assert_eq!(choices[1].label, "project:coder (default) — Project coder");
}
#[test]
fn profile_cycle_selects_profiles_and_can_opt_out_of_default() {
fn profile_cycle_selects_profiles_without_manifest_cascade_opt_out() {
let mut form = form("coder", true);
form.profile_choices = vec![
ProfileChoice {
selector: None,
label: "manifest cascade".to_string(),
is_default: false,
},
ProfileChoice {
selector: Some("project:coder".to_string()),
label: "project:coder (default)".to_string(),
@@ -969,7 +855,7 @@ description = "Project coder"
is_default: false,
},
];
form.profile_index = 1;
form.profile_index = 0;
assert_eq!(
form.selected_profile_selector().as_deref(),
@@ -981,7 +867,10 @@ description = "Project coder"
Some("user:reviewer")
);
form.cycle_profile_next();
assert_eq!(form.selected_profile_selector(), None);
assert_eq!(
form.selected_profile_selector().as_deref(),
Some("project:coder")
);
form.cycle_profile_prev();
assert_eq!(
form.selected_profile_selector().as_deref(),
@@ -991,15 +880,11 @@ description = "Project coder"
#[test]
fn initial_profile_index_adds_explicit_selector_not_in_discovery_list() {
let mut choices = vec![ProfileChoice {
selector: None,
label: "manifest cascade".to_string(),
is_default: false,
}];
let mut choices = Vec::new();
let selected = initial_profile_index(&mut choices, Some("coder"), 0);
assert_eq!(selected, 1);
assert_eq!(choices[1].selector.as_deref(), Some("coder"));
assert_eq!(choices[1].label, "coder");
assert_eq!(selected, 0);
assert_eq!(choices[0].selector.as_deref(), Some("coder"));
assert_eq!(choices[0].label, "coder");
}
#[test]