fix: complete multi-workdir isolation and launch UI
This commit is contained in:
@@ -52,7 +52,9 @@ impl Tool for EditTool {
|
||||
params.target_workdir.as_deref(),
|
||||
workdir::WorkdirSessionCapability::Edit,
|
||||
)?;
|
||||
let tracker = self.tracker.scoped(selected.alias.as_str());
|
||||
let tracker = self
|
||||
.tracker
|
||||
.scoped_attachment(&selected.alias, selected.generation);
|
||||
let path = WorkdirPath::new(¶ms.file_path).map_err(ToolsError::from)?;
|
||||
tracing::debug!(path = %path, replace_all = params.replace_all, "Edit");
|
||||
|
||||
|
||||
@@ -53,7 +53,9 @@ impl Tool for ReadTool {
|
||||
params.target_workdir.as_deref(),
|
||||
workdir::WorkdirSessionCapability::Read,
|
||||
)?;
|
||||
let tracker = self.tracker.scoped(selected.alias.as_str());
|
||||
let tracker = self
|
||||
.tracker
|
||||
.scoped_attachment(&selected.alias, selected.generation);
|
||||
let offset = params.offset.unwrap_or(0);
|
||||
let limit = params.limit.unwrap_or(DEFAULT_LIMIT).max(1);
|
||||
|
||||
@@ -216,7 +218,7 @@ mod tests {
|
||||
// History recorded
|
||||
assert!(
|
||||
tracker
|
||||
.scoped("workdir")
|
||||
.scoped_attachment(&workdir::WorkdirAttachmentAlias::new("workdir").unwrap(), 0,)
|
||||
.expected_workdir_hash(&WorkdirPath::new("a.txt").unwrap())
|
||||
.is_ok()
|
||||
);
|
||||
@@ -247,7 +249,7 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_tool_missing_file() {
|
||||
let (dir, fs, tracker) = setup();
|
||||
let (_dir, fs, tracker) = setup();
|
||||
let def = read_tool(fs, tracker);
|
||||
let (_, tool) = def();
|
||||
let input = serde_json::json!({
|
||||
|
||||
@@ -161,8 +161,9 @@ impl Tracker {
|
||||
}
|
||||
|
||||
/// Return a view whose read-before-mutation state is isolated to one
|
||||
/// Worker-local attachment alias while aggregate recency/change statistics
|
||||
/// remain shared with the Worker tracker.
|
||||
/// attachment incarnation (the Worker-local alias plus router generation),
|
||||
/// while aggregate recency/change statistics remain shared with the Worker
|
||||
/// tracker.
|
||||
pub fn scoped(&self, namespace: impl Into<String>) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
@@ -171,6 +172,14 @@ impl Tracker {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn scoped_attachment(
|
||||
&self,
|
||||
alias: &workdir::WorkdirAttachmentAlias,
|
||||
generation: u64,
|
||||
) -> Self {
|
||||
self.scoped(format!("{}#{generation}", alias.as_str()))
|
||||
}
|
||||
|
||||
fn key(&self, path: PathBuf) -> TrackedPath {
|
||||
TrackedPath {
|
||||
namespace: self.namespace.clone(),
|
||||
|
||||
@@ -49,7 +49,9 @@ impl Tool for WriteTool {
|
||||
params.target_workdir.as_deref(),
|
||||
workdir::WorkdirSessionCapability::Write,
|
||||
)?;
|
||||
let tracker = self.tracker.scoped(selected.alias.as_str());
|
||||
let tracker = self
|
||||
.tracker
|
||||
.scoped_attachment(&selected.alias, selected.generation);
|
||||
let path = WorkdirPath::new(¶ms.file_path).map_err(ToolsError::from)?;
|
||||
tracing::debug!(path = %path, bytes = params.content.len(), "Write");
|
||||
|
||||
|
||||
@@ -339,6 +339,66 @@ async fn routed_tools_observe_detach_after_registration() {
|
||||
router.close_all().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn detach_and_reattach_same_alias_requires_a_fresh_read() {
|
||||
let first = TempDir::new().unwrap();
|
||||
let second = TempDir::new().unwrap();
|
||||
let spill = TempDir::new().unwrap();
|
||||
std::fs::write(first.path().join("same.txt"), "same bytes\n").unwrap();
|
||||
std::fs::write(second.path().join("same.txt"), "same bytes\n").unwrap();
|
||||
|
||||
let router = Arc::new(WorkdirSessionRouter::new());
|
||||
let alias = WorkdirAttachmentAlias::new("checkout").unwrap();
|
||||
router
|
||||
.attach(
|
||||
alias.clone(),
|
||||
Arc::new(LocalWorkdirSession::new(
|
||||
scope_with_spill(first.path(), spill.path()),
|
||||
first.path().to_path_buf(),
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
let reg = Registry::new(routed_builtin_tools(
|
||||
router.clone(),
|
||||
Tracker::new(),
|
||||
spill.path().to_path_buf(),
|
||||
));
|
||||
call(
|
||||
®.get("Read"),
|
||||
json!({ "target_workdir": "checkout", "file_path": "same.txt" }),
|
||||
)
|
||||
.await;
|
||||
|
||||
router.detach(&alias).await.unwrap();
|
||||
router
|
||||
.attach(
|
||||
alias.clone(),
|
||||
Arc::new(LocalWorkdirSession::new(
|
||||
scope_with_spill(second.path(), spill.path()),
|
||||
second.path().to_path_buf(),
|
||||
)),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let error = call_err(
|
||||
®.get("Edit"),
|
||||
json!({
|
||||
"target_workdir": "checkout",
|
||||
"file_path": "same.txt",
|
||||
"old_string": "same",
|
||||
"new_string": "changed"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert!(error.to_string().contains("has not been read"), "{error}");
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(second.path().join("same.txt")).unwrap(),
|
||||
"same bytes\n"
|
||||
);
|
||||
|
||||
router.close_all().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn view_image_reads_scoped_bytes_into_durable_tool_detail() {
|
||||
let dir = TempDir::new().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user