blob: 7d02f4509253c7cccdf17b14436954a01d17dc91 [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 base64::{Engine, engine::general_purpose::STANDARD};
use std::path::Path;
// Git authors fixtures and supplies an independent status oracle. Agent values
// come from the public ApplicationClient through the real Session scope.
fn git(workspace: &Path, arguments: &[&str]) {
let output = std::process::Command::new("git")
.arg("-C")
.arg(workspace)
.args(arguments)
.env("GIT_CONFIG_NOSYSTEM", "1")
.env(
"GIT_CONFIG_GLOBAL",
if cfg!(windows) { "NUL" } else { "/dev/null" },
)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_INDEX_FILE")
.output()
.unwrap();
assert!(
output.status.success(),
"git {arguments:?}: {}",
String::from_utf8_lossy(&output.stderr)
);
}
fn repository(workspace: &Path) {
git(workspace, &["init", "--quiet", "--initial-branch=main"]);
git(
workspace,
&["config", "user.email", "git-fixture@example.invalid"],
);
git(workspace, &["config", "user.name", "Git fixture"]);
git(workspace, &["config", "core.autocrlf", "false"]);
// The workspace identity marker is fixture infrastructure, not a user edit.
std::fs::write(
workspace.join(".git/info/exclude"),
format!("/{}\n", maka_fs_tools::workspace::MARKER_FILE),
)
.unwrap();
}
fn path(name: &str) -> Value {
json!({"bytesBase64": STANDARD.encode(name.as_bytes())})
}
fn changed_lines(result: &Value) -> Vec<&str> {
assert_eq!(result["kind"], "text", "{result}");
result["patch"]
.as_str()
.unwrap()
.lines()
.filter(|line| {
(line.starts_with('+') && !line.starts_with("+++"))
|| (line.starts_with('-') && !line.starts_with("---"))
})
.collect()
}
#[tokio::test]
async fn selected_directory_create_excludes_workspace_marker_from_agent_and_git_status() {
tokio::time::timeout(Duration::from_secs(45), async {
let f = Fixture::new("http://127.0.0.1:1/v1", None).await;
// Unlike repository(), this leaves marker exclusion to actual create.
git(&f.workspace, &["init", "--quiet", "--initial-branch=main"]);
let user_file = "literal-user-untracked.txt";
std::fs::write(f.workspace.join(user_file), "user file\n").unwrap();
let input = f.create_input("Selected Git directory").await;
let created = f.mutation(2, "create", input, Uuid::new_v4()).await;
let status = match created
.as_ref()
.ok()
.and_then(|value| value["session"]["id"].as_str())
{
Some(session) => Some(f.query("gitStatus", json!({"sessionId":session})).await),
None => None,
};
let marker = maka_fs_tools::workspace::MARKER_FILE;
let marker_exists = f.workspace.join(marker).is_file();
let oracle = std::process::Command::new("git")
.arg("-C")
.arg(&f.workspace)
.args(["status", "--porcelain", "--untracked-files=all"])
.env("GIT_CONFIG_NOSYSTEM", "1")
.env(
"GIT_CONFIG_GLOBAL",
if cfg!(windows) { "NUL" } else { "/dev/null" },
)
.env_remove("GIT_DIR")
.env_remove("GIT_WORK_TREE")
.env_remove("GIT_INDEX_FILE")
.output();
// Complete real Host cleanup before the expected marker-leak RED.
let app_shutdown = f.app.shutdown().await;
let config_shutdown = f.config.shutdown().await;
app_shutdown.unwrap();
config_shutdown.unwrap();
let created = created.expect("selected-directory create must succeed");
assert_eq!(created["kind"], "created", "{created}");
assert!(
marker_exists,
"create must preserve intrinsic workspace identity"
);
let status = status
.expect("created Session ID")
.expect("Agent gitStatus");
let entries = status["entries"].as_array().unwrap();
let oracle = oracle.expect("real Git status oracle");
assert!(
oracle.status.success(),
"{}",
String::from_utf8_lossy(&oracle.stderr)
);
let porcelain = String::from_utf8(oracle.stdout).unwrap();
assert!(
entries
.iter()
.any(|entry| entry["area"] == "untracked" && entry["path"] == path(user_file)),
"{status}"
);
assert!(
porcelain
.lines()
.any(|line| line == format!("?? {user_file}")),
"{porcelain}"
);
let agent_has_marker = entries.iter().any(|entry| entry["path"] == path(marker));
let git_has_marker = porcelain.lines().any(|line| line == format!("?? {marker}"));
assert!(
!agent_has_marker && !git_has_marker,
"workspace marker leaked: Agent={status}, Git={porcelain}"
);
})
.await
.unwrap();
}
#[tokio::test]
async fn git_status_and_diff_read_current_workspace_without_mutating_index() {
tokio::time::timeout(Duration::from_secs(45), async {
let f = Fixture::new("http://127.0.0.1:1/v1", None).await;
let session = f.create("Git comparison").await;
repository(&f.workspace);
std::fs::write(f.workspace.join("tracked.txt"), "base\n").unwrap();
git(&f.workspace, &["add", "tracked.txt"]);
git(&f.workspace, &["commit", "--quiet", "-m", "base"]);
std::fs::write(f.workspace.join("tracked.txt"), "staged\n").unwrap();
git(&f.workspace, &["add", "tracked.txt"]);
std::fs::write(f.workspace.join("tracked.txt"), "worktree\n").unwrap();
std::fs::write(f.workspace.join("new.txt"), "new file\n").unwrap();
let index = f.workspace.join(".git/index");
let index_bytes = std::fs::read(&index).unwrap();
let index_modified = std::fs::metadata(&index).unwrap().modified().unwrap();
let observed = f.query("gitStatus", json!({"sessionId":session})).await;
// A preimplementation missing-method RED still closes the real runtime.
if observed.is_err() {
f.app.shutdown().await.unwrap();
f.config.shutdown().await.unwrap();
}
let status = observed.expect("Agent gitStatus must read the selected Session repository");
let mut entries = status["entries"].as_array().unwrap().clone();
entries.sort_by_key(|entry| {
(
entry["area"].as_str().unwrap().to_owned(),
entry["path"].to_string(),
)
});
let mut status = status;
status["entries"] = json!(entries);
let head = std::fs::read_to_string(f.workspace.join(".git/refs/heads/main")).unwrap();
assert_eq!(
status,
json!({"head":head.trim(),"entries":[
{"area":"staged","path":{"bytesBase64":"dHJhY2tlZC50eHQ="},"kind":"modified"},
{"area":"untracked","path":{"bytesBase64":"bmV3LnR4dA=="},"kind":"added"},
{"area":"worktree","path":{"bytesBase64":"dHJhY2tlZC50eHQ="},"kind":"modified"},
]})
);
for (area, name, expected) in [
("staged", "tracked.txt", vec!["-base", "+staged"]),
("worktree", "tracked.txt", vec!["-staged", "+worktree"]),
("untracked", "new.txt", vec!["+new file"]),
] {
let diff = f
.query(
"gitDiff",
json!({"sessionId":session,"area":area,"path":path(name)}),
)
.await
.unwrap();
assert_eq!(changed_lines(&diff), expected, "{area}");
}
std::fs::write(f.workspace.join("tracked.txt"), "later edit\n").unwrap();
let fresh = f
.query(
"gitDiff",
json!({"sessionId":session,"area":"worktree","path":path("tracked.txt")}),
)
.await
.unwrap();
assert_eq!(changed_lines(&fresh), vec!["-staged", "+later edit"]);
std::fs::remove_file(f.workspace.join("new.txt")).unwrap();
let fresh_status = f
.query("gitStatus", json!({"sessionId":session}))
.await
.unwrap();
let mut areas: Vec<_> = fresh_status["entries"]
.as_array()
.unwrap()
.iter()
.map(|entry| entry["area"].as_str().unwrap())
.collect();
areas.sort();
assert_eq!(areas, ["staged", "worktree"]);
assert_eq!(std::fs::read(&index).unwrap(), index_bytes);
assert_eq!(
std::fs::metadata(&index).unwrap().modified().unwrap(),
index_modified
);
assert!(!f.workspace.join(".git/index.lock").exists());
assert_eq!(
std::fs::read_to_string(f.workspace.join("tracked.txt")).unwrap(),
"later edit\n"
);
f.app.shutdown().await.unwrap();
f.config.shutdown().await.unwrap();
})
.await
.unwrap();
}
#[tokio::test]
async fn git_status_returns_every_literal_entry_through_public_host_without_mutating_repository() {
let f = Fixture::new("http://127.0.0.1:1/v1", None).await;
let session = f.create("Complete Git status").await;
repository(&f.workspace);
let staged = "staged-change.txt";
let worktree = "worktree-change.txt";
std::fs::write(f.workspace.join(staged), "staged base\n").unwrap();
std::fs::write(f.workspace.join(worktree), "worktree base\n").unwrap();
git(&f.workspace, &["add", staged, worktree]);
git(&f.workspace, &["commit", "--quiet", "-m", "status base"]);
std::fs::write(f.workspace.join(staged), "staged current\n").unwrap();
git(&f.workspace, &["add", staged]);
std::fs::write(f.workspace.join(worktree), "worktree current\n").unwrap();
let names = (0..1_800)
.map(|number| format!("literal-{number:04}-{}-中文.txt", "x".repeat(190)))
.collect::<Vec<_>>();
for name in &names {
std::fs::write(f.workspace.join(name), "literal untracked contents\n").unwrap();
}
let head = std::fs::read_to_string(f.workspace.join(".git/refs/heads/main")).unwrap();
let mut expected_entries = vec![
json!({"area":"staged","path":path(staged),"kind":"modified"}),
json!({"area":"worktree","path":path(worktree),"kind":"modified"}),
];
expected_entries.extend(
names
.iter()
.map(|name| json!({"area":"untracked","path":path(name),"kind":"added"})),
);
let expected = json!({"head":head.trim(),"entries":expected_entries});
let preserved = [
".git/HEAD",
".git/refs/heads/main",
".git/index",
staged,
worktree,
]
.into_iter()
.chain(names.iter().map(String::as_str))
.map(|name| {
let file = f.workspace.join(name);
let bytes = std::fs::read(&file).unwrap();
let modified = std::fs::metadata(&file).unwrap().modified().unwrap();
(file, bytes, modified)
})
.collect::<Vec<_>>();
let observed = f.query("gitStatus", json!({"sessionId":session})).await;
// Retain the query outcome and close both real Host owners before any RED assertion.
let app_shutdown = f.app.shutdown().await;
let config_shutdown = f.config.shutdown().await;
app_shutdown.unwrap();
config_shutdown.unwrap();
for (file, bytes, modified) in preserved {
assert_eq!(
std::fs::read(&file).unwrap(),
bytes,
"query changed {}",
file.display()
);
assert_eq!(
std::fs::metadata(&file).unwrap().modified().unwrap(),
modified,
"query touched {}",
file.display()
);
}
assert!(!f.workspace.join(".git/index.lock").exists());
assert!(serde_json::to_vec(&expected).unwrap().len() > 512 * 1024);
let status = observed.expect("public Host gitStatus must deliver every repository entry");
assert_eq!(status["head"], head.trim());
assert_eq!(
status["entries"].as_array().unwrap().len(),
1_802,
"public Host gitStatus must not return a bounded prefix"
);
for (number, (actual, expected)) in status["entries"]
.as_array()
.unwrap()
.iter()
.zip(expected["entries"].as_array().unwrap())
.enumerate()
{
assert_eq!(
actual, expected,
"complete public Host status entry {number}"
);
}
assert!(
status == expected,
"complete public Host gitStatus must have exactly the expected head/entries shape"
);
}
#[tokio::test]
async fn git_diff_returns_complete_large_utf8_comparisons_without_mutating_repository() {
let f = Fixture::new("http://127.0.0.1:1/v1", None).await;
let session = f.create("Large Git comparisons").await;
repository(&f.workspace);
let tracked = "large-tracked.txt";
let untracked = "large-new.txt";
let base: String = (0..65_537)
.map(|line| format!("row-{line:08} 中文😀 payload\n"))
.collect();
let staged = base.replace("row-00032768 中文😀 payload", "staged 中文😀 change");
let working = staged.replace("row-00049152 中文😀 payload", "worktree 中文😀 change");
let added: String = (0..25_000)
.map(|line| format!("new-row-{line:08} 中文😀\n"))
.collect();
std::fs::write(f.workspace.join(tracked), &base).unwrap();
git(&f.workspace, &["add", tracked]);
git(&f.workspace, &["commit", "--quiet", "-m", "large base"]);
std::fs::write(f.workspace.join(tracked), &staged).unwrap();
git(&f.workspace, &["add", tracked]);
std::fs::write(f.workspace.join(tracked), &working).unwrap();
std::fs::write(f.workspace.join(untracked), &added).unwrap();
let preserved = [
".git/HEAD",
".git/refs/heads/main",
".git/index",
tracked,
untracked,
]
.into_iter()
.map(|name| {
let file = f.workspace.join(name);
let bytes = std::fs::read(&file).unwrap();
let modified = std::fs::metadata(&file).unwrap().modified().unwrap();
(file, bytes, modified)
})
.collect::<Vec<_>>();
let mut results = Vec::new();
for (area, name) in [
("staged", tracked),
("worktree", tracked),
("untracked", untracked),
] {
results.push(
f.query(
"gitDiff",
json!({"sessionId":session,"area":area,"path":path(name)}),
)
.await,
);
}
// Await both real runtime owners before asserting the expected baseline refusals.
let app_shutdown = f.app.shutdown().await;
let config_shutdown = f.config.shutdown().await;
app_shutdown.unwrap();
config_shutdown.unwrap();
for (file, bytes, modified) in preserved {
assert_eq!(
std::fs::read(&file).unwrap(),
bytes,
"query changed {}",
file.display()
);
assert_eq!(
std::fs::metadata(&file).unwrap().modified().unwrap(),
modified,
"query touched {}",
file.display()
);
}
assert!(!f.workspace.join(".git/index.lock").exists());
assert!(base.len() > 1024 * 1024 && staged.len() > 1024 * 1024 && working.len() > 1024 * 1024);
assert!(added.len() > 512 * 1024 && added.len() < 1024 * 1024);
let results = results
.into_iter()
.collect::<Result<Vec<_>, _>>()
.expect("public Host gitDiff queries must return full values");
assert_eq!(
results
.iter()
.map(|result| result["kind"].clone())
.collect::<Vec<_>>(),
vec![json!("text"); 3],
"large staged, worktree and untracked comparisons must all be text"
);
assert_eq!(
changed_lines(&results[0]),
["-row-00032768 中文😀 payload", "+staged 中文😀 change"]
);
assert_eq!(
changed_lines(&results[1]),
["-row-00049152 中文😀 payload", "+worktree 中文😀 change"]
);
assert!(results[2]["patch"].as_str().unwrap().len() > 512 * 1024);
assert!(serde_json::to_vec(&results[2]).unwrap().len() > 512 * 1024);
for (result, name, before, expected) in [
(&results[0], tracked, Some(base.as_str()), staged.as_str()),
(
&results[1],
tracked,
Some(staged.as_str()),
working.as_str(),
),
(&results[2], untracked, None, added.as_str()),
] {
// Real Git independently checks every hunk, header path and original line;
// exact destination bytes detect truncation anywhere in the returned patch.
let oracle = tempfile::tempdir().unwrap();
if let Some(before) = before {
std::fs::write(oracle.path().join(name), before).unwrap();
}
let patch = oracle.path().join("input.patch");
std::fs::write(&patch, result["patch"].as_str().unwrap()).unwrap();
git(oracle.path(), &["apply", "--", patch.to_str().unwrap()]);
assert_eq!(
std::fs::read(oracle.path().join(name)).unwrap(),
expected.as_bytes(),
"complete patch for {name}"
);
}
}
#[tokio::test]
async fn git_queries_keep_unborn_binary_results_within_session_and_method_grants() {
tokio::time::timeout(Duration::from_secs(45), async {
let f = Fixture::new("http://127.0.0.1:1/v1", None).await;
let session = f.create("Unborn Git").await;
assert_eq!(f.query("gitStatus", json!({"sessionId":session})).await.unwrap(), Value::Null);
assert_eq!(f.query("gitDiff", json!({"sessionId":session,"area":"untracked","path":path("binary.dat")})).await.unwrap(), Value::Null);
repository(&f.workspace);
std::fs::write(f.workspace.join("binary.dat"), b"before\0after").unwrap();
git(&f.workspace, &["add", "binary.dat"]);
let expected = json!({"head":null,"entries":[{"area":"staged","path":{"bytesBase64":"YmluYXJ5LmRhdA=="},"kind":"added"}]});
assert_eq!(f.query("gitStatus", json!({"sessionId":session})).await.unwrap(), expected);
let input = json!({"sessionId":session,"area":"staged","path":path("binary.dat")});
assert_eq!(f.query("gitDiff", input.clone()).await.unwrap(), json!({"kind":"binary"}));
for (method, input) in [("gitStatus", json!({"sessionId":session})), ("gitDiff", input)] {
let denied = f.app.client().start(f.binding(2).await, method.into(), input.clone(),
scope(&f.issuer, false), Attachments::new().into()).await;
assert_eq!(denied.err().unwrap(), "denied", "{method} must retain its actual Host method grant");
let mut foreign = input;
foreign["sessionId"] = json!("foreign-session");
assert_eq!(f.query(method, foreign).await.unwrap_err(), "agent_session_unavailable", "{method}");
}
assert_eq!(f.query("gitStatus", json!({"sessionId":session})).await.unwrap(), expected);
f.app.shutdown().await.unwrap();
f.config.shutdown().await.unwrap();
}).await.unwrap();
}
#[tokio::test]
async fn git_queries_reject_selected_subdirectories_and_replaced_workspace_identity() {
tokio::time::timeout(Duration::from_secs(45), async {
let f = Fixture::new("http://127.0.0.1:1/v1", None).await;
let root_session = f.create("Repository root").await;
repository(&f.workspace);
std::fs::create_dir(f.workspace.join("selected")).unwrap();
std::fs::write(f.workspace.join("selected/inside.txt"), "inside base\n").unwrap();
std::fs::write(f.workspace.join("sibling.txt"), "sibling base\n").unwrap();
git(&f.workspace, &["add", "selected/inside.txt", "sibling.txt"]);
git(&f.workspace, &["commit", "--quiet", "-m", "base"]);
std::fs::write(f.workspace.join("selected/inside.txt"), "inside current\n").unwrap();
std::fs::write(f.workspace.join("sibling.txt"), "private sibling current\n").unwrap();
let root_diff = f
.query(
"gitDiff",
json!({"sessionId":root_session,"area":"worktree","path":path("sibling.txt")}),
)
.await
.unwrap();
assert_eq!(
changed_lines(&root_diff),
["-sibling base", "+private sibling current"]
);
let roots = f
.query("directories", json!({"kind":"roots"}))
.await
.unwrap();
let children = f
.query(
"directories",
json!({"kind":"children","selection":roots["roots"][0]["selection"],"after":null}),
)
.await
.unwrap();
let selected = children["children"]
.as_array()
.unwrap()
.iter()
.find(|entry| {
Path::new(entry["path"].as_str().unwrap()).file_name()
== Some(std::ffi::OsStr::new("selected"))
})
.unwrap();
let child = f
.mutation(
2,
"create",
json!({"title":"Selected child","labels":[],
"workspace":{"kind":"directory","selection":selected["selection"]}}),
Uuid::new_v4(),
)
.await
.unwrap();
assert_eq!(child["kind"], "created");
let child_session = &child["session"]["id"];
for (method, input) in [
("gitStatus", json!({"sessionId":child_session})),
(
"gitDiff",
json!({"sessionId":child_session,"area":"worktree","path":path("inside.txt")}),
),
] {
let error = f.query(method, input).await.unwrap_err();
assert_eq!(error, "agent_git_unsupported", "{method}");
assert!(!error.contains("private sibling current"));
}
let original_marker =
std::fs::read(f.workspace.join(maka_fs_tools::workspace::MARKER_FILE)).unwrap();
let moved = f.workspace.with_file_name("original-workspace");
std::fs::rename(&f.workspace, &moved).unwrap();
std::fs::create_dir(&f.workspace).unwrap();
std::fs::write(
f.workspace.join(maka_fs_tools::workspace::MARKER_FILE),
original_marker,
)
.unwrap();
repository(&f.workspace);
std::fs::write(f.workspace.join("replacement.txt"), "must not retarget\n").unwrap();
for (method, input) in [
("gitStatus", json!({"sessionId":root_session})),
(
"gitDiff",
json!({"sessionId":root_session,"area":"untracked","path":path("replacement.txt")}),
),
] {
assert_eq!(
f.query(method, input).await.unwrap_err(),
"agent_workspace_unavailable",
"{method} must validate the original workspace identity"
);
}
assert_eq!(
std::fs::read_to_string(moved.join("sibling.txt")).unwrap(),
"private sibling current\n"
);
f.app.shutdown().await.unwrap();
f.config.shutdown().await.unwrap();
})
.await
.unwrap();
}
#[cfg(unix)]
#[tokio::test]
async fn git_diff_reads_link_text_without_following_the_target_outside_its_workspace() {
tokio::time::timeout(Duration::from_secs(45), async {
use std::os::unix::fs::symlink;
let f = Fixture::new("http://127.0.0.1:1/v1", None).await;
let session = f.create("Link text").await;
repository(&f.workspace);
std::fs::write(f.root.join("private-secret"), "private target payload\n").unwrap();
symlink("../old-target", f.workspace.join("link.txt")).unwrap();
git(&f.workspace, &["add", "link.txt"]);
git(&f.workspace, &["commit", "--quiet", "-m", "base link"]);
std::fs::remove_file(f.workspace.join("link.txt")).unwrap();
symlink("../root/private-secret", f.workspace.join("link.txt")).unwrap();
let diff = f
.query(
"gitDiff",
json!({"sessionId":session,"area":"worktree","path":path("link.txt")}),
)
.await
.unwrap();
assert_eq!(
changed_lines(&diff),
["-../old-target", "+../root/private-secret"]
);
assert!(
!diff["patch"]
.as_str()
.unwrap()
.contains("private target payload")
);
assert_eq!(
std::fs::read_to_string(f.root.join("private-secret")).unwrap(),
"private target payload\n"
);
f.app.shutdown().await.unwrap();
f.config.shutdown().await.unwrap();
})
.await
.unwrap();
}
#[cfg(unix)]
#[tokio::test]
async fn git_path_bytes_roundtrip_through_public_status_and_diff() {
tokio::time::timeout(Duration::from_secs(45), async {
use std::os::unix::ffi::OsStringExt;
let f = Fixture::new("http://127.0.0.1:1/v1", None).await;
let session = f.create("Opaque Git paths").await;
repository(&f.workspace);
// APFS rejects invalid UTF-8 filenames; still exercise exact legal
// bytes that cannot safely be treated as shell syntax or quoted text.
#[cfg(target_os = "macos")]
let (bytes, expected) = (
"odd-中文\n\t\\.txt".as_bytes().to_vec(),
"b2RkLeS4reaWhwoJXC50eHQ=",
);
#[cfg(not(target_os = "macos"))]
let (bytes, expected) = (b"odd-\xff\n\t.txt".to_vec(), "b2RkLf8KCS50eHQ=");
let name = std::ffi::OsString::from_vec(bytes);
std::fs::write(f.workspace.join(name), "exact file contents\n").unwrap();
let status = f
.query("gitStatus", json!({"sessionId":session}))
.await
.unwrap();
assert_eq!(
status["entries"],
json!([{"area":"untracked","path":{"bytesBase64":expected},"kind":"added"}])
);
let diff = f
.query(
"gitDiff",
json!({"sessionId":session,"area":"untracked","path":status["entries"][0]["path"]}),
)
.await
.unwrap();
assert_eq!(changed_lines(&diff), ["+exact file contents"]);
f.app.shutdown().await.unwrap();
f.config.shutdown().await.unwrap();
})
.await
.unwrap();
}
#[tokio::test]
async fn git_queries_use_the_selected_managed_linked_worktree_not_its_source_repository() {
tokio::time::timeout(Duration::from_secs(45), async {
let f = Fixture::configured("http://127.0.0.1:1/v1", None, None, None, |settings| {
settings.isolated_workspaces = true
})
.await;
let source_session = f.create("Source repository").await;
repository(&f.workspace);
std::fs::write(f.workspace.join("tracked.txt"), "source base\n").unwrap();
git(&f.workspace, &["add", "tracked.txt"]);
git(&f.workspace, &["commit", "--quiet", "-m", "source base"]);
let mut input = f.create_input("Managed checkout").await;
input["workspace"]["kind"] = json!("isolated_git");
let created = f
.mutation(2, "create", input, Uuid::new_v4())
.await
.unwrap();
assert_eq!(created["kind"], "created", "{created}");
let managed =
std::path::PathBuf::from(created["session"]["workspace"]["path"].as_str().unwrap());
assert!(
managed.join(".git").is_file(),
"exercise an actual linked worktree"
);
std::fs::write(managed.join("tracked.txt"), "managed current\n").unwrap();
let session = &created["session"]["id"];
let diff = f
.query(
"gitDiff",
json!({"sessionId":session,"area":"worktree","path":path("tracked.txt")}),
)
.await
.unwrap();
assert_eq!(changed_lines(&diff), ["-source base", "+managed current"]);
// Public browsing shares this exact managed-worktree exception, while
// the private parent and administration directories remain unreachable.
let files = f
.query(
"listFiles",
json!({
"sessionId":session,"path":null,"after":null,
}),
)
.await
.unwrap();
assert_eq!(
files["entries"],
json!([
{"path":{"bytesBase64":"dHJhY2tlZC50eHQ="},"kind":"file"},
])
);
assert_eq!(files["next"], Value::Null);
assert!(
f.query(
"listFiles",
json!({
"sessionId":session,"path":path("../"),"after":null,
})
)
.await
.is_err()
);
let status = f
.query("gitStatus", json!({"sessionId":session}))
.await
.unwrap();
assert_eq!(
status["entries"],
json!([{"area":"worktree","path":{"bytesBase64":"dHJhY2tlZC50eHQ="},"kind":"modified"}])
);
assert_eq!(
f.query("gitStatus", json!({"sessionId":source_session}))
.await
.unwrap()["entries"],
json!([])
);
assert_eq!(
std::fs::read_to_string(f.workspace.join("tracked.txt")).unwrap(),
"source base\n"
);
f.app.shutdown().await.unwrap();
f.config.shutdown().await.unwrap();
})
.await
.unwrap();
}