blob: 2f1b0d01c1c1d7adf0de5c30ed3c90d32f69baa7 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::*;
use std::path::Path;
pub(super) async fn preferences(path: &Path) -> preferences::Store {
preferences::Store::new(
Arc::new(
maka_event_log::EventLog::open(
&path.join(format!("preferences-{}.sqlite", uuid::Uuid::new_v4())),
)
.await
.unwrap(),
),
"test_".into(),
)
}
pub(super) fn scope() -> Scope {
maka_capabilities::call::Issuer::default()
.issue(
maka_capabilities::call::Identity::Remote {
request_id: uuid::Uuid::new_v4(),
},
tokio_util::sync::CancellationToken::new(),
)
.unwrap()
}
pub(super) fn write(root: &Path, id: &str, body: &str) {
std::fs::create_dir_all(root.join(id)).unwrap();
std::fs::write(
root.join(id).join("SKILL.md"),
format!("---\nname: {id}\ndescription: Reference for {id}\n---\n{body}"),
)
.unwrap();
}
pub(super) fn source(id: &str, path: &Path) -> Source {
Source {
id: id.into(),
label: id.into(),
path: path.to_str().unwrap().into(),
}
}
fn first() -> List {
List {
workspace: None,
query: String::new(),
cursor: None,
}
}
#[tokio::test]
async fn original_discovery_paging_and_exact_body_reads_reject_stale_or_repurposed_cursors() {
let dir = tempfile::tempdir().unwrap();
let a = dir.path().join("a");
let b = dir.path().join("b");
let body = "本地 skill 说明 🦀\n".repeat(2000);
for i in 0..40 {
write(
&a,
&format!("skill-{i:02}"),
if i == 0 { &body } else { "original" },
);
}
write(&b, "skill-00", "shadowed");
std::fs::create_dir_all(b.join("broken")).unwrap();
std::fs::write(b.join("broken/SKILL.md"), "missing frontmatter").unwrap();
let library = Library::open(
Configuration {
home: None,
project_roots: vec![],
bundled: false,
manage: false,
preferences: true,
sources: vec![source("a", &a), source("b", &b)],
},
dir.path().join("private"),
preferences(dir.path()).await,
)
.unwrap();
let call = scope();
let Page::Page {
revision,
entries,
after,
..
} = library
.snapshot(&call)
.await
.unwrap()
.list(first())
.unwrap()
else {
panic!()
};
assert_eq!(entries.len(), 32);
let cursor = after.unwrap();
assert!(
library
.snapshot(&call)
.await
.unwrap()
.list(List {
workspace: None,
query: "other".into(),
cursor: Some(cursor.clone())
})
.is_err()
);
let Page::Page {
entries: remaining,
after,
..
} = library
.snapshot(&call)
.await
.unwrap()
.list(List {
workspace: None,
query: String::new(),
cursor: Some(cursor),
})
.unwrap()
else {
panic!()
};
assert!(after.is_none());
assert_eq!(entries.len() + remaining.len(), 42);
assert_eq!(
remaining
.iter()
.find(|entry| entry.reference == "source:b:skill-00")
.unwrap()
.shadowed_by
.as_deref(),
Some("source:a:skill-00")
);
assert!(
!remaining
.iter()
.find(|entry| entry.reference == "source:b:broken")
.unwrap()
.valid
);
let mut text = String::new();
let mut offset = 0;
loop {
let Document::Page {
text: page,
after,
total_bytes,
..
} = library
.snapshot(&call)
.await
.unwrap()
.read(Read {
workspace: None,
reference: "source:a:skill-00".into(),
revision: revision.clone(),
offset,
})
.unwrap()
else {
panic!()
};
assert!(page.len() <= TEXT_BYTES);
assert_eq!(total_bytes, body.trim().len());
text.push_str(&page);
let Some(next) = after else {
break;
};
assert!(next > offset);
offset = next;
}
assert_eq!(text, body.trim());
write(&a, "skill-00", "updated");
assert!(matches!(
library
.snapshot(&call)
.await
.unwrap()
.read(Read {
workspace: None,
reference: "source:a:skill-00".into(),
revision,
offset: 0
})
.unwrap(),
Document::Changed
));
call.finish().await.unwrap();
assert!(library.snapshot(&call).await.is_err());
}
#[tokio::test]
async fn selected_directory_handles_and_private_root_ceiling_remain_authoritative() {
let dir = tempfile::tempdir().unwrap();
let selected = dir.path().join("selected");
let private = dir.path().join("private");
let outside = dir.path().join("outside");
write(&selected, "allowed", "captured original");
write(&private, "hidden", "private secret");
write(&outside, "unselected", "outside secret");
#[cfg(unix)]
std::os::unix::fs::symlink(outside.join("unselected"), selected.join("link")).unwrap();
let library = Library::open(
Configuration {
home: None,
project_roots: vec![],
bundled: false,
manage: false,
preferences: true,
sources: vec![source("selected", &selected), source("private", &private)],
},
private.canonicalize().unwrap(),
preferences(dir.path()).await,
)
.unwrap();
std::fs::rename(&selected, dir.path().join("original")).unwrap();
write(&selected, "allowed", "replacement must not redirect");
let call = scope();
let Page::Page {
revision,
entries,
diagnostics,
..
} = library
.snapshot(&call)
.await
.unwrap()
.list(first())
.unwrap()
else {
panic!()
};
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].reference, "source:selected:allowed");
assert!(diagnostics.iter().any(|s| s.starts_with("private:")));
for reference in [
"source:private:hidden",
"source:selected:link",
"source:selected:../../outside/unselected",
] {
assert!(matches!(
library
.snapshot(&call)
.await
.unwrap()
.read(Read {
workspace: None,
reference: reference.into(),
revision: revision.clone(),
offset: 0
})
.unwrap(),
Document::Unavailable
));
}
let Document::Page { text, .. } = library
.snapshot(&call)
.await
.unwrap()
.read(Read {
workspace: None,
reference: entries[0].reference.clone(),
revision,
offset: 0,
})
.unwrap()
else {
panic!()
};
assert_eq!(text, "captured original");
call.finish().await.unwrap();
}
#[tokio::test]
async fn cancellation_never_turns_discovery_failure_into_an_empty_catalog() {
let dir = tempfile::tempdir().unwrap();
write(dir.path(), "one", "content");
let library = Library::open(
Configuration {
home: None,
project_roots: vec![],
bundled: false,
manage: false,
preferences: true,
sources: vec![source("local", dir.path())],
},
dir.path().join("private"),
preferences(dir.path()).await,
)
.unwrap();
let call = scope();
call.cancellation.cancel();
assert!(library.snapshot(&call).await.is_err());
call.finish().await.unwrap();
assert!(
Library::open(
Configuration {
home: None,
project_roots: vec![],
bundled: false,
manage: false,
preferences: true,
sources: vec![source("same", dir.path()), source("same", dir.path())]
},
dir.path().join("private"),
preferences(dir.path()).await
)
.is_err()
);
}
#[tokio::test]
async fn escaped_instruction_pages_remain_within_the_remote_delivery_budget() {
let dir = tempfile::tempdir().unwrap();
let body = "\u{0001}".repeat(TEXT_BYTES * 2);
write(dir.path(), "escaped", &body);
let library = Library::open(
Configuration {
home: None,
project_roots: vec![],
bundled: false,
manage: false,
preferences: true,
sources: vec![source("local", dir.path())],
},
dir.path().join("private"),
preferences(dir.path()).await,
)
.unwrap();
let call = scope();
let snapshot = library.snapshot(&call).await.unwrap();
let revision = snapshot.revision.clone();
let page = snapshot
.read(Read {
workspace: None,
reference: "source:local:escaped".into(),
revision,
offset: 0,
})
.unwrap();
let value = encode(page).unwrap();
assert_eq!(value["text"].as_str().unwrap(), &body[..TEXT_BYTES]);
assert_eq!(value["after"], TEXT_BYTES);
call.finish().await.unwrap();
}
#[tokio::test]
async fn execution_basis_preserves_real_captures_and_rejects_replaced_skill_directories() {
use maka_event_log::{
EventLog,
root::{RootNamespaces, RootOwner},
};
let temporary = tempfile::tempdir().unwrap();
let path = temporary.path();
let root = Arc::new(
RootOwner::create(
&path.join("private"),
&RootNamespaces {
ownership: path.join("owners"),
control: path.join("control"),
},
)
.unwrap(),
);
let store = Arc::new(EventLog::for_root(root.clone()).await.unwrap());
for name in ["home", "projects", "custom"] {
std::fs::create_dir(path.join(name)).unwrap();
}
let config = || Configuration {
home: Some(path.join("home").to_str().unwrap().into()),
project_roots: vec![maka_host_runtime::DirectoryRootSpec {
label: "Projects".into(),
path: path.join("projects"),
}],
bundled: true,
manage: true,
preferences: true,
sources: vec![source("custom", &path.join("custom"))],
};
let open = || {
Library::open(
config(),
root.canonical_path().to_owned(),
preferences::Store::new(store.clone(), "stable/".into()),
)
.unwrap()
};
let mut first = open();
first.managed = Some(
management::Managed::open(root.clone(), "stable".into())
.await
.unwrap(),
);
let basis = first.execution_basis().unwrap();
let mut same = open();
same.managed = Some(
management::Managed::open(root.clone(), "stable".into())
.await
.unwrap(),
);
assert_eq!(same.execution_basis().unwrap(), basis);
write(
&path.join("custom"),
"mutable",
"live contents do not change the target",
);
assert_eq!(first.execution_basis().unwrap(), basis);
let mut other_namespace = first.clone();
other_namespace.preferences = preferences::Store::new(store.clone(), "other/".into());
assert_ne!(other_namespace.execution_basis().unwrap(), basis);
let mut other_managed = first.clone();
other_managed.managed = Some(
management::Managed::open(root.clone(), "other".into())
.await
.unwrap(),
);
assert_ne!(other_managed.execution_basis().unwrap(), basis);
for name in ["custom", "home", "projects"] {
std::fs::rename(path.join(name), path.join(format!("original-{name}"))).unwrap();
std::fs::create_dir(path.join(name)).unwrap();
let mut changed = open();
changed.managed = first.managed.clone();
assert_ne!(changed.execution_basis().unwrap(), basis, "{name}");
assert_eq!(first.execution_basis().unwrap(), basis, "held {name}");
std::fs::remove_dir(path.join(name)).unwrap();
std::fs::rename(path.join(format!("original-{name}")), path.join(name)).unwrap();
}
store.shutdown().await.unwrap();
}