use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicU32, Ordering};
static COUNTER: AtomicU32 = AtomicU32::new(0);
struct Sandbox {
base: PathBuf,
src: PathBuf,
mind_home: PathBuf,
claude_home: PathBuf,
}
struct Run {
stdout: String,
stderr: String,
success: bool,
}
impl Sandbox {
fn new() -> Self {
let n = COUNTER.fetch_add(1, Ordering::SeqCst);
let base = std::env::temp_dir().join(format!("mind-wfup-{}-{n}", std::process::id()));
let _ = std::fs::remove_dir_all(&base);
let src = base.join("wf-source");
let sb = Sandbox {
base: base.clone(),
src: src.clone(),
mind_home: base.join("mind"),
claude_home: base.join("claude"),
};
git_init(&src);
sb
}
fn mind(&self, args: &[&str]) -> Run {
let out = Command::new(env!("CARGO_BIN_EXE_mind"))
.args(args)
.env("MIND_HOME", &self.mind_home)
.env("CLAUDE_HOME", &self.claude_home)
.env_remove("MIND_ABSORB_TO")
.env_remove("MIND_AGENT_HOMES")
.env_remove("MIND_MAX_METADATA_SIZE")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null())
.output()
.expect("run mind");
Run {
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
success: out.status.success(),
}
}
fn mind_interactive(&self, args: &[&str], stdin: &str) -> Run {
use std::io::Write as _;
let mut child = Command::new(env!("CARGO_BIN_EXE_mind"))
.args(args)
.env("MIND_HOME", &self.mind_home)
.env("CLAUDE_HOME", &self.claude_home)
.env("MIND_TTY", "1")
.env_remove("MIND_ABSORB_TO")
.env_remove("MIND_AGENT_HOMES")
.env_remove("MIND_MAX_METADATA_SIZE")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::piped())
.spawn()
.expect("spawn mind");
child
.stdin
.as_mut()
.expect("piped stdin")
.write_all(stdin.as_bytes())
.expect("write stdin");
let out = child.wait_with_output().expect("wait for mind");
Run {
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
success: out.status.success(),
}
}
fn src_spec(&self) -> String {
self.src.to_string_lossy().into_owned()
}
fn write_workflow(&self, name: &str, meta_name: &str) {
write_file(
&self.src.join("workflows").join(format!("{name}.js")),
&workflow_js(meta_name, name),
);
}
fn write_workflow_raw(&self, name: &str, body: &str) {
write_file(&self.src.join("workflows").join(format!("{name}.js")), body);
}
fn commit_src(&self, msg: &str) {
git(&self.src, &["add", "-A"]);
git(&self.src, &["commit", "-qm", msg]);
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.base);
}
}
fn workflow_js(meta_name: &str, description_of: &str) -> String {
format!(
"export const meta = {{\n \
name: '{meta_name}',\n \
description: 'Run the {description_of} pass',\n \
phases: [{{ title: 'Go' }}],\n\
}}\n\
\n\
phase('Go')\n\
const done = await agent('Do the {description_of} work.')\n\
\n\
return done\n"
)
}
fn write_file(path: &Path, contents: &str) {
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, contents).unwrap();
}
fn git(dir: &Path, args: &[&str]) {
let status = Command::new("git")
.args(args)
.current_dir(dir)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.expect("run git");
assert!(status.success(), "git {args:?} failed in {dir:?}");
}
fn git_init(dir: &Path) {
std::fs::create_dir_all(dir).unwrap();
git(dir, &["-c", "init.defaultBranch=main", "init", "-q"]);
git(dir, &["config", "user.email", "t@t"]);
git(dir, &["config", "user.name", "t"]);
std::fs::write(dir.join("README.md"), "# wf-source\n").unwrap();
git(dir, &["add", "README.md"]);
git(dir, &["commit", "-qm", "init"]);
}
#[test]
fn wf24_wf29_upgrade_warns_on_newly_diverged_and_colliding_meta_name() {
let sb = Sandbox::new();
sb.write_workflow("alpha", "alpha");
sb.write_workflow("beta", "beta");
sb.commit_src("add workflows");
let spec = sb.src_spec();
let meld = sb.mind(&["meld", &spec, "--register-only"]);
assert!(
meld.success,
"meld must succeed: stdout={} stderr={}",
meld.stdout, meld.stderr
);
let learn_a = sb.mind(&["learn", "workflow:alpha"]);
assert!(
learn_a.success,
"learn workflow:alpha must succeed: stdout={} stderr={}",
learn_a.stdout, learn_a.stderr
);
let learn_b = sb.mind(&["learn", "workflow:beta"]);
assert!(
learn_b.success,
"learn workflow:beta must succeed: stdout={} stderr={}",
learn_b.stdout, learn_b.stderr
);
assert!(
!learn_a.stderr.contains("the harness resolves it as"),
"a workflow whose meta.name matches its item name must draw no \
divergence warning at learn: {}",
learn_a.stderr
);
assert!(
!learn_b.stderr.contains("it answers to the harness name"),
"two workflows with distinct harness names must draw no collision \
warning at learn: {}",
learn_b.stderr
);
sb.write_workflow("alpha", "beta");
sb.commit_src("alpha now answers to beta");
let sync = sb.mind(&["sync"]);
assert!(
sync.success,
"sync must succeed: stdout={} stderr={}",
sync.stdout, sync.stderr
);
let up = sb.mind(&["upgrade", "--yes"]);
assert!(
up.success,
"upgrade must succeed despite the warnings: stdout={} stderr={}",
up.stdout, up.stderr
);
assert!(
up.stdout.contains("upgraded"),
"the upgrade must actually have applied: stdout={} stderr={}",
up.stdout,
up.stderr
);
assert!(
up.stderr.contains("workflow:alpha"),
"the upgrade warnings must name the upgraded item: {}",
up.stderr
);
assert!(
up.stderr
.contains("the harness resolves it as 'beta', not 'alpha'"),
"upgrade must report the WF-24 divergence introduced upstream: {}",
up.stderr
);
assert!(
up.stderr
.contains("it answers to the harness name 'beta', which workflow:beta also claims"),
"upgrade must report the WF-29 collision against the whole installed \
set, not just the items it touched: {}",
up.stderr
);
}
#[test]
fn wf24_upgrade_reports_a_renamed_workflow_under_its_new_key() {
let sb = Sandbox::new();
sb.write_workflow("alpha", "alpha");
sb.commit_src("add workflow");
let spec = sb.src_spec();
let meld = sb.mind(&["meld", &spec, "--register-only"]);
assert!(
meld.success,
"meld must succeed: stdout={} stderr={}",
meld.stdout, meld.stderr
);
let learn = sb.mind(&["learn", "workflow:alpha"]);
assert!(
learn.success,
"learn workflow:alpha must succeed: stdout={} stderr={}",
learn.stdout, learn.stderr
);
write_file(
&sb.src.join("mind.toml"),
"[source]\ndescription = \"workflow fixture\"\nprefix = \"labs\"\n",
);
sb.commit_src("namespace the source");
let sync = sb.mind(&["sync"]);
assert!(
sync.success,
"sync must succeed: stdout={} stderr={}",
sync.stdout, sync.stderr
);
let up = sb.mind(&["upgrade", "--yes"]);
assert!(
up.success,
"upgrade must succeed: stdout={} stderr={}",
up.stdout, up.stderr
);
assert!(
up.stderr.contains("workflow:labs:alpha"),
"the warning must be keyed to the new name, not the pre-rename one: {}",
up.stderr
);
assert!(
up.stderr
.contains("the harness resolves it as 'alpha', not 'labs:alpha'"),
"upgrade must report the divergence a prefix change introduces: {}",
up.stderr
);
assert!(
up.stderr.contains("`meta.name: '{{ns:alpha}}'`"),
"the remedy must be the token that actually resolves: {}",
up.stderr
);
assert!(
!up.stderr.contains("{{ns:labs:alpha}}"),
"a prefixed referent names no sibling (NS-11): {}",
up.stderr
);
}
#[test]
fn wf24_wf29_upgrade_warnings_reach_stderr_under_json() {
let sb = Sandbox::new();
sb.write_workflow("alpha", "alpha");
sb.write_workflow("beta", "beta");
sb.commit_src("add workflows");
let spec = sb.src_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
assert!(sb.mind(&["learn", "workflow:alpha"]).success);
assert!(sb.mind(&["learn", "workflow:beta"]).success);
sb.write_workflow("alpha", "beta");
sb.commit_src("alpha now answers to beta");
assert!(sb.mind(&["sync"]).success);
let up = sb.mind(&["upgrade", "--yes", "--json"]);
assert!(
up.success,
"upgrade --json must succeed: stdout={} stderr={}",
up.stdout, up.stderr
);
let doc: serde_json::Value = serde_json::from_str(up.stdout.trim())
.unwrap_or_else(|e| panic!("stdout must be one JSON document: {e}\n{}", up.stdout));
assert_eq!(
doc["action"].as_str(),
Some("upgrade"),
"the document must answer the invoked verb: {}",
up.stdout
);
assert!(
!up.stdout.contains("warning:"),
"no warning may land on stdout under --json: {}",
up.stdout
);
assert!(
up.stderr
.contains("the harness resolves it as 'beta', not 'alpha'"),
"the WF-24 divergence must survive --json (on stderr): {}",
up.stderr
);
assert!(
up.stderr
.contains("it answers to the harness name 'beta', which workflow:beta also claims"),
"the WF-29 collision must survive --json (on stderr): {}",
up.stderr
);
}
#[test]
fn wf30_wf31_upgrade_warns_when_an_edit_makes_a_workflow_unloadable() {
let sb = Sandbox::new();
sb.write_workflow("gamma", "gamma");
sb.write_workflow("delta", "delta");
sb.commit_src("add workflows");
let spec = sb.src_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
let learn_g = sb.mind(&["learn", "workflow:gamma"]);
assert!(learn_g.success, "learn gamma: {}", learn_g.stderr);
let learn_d = sb.mind(&["learn", "workflow:delta"]);
assert!(learn_d.success, "learn delta: {}", learn_d.stderr);
assert!(
!learn_g.stderr.contains("will not load this workflow")
&& !learn_d.stderr.contains("will not load this workflow"),
"a loadable workflow must draw no WF-30 warning at learn: {} {}",
learn_g.stderr,
learn_d.stderr
);
sb.write_workflow_raw("gamma", "export const meta = {\n name: 'gamma',\n}\n");
sb.write_workflow_raw("delta", "// no meta here at all\nconst x = 1\n");
sb.commit_src("break both workflows");
assert!(sb.mind(&["sync"]).success);
let up = sb.mind(&["upgrade", "--yes"]);
assert!(
up.success,
"upgrade must succeed despite the WF-30 findings: stdout={} stderr={}",
up.stdout, up.stderr
);
assert!(
up.stderr.contains(
"workflow:gamma: the harness will not load this workflow: `meta.description` is \
missing; installed anyway"
),
"upgrade must report the missing `meta.description`: {}",
up.stderr
);
assert!(
up.stderr.contains(
"workflow:delta: the harness will not load this workflow: mind read no `name`, \
`description`, or `whenToUse` from its `meta`; installed anyway"
),
"upgrade must report the unreadable `meta`, in terms of what mind read: {}",
up.stderr
);
assert!(
!up.stderr.contains("the harness resolves it as"),
"an unloadable workflow must not also draw a divergence warning: {}",
up.stderr
);
let installed = std::fs::read_to_string(sb.claude_home.join("workflows/delta.js"))
.expect("the link must still resolve after the upgrade");
assert!(
installed.contains("no meta here at all"),
"the upgrade must have applied the unloadable version: {installed}"
);
}
#[test]
fn wf32_upgrade_reports_a_workflow_that_grew_past_the_cap_and_installs_it() {
let sb = Sandbox::new();
sb.write_workflow("big", "big");
sb.commit_src("add workflow");
let spec = sb.src_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
let learn = sb.mind(&["learn", "workflow:big"]);
assert!(learn.success, "learn: {}", learn.stderr);
assert!(
!learn.stderr.contains("byte cap"),
"the under-cap install must draw no overage warning: {}",
learn.stderr
);
let mut body = String::from("export const meta = {\n name: 'big',\n description: 'Big'\n}\n");
body.push_str("// ");
body.push_str(&"p".repeat(524_288));
body.push('\n');
sb.write_workflow_raw("big", &body);
sb.commit_src("pad past the cap");
assert!(sb.mind(&["sync"]).success);
let up = sb.mind(&["upgrade", "--yes"]);
assert!(
up.success,
"an over-cap upgrade must still succeed: stdout={} stderr={}",
up.stdout, up.stderr
);
assert!(
up.stderr
.contains("over the harness's 524288-byte cap; installed anyway"),
"upgrade must report the overage: {}",
up.stderr
);
assert!(
up.stderr.contains("workflow:big"),
"the overage must be keyed to the item: {}",
up.stderr
);
let installed = std::fs::metadata(sb.claude_home.join("workflows/big.js"))
.expect("the over-cap workflow must still be installed");
assert!(
installed.len() > 524_288,
"the over-cap file must be what landed, unmodified: {} bytes",
installed.len()
);
}
#[test]
fn an_upgrade_of_one_workflow_does_not_re_report_an_untouched_diverged_one() {
let sb = Sandbox::new();
sb.write_workflow("alpha", "not-alpha");
sb.write_workflow("beta", "beta");
sb.commit_src("add workflows");
let spec = sb.src_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
let learn_a = sb.mind(&["learn", "workflow:alpha"]);
assert!(learn_a.success, "learn alpha: {}", learn_a.stderr);
assert!(
learn_a
.stderr
.contains("the harness resolves it as 'not-alpha'"),
"the fixture's divergence must be real, or this test proves nothing: {}",
learn_a.stderr
);
let learn_b = sb.mind(&["learn", "workflow:beta"]);
assert!(learn_b.success, "learn beta: {}", learn_b.stderr);
let store_alpha = sb.mind_home.join("store/workflow/alpha");
let before = std::fs::read(&store_alpha).expect("alpha's store copy");
sb.write_workflow_raw(
"beta",
&format!("{}// touched\n", workflow_js("beta", "beta")),
);
sb.commit_src("touch beta only");
assert!(sb.mind(&["sync"]).success);
let up = sb.mind(&["upgrade", "--yes"]);
assert!(
up.success,
"upgrade must succeed: stdout={} stderr={}",
up.stdout, up.stderr
);
assert!(
up.stdout.contains("upgraded"),
"beta must actually have upgraded: {}",
up.stdout
);
assert!(
!up.stderr.contains("workflow:alpha"),
"no warning may be keyed to the untouched workflow:alpha: {}",
up.stderr
);
assert!(
!up.stderr.contains("workflow:beta"),
"beta's name is unchanged, so it draws no divergence: {}",
up.stderr
);
let after = std::fs::read(&store_alpha).expect("alpha's store copy after");
assert_eq!(
before, after,
"alpha's store copy must be byte-identical after the upgrade"
);
}
#[test]
fn an_upgrade_that_touches_no_workflow_reports_no_workflow_warning() {
let sb = Sandbox::new();
sb.write_workflow("alpha", "not-alpha");
write_file(
&sb.src.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff\n---\n# review\n",
);
sb.commit_src("add a workflow and a skill");
let spec = sb.src_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
let learn_w = sb.mind(&["learn", "workflow:alpha"]);
assert!(learn_w.success, "learn workflow: {}", learn_w.stderr);
assert!(
learn_w
.stderr
.contains("the harness resolves it as 'not-alpha'"),
"the fixture's divergence must be real, or this test proves nothing: {}",
learn_w.stderr
);
assert!(sb.mind(&["learn", "skill:review"]).success);
write_file(
&sb.src.join("skills/review/SKILL.md"),
"---\nname: review\ndescription: Review the diff\n---\n# review\n\nNow with detail.\n",
);
sb.commit_src("edit the skill");
assert!(sb.mind(&["sync"]).success);
let up = sb.mind(&["upgrade", "skill:review", "--yes"]);
assert!(
up.success,
"upgrade must succeed: stdout={} stderr={}",
up.stdout, up.stderr
);
assert!(
up.stdout.contains("upgraded"),
"the skill must actually have upgraded: {}",
up.stdout
);
assert!(
!up.stderr.contains("the harness resolves it as"),
"an upgrade that touched no workflow must say nothing about one: {}",
up.stderr
);
assert!(
!up.stderr.contains("will not load this workflow"),
"an upgrade that touched no workflow must say nothing about one: {}",
up.stderr
);
}
#[test]
fn upgrade_warns_about_a_workflow_applied_before_a_later_item_failed() {
let sb = Sandbox::new();
sb.write_workflow("alpha", "alpha");
sb.write_workflow("zbad", "zbad");
sb.commit_src("add workflows");
let spec = sb.src_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
assert!(sb.mind(&["learn", "workflow:alpha"]).success);
assert!(sb.mind(&["learn", "workflow:zbad"]).success);
sb.write_workflow("alpha", "renamed-upstream");
sb.write_workflow_raw(
"zbad",
"export const meta = {\n name: 'zbad',\n description: 'Bad'\n}\n\
// {{ns:no-such-sibling}}\n",
);
sb.commit_src("diverge alpha, break zbad");
assert!(sb.mind(&["sync"]).success);
let up = sb.mind(&["upgrade", "--yes"]);
assert!(
!up.success,
"the batch must fail on zbad's bad reference: stdout={} stderr={}",
up.stdout, up.stderr
);
let installed = std::fs::read_to_string(sb.claude_home.join("workflows/alpha.js"))
.expect("alpha must still be installed");
assert!(
installed.contains("name: 'renamed-upstream'"),
"alpha's upgrade must have been applied before zbad failed: {installed}"
);
let recall = sb.mind(&["recall"]);
assert!(
recall.success && recall.stdout.contains("alpha"),
"alpha must remain recorded after the failed batch: {} {}",
recall.stdout,
recall.stderr
);
assert!(
up.stderr
.contains("the harness resolves it as 'renamed-upstream'"),
"the workflow applied before the batch failed must still be warned \
about -- no later run will look at it again: {}",
up.stderr
);
}
#[test]
fn upgrade_warns_when_the_old_item_fails_to_uninstall_after_a_rename() {
let sb = Sandbox::new();
sb.write_workflow("deploy", "deploy");
write_file(
&sb.src.join("mind.toml"),
"[[items]]\nkind = \"workflow\"\nname = \"deploy\"\n\
path = \"workflows/deploy.js\"\nuninstall = \"exit 3\"\n",
);
sb.commit_src("declare the workflow with a failing uninstall hook");
let spec = sb.src_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
assert!(sb.mind(&["learn", "workflow:deploy"]).success);
write_file(
&sb.src.join("mind.toml"),
"[source]\nprefix = \"jk\"\n\n\
[[items]]\nkind = \"workflow\"\nname = \"deploy\"\n\
path = \"workflows/deploy.js\"\nuninstall = \"exit 3\"\n",
);
sb.commit_src("add a prefix");
assert!(sb.mind(&["sync"]).success);
let up = sb.mind_interactive(&["upgrade", "--yes"], "y\n");
assert!(
!up.success,
"the failing uninstall hook must fail the run: stdout={} stderr={}",
up.stdout, up.stderr
);
assert!(
sb.claude_home.join("workflows/jk:deploy.js").exists(),
"the renamed copy is live on disk before the failure: stdout={} stderr={}",
up.stdout,
up.stderr
);
assert!(
up.stderr.contains("the harness resolves it as 'deploy'"),
"the divergence the rename introduced must be reported by the run that \
introduced it: {}",
up.stderr
);
}
#[test]
fn an_over_cap_workflow_installs_and_leaves_its_source_scannable() {
let sb = Sandbox::new();
sb.write_workflow("fine", "fine");
let mut huge = workflow_js("huge", "huge");
huge.push_str("// ");
huge.push_str(&"p".repeat(8 * 1024 * 1024));
huge.push('\n');
sb.write_workflow_raw("huge", &huge);
sb.commit_src("add workflows");
let spec = sb.src_spec();
assert!(sb.mind(&["meld", &spec, "--register-only"]).success);
let fine = sb.mind(&["learn", "workflow:fine"]);
assert!(
fine.success,
"the healthy sibling must still be installable: stdout={} stderr={}",
fine.stdout, fine.stderr
);
assert!(
!fine.stderr.contains("8 MiB size cap"),
"scanning the source must not fail on the oversized sibling: {}",
fine.stderr
);
let huge_run = sb.mind(&["learn", "workflow:huge"]);
assert!(
huge_run.success,
"an unloadable workflow installs anyway (WF-31): stdout={} stderr={}",
huge_run.stdout, huge_run.stderr
);
assert!(
sb.claude_home.join("workflows/huge.js").exists(),
"the oversized workflow must be linked into the lobe"
);
assert!(
huge_run
.stderr
.contains("over mind's own 8 MiB metadata read cap"),
"mind's own cap is warned about as mind's own, not as an error: {}",
huge_run.stderr
);
assert!(
huge_run.stderr.contains("--max-metadata-size"),
"and names the flag that raises it: {}",
huge_run.stderr
);
assert!(
!huge_run
.stderr
.contains("the harness will not load this workflow: mind read no"),
"a file mind never read is not a file mind can call unloadable: {}",
huge_run.stderr
);
assert!(
!huge_run.stderr.contains("8 MiB size cap"),
"the cap bounds mind's read; it is not reported as a failure: {}",
huge_run.stderr
);
}