story-kit: done 199_story_web_ui_submits_all_queued_items_at_once
This commit is contained in:
@@ -9,6 +9,41 @@ pub struct ProjectConfig {
|
|||||||
pub component: Vec<ComponentConfig>,
|
pub component: Vec<ComponentConfig>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub agent: Vec<AgentConfig>,
|
pub agent: Vec<AgentConfig>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub watcher: WatcherConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for the filesystem watcher's sweep behaviour.
|
||||||
|
///
|
||||||
|
/// Controls how often the watcher checks `5_done/` for items to promote to
|
||||||
|
/// `6_archived/`, and how long items must remain in `5_done/` before promotion.
|
||||||
|
#[derive(Debug, Clone, Deserialize, PartialEq)]
|
||||||
|
pub struct WatcherConfig {
|
||||||
|
/// How often (in seconds) to check `5_done/` for items to archive.
|
||||||
|
/// Default: 60 seconds.
|
||||||
|
#[serde(default = "default_sweep_interval_secs")]
|
||||||
|
pub sweep_interval_secs: u64,
|
||||||
|
/// How long (in seconds) an item must remain in `5_done/` before being
|
||||||
|
/// moved to `6_archived/`. Default: 14400 (4 hours).
|
||||||
|
#[serde(default = "default_done_retention_secs")]
|
||||||
|
pub done_retention_secs: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for WatcherConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
sweep_interval_secs: default_sweep_interval_secs(),
|
||||||
|
done_retention_secs: default_done_retention_secs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_sweep_interval_secs() -> u64 {
|
||||||
|
60
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_done_retention_secs() -> u64 {
|
||||||
|
4 * 60 * 60 // 4 hours
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
@@ -87,6 +122,8 @@ struct LegacyProjectConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
component: Vec<ComponentConfig>,
|
component: Vec<ComponentConfig>,
|
||||||
agent: Option<AgentConfig>,
|
agent: Option<AgentConfig>,
|
||||||
|
#[serde(default)]
|
||||||
|
watcher: WatcherConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProjectConfig {
|
impl Default for ProjectConfig {
|
||||||
@@ -107,6 +144,7 @@ impl Default for ProjectConfig {
|
|||||||
stage: None,
|
stage: None,
|
||||||
inactivity_timeout_secs: default_inactivity_timeout_secs(),
|
inactivity_timeout_secs: default_inactivity_timeout_secs(),
|
||||||
}],
|
}],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +185,7 @@ impl ProjectConfig {
|
|||||||
let config = ProjectConfig {
|
let config = ProjectConfig {
|
||||||
component: legacy.component,
|
component: legacy.component,
|
||||||
agent: vec![agent],
|
agent: vec![agent],
|
||||||
|
watcher: legacy.watcher,
|
||||||
};
|
};
|
||||||
validate_agents(&config.agent)?;
|
validate_agents(&config.agent)?;
|
||||||
return Ok(config);
|
return Ok(config);
|
||||||
@@ -166,6 +205,7 @@ impl ProjectConfig {
|
|||||||
let config = ProjectConfig {
|
let config = ProjectConfig {
|
||||||
component: legacy.component,
|
component: legacy.component,
|
||||||
agent: vec![agent],
|
agent: vec![agent],
|
||||||
|
watcher: legacy.watcher,
|
||||||
};
|
};
|
||||||
validate_agents(&config.agent)?;
|
validate_agents(&config.agent)?;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
@@ -173,6 +213,7 @@ impl ProjectConfig {
|
|||||||
Ok(ProjectConfig {
|
Ok(ProjectConfig {
|
||||||
component: legacy.component,
|
component: legacy.component,
|
||||||
agent: Vec::new(),
|
agent: Vec::new(),
|
||||||
|
watcher: legacy.watcher,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -517,4 +558,93 @@ model = "sonnet"
|
|||||||
assert_eq!(config.agent[0].name, "main");
|
assert_eq!(config.agent[0].name, "main");
|
||||||
assert_eq!(config.agent[0].model, Some("sonnet".to_string()));
|
assert_eq!(config.agent[0].model, Some("sonnet".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── WatcherConfig ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn watcher_config_defaults_when_omitted() {
|
||||||
|
let toml_str = r#"
|
||||||
|
[[agent]]
|
||||||
|
name = "coder"
|
||||||
|
"#;
|
||||||
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
||||||
|
assert_eq!(config.watcher.sweep_interval_secs, 60);
|
||||||
|
assert_eq!(config.watcher.done_retention_secs, 4 * 60 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn watcher_config_custom_values() {
|
||||||
|
let toml_str = r#"
|
||||||
|
[watcher]
|
||||||
|
sweep_interval_secs = 30
|
||||||
|
done_retention_secs = 7200
|
||||||
|
|
||||||
|
[[agent]]
|
||||||
|
name = "coder"
|
||||||
|
"#;
|
||||||
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
||||||
|
assert_eq!(config.watcher.sweep_interval_secs, 30);
|
||||||
|
assert_eq!(config.watcher.done_retention_secs, 7200);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn watcher_config_partial_override() {
|
||||||
|
let toml_str = r#"
|
||||||
|
[watcher]
|
||||||
|
sweep_interval_secs = 10
|
||||||
|
|
||||||
|
[[agent]]
|
||||||
|
name = "coder"
|
||||||
|
"#;
|
||||||
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
||||||
|
assert_eq!(config.watcher.sweep_interval_secs, 10);
|
||||||
|
// done_retention_secs should fall back to the default (4 hours).
|
||||||
|
assert_eq!(config.watcher.done_retention_secs, 4 * 60 * 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn watcher_config_from_file() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let sk = tmp.path().join(".story_kit");
|
||||||
|
fs::create_dir_all(&sk).unwrap();
|
||||||
|
fs::write(
|
||||||
|
sk.join("project.toml"),
|
||||||
|
r#"
|
||||||
|
[watcher]
|
||||||
|
sweep_interval_secs = 120
|
||||||
|
done_retention_secs = 3600
|
||||||
|
|
||||||
|
[[agent]]
|
||||||
|
name = "coder"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
|
assert_eq!(config.watcher.sweep_interval_secs, 120);
|
||||||
|
assert_eq!(config.watcher.done_retention_secs, 3600);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn watcher_config_default_when_no_file() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let config = ProjectConfig::load(tmp.path()).unwrap();
|
||||||
|
assert_eq!(config.watcher, WatcherConfig::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn watcher_config_preserved_in_legacy_format() {
|
||||||
|
let toml_str = r#"
|
||||||
|
[watcher]
|
||||||
|
sweep_interval_secs = 15
|
||||||
|
done_retention_secs = 900
|
||||||
|
|
||||||
|
[agent]
|
||||||
|
command = "claude"
|
||||||
|
"#;
|
||||||
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
||||||
|
assert_eq!(config.watcher.sweep_interval_secs, 15);
|
||||||
|
assert_eq!(config.watcher.done_retention_secs, 900);
|
||||||
|
assert_eq!(config.agent.len(), 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
//! via exit-code inspection and silently skips the commit while still broadcasting
|
//! via exit-code inspection and silently skips the commit while still broadcasting
|
||||||
//! the event so connected clients stay in sync.
|
//! the event so connected clients stay in sync.
|
||||||
|
|
||||||
|
use crate::config::{ProjectConfig, WatcherConfig};
|
||||||
use crate::slog;
|
use crate::slog;
|
||||||
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher, recommended_watcher};
|
use notify::{EventKind, RecommendedWatcher, RecursiveMode, Watcher, recommended_watcher};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -205,12 +206,11 @@ fn flush_pending(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scan `work/5_done/` and move any `.md` files whose mtime is older than
|
/// Scan `work/5_done/` and move any `.md` files whose mtime is older than
|
||||||
/// `DONE_RETENTION` to `work/6_archived/`.
|
/// `done_retention` to `work/6_archived/`.
|
||||||
///
|
///
|
||||||
/// Called periodically from the watcher thread. File moves will trigger normal
|
/// Called periodically from the watcher thread. File moves will trigger normal
|
||||||
/// watcher events, which `flush_pending` will commit and broadcast.
|
/// watcher events, which `flush_pending` will commit and broadcast.
|
||||||
fn sweep_done_to_archived(work_dir: &Path) {
|
fn sweep_done_to_archived(work_dir: &Path, done_retention: Duration) {
|
||||||
const DONE_RETENTION: Duration = Duration::from_secs(4 * 60 * 60);
|
|
||||||
|
|
||||||
let done_dir = work_dir.join("5_done");
|
let done_dir = work_dir.join("5_done");
|
||||||
if !done_dir.exists() {
|
if !done_dir.exists() {
|
||||||
@@ -242,7 +242,7 @@ fn sweep_done_to_archived(work_dir: &Path) {
|
|||||||
.duration_since(mtime)
|
.duration_since(mtime)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if age >= DONE_RETENTION {
|
if age >= done_retention {
|
||||||
if let Err(e) = std::fs::create_dir_all(&archived_dir) {
|
if let Err(e) = std::fs::create_dir_all(&archived_dir) {
|
||||||
slog!("[watcher] sweep: failed to create 6_archived/: {e}");
|
slog!("[watcher] sweep: failed to create 6_archived/: {e}");
|
||||||
continue;
|
continue;
|
||||||
@@ -270,10 +270,12 @@ fn sweep_done_to_archived(work_dir: &Path) {
|
|||||||
/// `git_root` — project root (passed to `git` commands as cwd, and used to
|
/// `git_root` — project root (passed to `git` commands as cwd, and used to
|
||||||
/// derive the config file path `.story_kit/project.toml`).
|
/// derive the config file path `.story_kit/project.toml`).
|
||||||
/// `event_tx` — broadcast sender; each connected WebSocket client holds a receiver.
|
/// `event_tx` — broadcast sender; each connected WebSocket client holds a receiver.
|
||||||
|
/// `watcher_config` — initial sweep configuration loaded from `project.toml`.
|
||||||
pub fn start_watcher(
|
pub fn start_watcher(
|
||||||
work_dir: PathBuf,
|
work_dir: PathBuf,
|
||||||
git_root: PathBuf,
|
git_root: PathBuf,
|
||||||
event_tx: broadcast::Sender<WatcherEvent>,
|
event_tx: broadcast::Sender<WatcherEvent>,
|
||||||
|
watcher_config: WatcherConfig,
|
||||||
) {
|
) {
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let (notify_tx, notify_rx) = mpsc::channel::<notify::Result<notify::Event>>();
|
let (notify_tx, notify_rx) = mpsc::channel::<notify::Result<notify::Event>>();
|
||||||
@@ -304,8 +306,15 @@ pub fn start_watcher(
|
|||||||
slog!("[watcher] watching {}", work_dir.display());
|
slog!("[watcher] watching {}", work_dir.display());
|
||||||
|
|
||||||
const DEBOUNCE: Duration = Duration::from_millis(300);
|
const DEBOUNCE: Duration = Duration::from_millis(300);
|
||||||
/// How often to check 5_done/ for items to promote to 6_archived/.
|
|
||||||
const SWEEP_INTERVAL: Duration = Duration::from_secs(60);
|
// Mutable sweep config — hot-reloaded when project.toml changes.
|
||||||
|
let mut sweep_interval = Duration::from_secs(watcher_config.sweep_interval_secs);
|
||||||
|
let mut done_retention = Duration::from_secs(watcher_config.done_retention_secs);
|
||||||
|
slog!(
|
||||||
|
"[watcher] sweep_interval={}s done_retention={}s",
|
||||||
|
watcher_config.sweep_interval_secs,
|
||||||
|
watcher_config.done_retention_secs
|
||||||
|
);
|
||||||
|
|
||||||
// Map path → stage for pending (uncommitted) work-item changes.
|
// Map path → stage for pending (uncommitted) work-item changes.
|
||||||
let mut pending: HashMap<PathBuf, String> = HashMap::new();
|
let mut pending: HashMap<PathBuf, String> = HashMap::new();
|
||||||
@@ -315,7 +324,7 @@ pub fn start_watcher(
|
|||||||
// Track when we last swept 5_done/ → 6_archived/.
|
// Track when we last swept 5_done/ → 6_archived/.
|
||||||
// Initialise to "now minus interval" so the first sweep runs on startup.
|
// Initialise to "now minus interval" so the first sweep runs on startup.
|
||||||
let mut last_sweep = Instant::now()
|
let mut last_sweep = Instant::now()
|
||||||
.checked_sub(SWEEP_INTERVAL)
|
.checked_sub(sweep_interval)
|
||||||
.unwrap_or_else(Instant::now);
|
.unwrap_or_else(Instant::now);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -368,15 +377,40 @@ pub fn start_watcher(
|
|||||||
if config_changed_pending {
|
if config_changed_pending {
|
||||||
slog!("[watcher] broadcasting agent_config_changed");
|
slog!("[watcher] broadcasting agent_config_changed");
|
||||||
let _ = event_tx.send(WatcherEvent::ConfigChanged);
|
let _ = event_tx.send(WatcherEvent::ConfigChanged);
|
||||||
|
|
||||||
|
// Hot-reload sweep config from project.toml.
|
||||||
|
match ProjectConfig::load(&git_root) {
|
||||||
|
Ok(cfg) => {
|
||||||
|
let new_sweep =
|
||||||
|
Duration::from_secs(cfg.watcher.sweep_interval_secs);
|
||||||
|
let new_retention =
|
||||||
|
Duration::from_secs(cfg.watcher.done_retention_secs);
|
||||||
|
if new_sweep != sweep_interval
|
||||||
|
|| new_retention != done_retention
|
||||||
|
{
|
||||||
|
slog!(
|
||||||
|
"[watcher] hot-reload: sweep_interval={}s done_retention={}s",
|
||||||
|
cfg.watcher.sweep_interval_secs,
|
||||||
|
cfg.watcher.done_retention_secs
|
||||||
|
);
|
||||||
|
sweep_interval = new_sweep;
|
||||||
|
done_retention = new_retention;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
slog!("[watcher] hot-reload: failed to parse config: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config_changed_pending = false;
|
config_changed_pending = false;
|
||||||
}
|
}
|
||||||
deadline = None;
|
deadline = None;
|
||||||
|
|
||||||
// Periodically promote old items from 5_done/ to 6_archived/.
|
// Periodically promote old items from 5_done/ to 6_archived/.
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
if now.duration_since(last_sweep) >= SWEEP_INTERVAL {
|
if now.duration_since(last_sweep) >= sweep_interval {
|
||||||
last_sweep = now;
|
last_sweep = now;
|
||||||
sweep_done_to_archived(&work_dir);
|
sweep_done_to_archived(&work_dir, done_retention);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -723,7 +757,8 @@ mod tests {
|
|||||||
filetime::set_file_mtime(&story_path, filetime::FileTime::from_system_time(past))
|
filetime::set_file_mtime(&story_path, filetime::FileTime::from_system_time(past))
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
sweep_done_to_archived(&work_dir);
|
let retention = Duration::from_secs(4 * 60 * 60);
|
||||||
|
sweep_done_to_archived(&work_dir, retention);
|
||||||
|
|
||||||
assert!(!story_path.exists(), "old item should be moved out of 5_done/");
|
assert!(!story_path.exists(), "old item should be moved out of 5_done/");
|
||||||
assert!(
|
assert!(
|
||||||
@@ -743,8 +778,64 @@ mod tests {
|
|||||||
let story_path = done_dir.join("11_story_new.md");
|
let story_path = done_dir.join("11_story_new.md");
|
||||||
fs::write(&story_path, "---\nname: new\n---\n").unwrap();
|
fs::write(&story_path, "---\nname: new\n---\n").unwrap();
|
||||||
|
|
||||||
sweep_done_to_archived(&work_dir);
|
let retention = Duration::from_secs(4 * 60 * 60);
|
||||||
|
sweep_done_to_archived(&work_dir, retention);
|
||||||
|
|
||||||
assert!(story_path.exists(), "recent item should remain in 5_done/");
|
assert!(story_path.exists(), "recent item should remain in 5_done/");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sweep_respects_custom_retention() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let work_dir = tmp.path().join(".story_kit").join("work");
|
||||||
|
let done_dir = work_dir.join("5_done");
|
||||||
|
let archived_dir = work_dir.join("6_archived");
|
||||||
|
fs::create_dir_all(&done_dir).unwrap();
|
||||||
|
|
||||||
|
// Write a file and backdate its mtime to 2 minutes ago.
|
||||||
|
let story_path = done_dir.join("12_story_custom.md");
|
||||||
|
fs::write(&story_path, "---\nname: custom\n---\n").unwrap();
|
||||||
|
let past = SystemTime::now()
|
||||||
|
.checked_sub(Duration::from_secs(120))
|
||||||
|
.unwrap();
|
||||||
|
filetime::set_file_mtime(&story_path, filetime::FileTime::from_system_time(past))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// With a 1-minute retention, the 2-minute-old file should be swept.
|
||||||
|
sweep_done_to_archived(&work_dir, Duration::from_secs(60));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!story_path.exists(),
|
||||||
|
"item older than custom retention should be moved"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
archived_dir.join("12_story_custom.md").exists(),
|
||||||
|
"item should appear in 6_archived/"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sweep_custom_retention_keeps_younger_items() {
|
||||||
|
let tmp = TempDir::new().unwrap();
|
||||||
|
let work_dir = tmp.path().join(".story_kit").join("work");
|
||||||
|
let done_dir = work_dir.join("5_done");
|
||||||
|
fs::create_dir_all(&done_dir).unwrap();
|
||||||
|
|
||||||
|
// Write a file and backdate its mtime to 30 seconds ago.
|
||||||
|
let story_path = done_dir.join("13_story_young.md");
|
||||||
|
fs::write(&story_path, "---\nname: young\n---\n").unwrap();
|
||||||
|
let past = SystemTime::now()
|
||||||
|
.checked_sub(Duration::from_secs(30))
|
||||||
|
.unwrap();
|
||||||
|
filetime::set_file_mtime(&story_path, filetime::FileTime::from_system_time(past))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// With a 1-minute retention, the 30-second-old file should stay.
|
||||||
|
sweep_done_to_archived(&work_dir, Duration::from_secs(60));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
story_path.exists(),
|
||||||
|
"item younger than custom retention should remain"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,7 +73,15 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
if let Some(ref root) = *app_state.project_root.lock().unwrap() {
|
if let Some(ref root) = *app_state.project_root.lock().unwrap() {
|
||||||
let work_dir = root.join(".story_kit").join("work");
|
let work_dir = root.join(".story_kit").join("work");
|
||||||
if work_dir.is_dir() {
|
if work_dir.is_dir() {
|
||||||
io::watcher::start_watcher(work_dir, root.clone(), watcher_tx.clone());
|
let watcher_config = config::ProjectConfig::load(root)
|
||||||
|
.map(|c| c.watcher)
|
||||||
|
.unwrap_or_default();
|
||||||
|
io::watcher::start_watcher(
|
||||||
|
work_dir,
|
||||||
|
root.clone(),
|
||||||
|
watcher_tx.clone(),
|
||||||
|
watcher_config,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -307,7 +307,7 @@ async fn run_shell_command(cmd: &str, cwd: &Path) -> Result<(), String> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::config::ComponentConfig;
|
use crate::config::{ComponentConfig, WatcherConfig};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
@@ -491,6 +491,7 @@ mod tests {
|
|||||||
let config = ProjectConfig {
|
let config = ProjectConfig {
|
||||||
component: vec![],
|
component: vec![],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
// Should complete without panic
|
// Should complete without panic
|
||||||
run_setup_commands(tmp.path(), &config).await;
|
run_setup_commands(tmp.path(), &config).await;
|
||||||
@@ -507,6 +508,7 @@ mod tests {
|
|||||||
teardown: vec![],
|
teardown: vec![],
|
||||||
}],
|
}],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
// Should complete without panic
|
// Should complete without panic
|
||||||
run_setup_commands(tmp.path(), &config).await;
|
run_setup_commands(tmp.path(), &config).await;
|
||||||
@@ -523,6 +525,7 @@ mod tests {
|
|||||||
teardown: vec![],
|
teardown: vec![],
|
||||||
}],
|
}],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
// Setup command failures are non-fatal — should not panic or propagate
|
// Setup command failures are non-fatal — should not panic or propagate
|
||||||
run_setup_commands(tmp.path(), &config).await;
|
run_setup_commands(tmp.path(), &config).await;
|
||||||
@@ -539,6 +542,7 @@ mod tests {
|
|||||||
teardown: vec!["exit 1".to_string()],
|
teardown: vec!["exit 1".to_string()],
|
||||||
}],
|
}],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
// Teardown failures are best-effort — should not propagate
|
// Teardown failures are best-effort — should not propagate
|
||||||
assert!(run_teardown_commands(tmp.path(), &config).await.is_ok());
|
assert!(run_teardown_commands(tmp.path(), &config).await.is_ok());
|
||||||
@@ -554,6 +558,7 @@ mod tests {
|
|||||||
let config = ProjectConfig {
|
let config = ProjectConfig {
|
||||||
component: vec![],
|
component: vec![],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
let info = create_worktree(&project_root, "42_fresh_test", &config, 3001)
|
let info = create_worktree(&project_root, "42_fresh_test", &config, 3001)
|
||||||
.await
|
.await
|
||||||
@@ -576,6 +581,7 @@ mod tests {
|
|||||||
let config = ProjectConfig {
|
let config = ProjectConfig {
|
||||||
component: vec![],
|
component: vec![],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
// First creation
|
// First creation
|
||||||
let _info1 = create_worktree(&project_root, "43_reuse_test", &config, 3001)
|
let _info1 = create_worktree(&project_root, "43_reuse_test", &config, 3001)
|
||||||
@@ -614,6 +620,7 @@ mod tests {
|
|||||||
let config = ProjectConfig {
|
let config = ProjectConfig {
|
||||||
component: vec![],
|
component: vec![],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = remove_worktree_by_story_id(tmp.path(), "99_nonexistent", &config).await;
|
let result = remove_worktree_by_story_id(tmp.path(), "99_nonexistent", &config).await;
|
||||||
@@ -635,6 +642,7 @@ mod tests {
|
|||||||
let config = ProjectConfig {
|
let config = ProjectConfig {
|
||||||
component: vec![],
|
component: vec![],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
create_worktree(&project_root, "88_remove_by_id", &config, 3001)
|
create_worktree(&project_root, "88_remove_by_id", &config, 3001)
|
||||||
.await
|
.await
|
||||||
@@ -660,6 +668,7 @@ mod tests {
|
|||||||
teardown: vec![],
|
teardown: vec![],
|
||||||
}],
|
}],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
// Even though setup commands fail, create_worktree must succeed
|
// Even though setup commands fail, create_worktree must succeed
|
||||||
// so the agent can start and fix the problem itself.
|
// so the agent can start and fix the problem itself.
|
||||||
@@ -684,6 +693,7 @@ mod tests {
|
|||||||
let empty_config = ProjectConfig {
|
let empty_config = ProjectConfig {
|
||||||
component: vec![],
|
component: vec![],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
// First creation — no setup commands, should succeed
|
// First creation — no setup commands, should succeed
|
||||||
create_worktree(&project_root, "173_reuse_fail", &empty_config, 3001)
|
create_worktree(&project_root, "173_reuse_fail", &empty_config, 3001)
|
||||||
@@ -698,6 +708,7 @@ mod tests {
|
|||||||
teardown: vec![],
|
teardown: vec![],
|
||||||
}],
|
}],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
// Second call — worktree exists, setup commands fail, must still succeed
|
// Second call — worktree exists, setup commands fail, must still succeed
|
||||||
let result =
|
let result =
|
||||||
@@ -719,6 +730,7 @@ mod tests {
|
|||||||
let config = ProjectConfig {
|
let config = ProjectConfig {
|
||||||
component: vec![],
|
component: vec![],
|
||||||
agent: vec![],
|
agent: vec![],
|
||||||
|
watcher: WatcherConfig::default(),
|
||||||
};
|
};
|
||||||
let info = create_worktree(&project_root, "77_remove_async", &config, 3001)
|
let info = create_worktree(&project_root, "77_remove_async", &config, 3001)
|
||||||
.await
|
.await
|
||||||
|
|||||||
Reference in New Issue
Block a user