- 20732
) - 20733
.await; - 20734
assert_eq!(response.status(), StatusCode::OK); - 20735
assert!(!dir.path().join("result.json").exists()); - 20736
assert_eq!( - 20737
vak_sandbox::load_records(&sandbox_records_path(&state)) - 20738
.unwrap() - 20739
.len(), - 20740
3 - 20741
); - 20742
} - 20743
- 20744
#[tokio::test] - 20745
async fn candidate_export_records_failed_html_check_without_claiming_target_success() { - 20746
vak_config::paths::isolate_home_for_tests(); - 20747
let dir = tempfile::tempdir().unwrap(); - 20748
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 20749
core.set_sessions_home(dir.path().join("home")); - 20750
seed_bound_result(&core, "session-1", "exec-1"); - 20751
let state = AppState::new(core); - 20752
let scratch = dir.path().join(".vak/scratch/e1"); - 20753
tokio::fs::create_dir_all(&scratch).await.unwrap(); - 20754
tokio::fs::write( - 20755
scratch.join("blank.html"), - 20756
"<!doctype html><html><head><title>Draft</head><body><main>Missing</main></body></html>", - 20757
) - 20758
.await - 20759
.unwrap(); - 20760
- 20761
let candidate = export_candidate(&state).await; - 20762
assert!(candidate.verified, "frozen bytes still have integrity"); - 20763
assert_eq!(candidate.candidate.target_checks.len(), 1); - 20764
assert_eq!(candidate.draft_checks.len(), 1); - 20765
assert_eq!(candidate.draft_checks[0].verifier, "format.html.body"); - 20766
assert_eq!(candidate.draft_checks[0].status, "failed"); - 20767
assert!( - 20768
candidate.draft_checks[0] - 20769
.evidence - 20770
.contains("unclosed <title>") - 20771
); - 20772
} - 20773
- 20774
#[tokio::test] - 20775
async fn promotion_rejects_files_outside_the_saved_candidate() { - 20776
vak_config::paths::isolate_home_for_tests(); - 20777
let dir = tempfile::tempdir().unwrap(); - 20778
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 20779
core.set_sessions_home(dir.path().join("home")); - 20780
seed_bound_result(&core, "session-1", "exec-1"); - 20781
let state = AppState::new(core); - 20782
let scratch = dir.path().join(".vak/scratch/e1"); - 20783
tokio::fs::create_dir_all(&scratch).await.unwrap(); - 20784
tokio::fs::write(scratch.join("result.txt"), "candidate") - 20785
.await - 20786
.unwrap(); - 20787
let candidate = export_candidate(&state).await; - 20788
let response = promote_sandbox_candidate( - 20789
State(state), - 20790
Path("session-1".into()), - 20791
Json(SandboxPromotionBody { - 20792
candidate_id: candidate.candidate.candidate_id, - 20793
files: vec!["unreviewed.txt".into()], - 20794
}), - 20795
) - 20796
.await; - 20797
assert_eq!(response.status(), StatusCode::BAD_REQUEST); - 20798
assert!(!dir.path().join("unreviewed.txt").exists()); - 20799
assert!(!dir.path().join("result.txt").exists()); - 20800
} - 20801
- 20802
#[tokio::test] - 20803
async fn candidate_launch_root_is_session_bound_and_hash_verified() { - 20804
vak_config::paths::isolate_home_for_tests(); - 20805
let dir = tempfile::tempdir().unwrap(); - 20806
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 20807
core.set_sessions_home(dir.path().join("home")); - 20808
seed_bound_result(&core, "session-1", "exec-1"); - 20809
let state = AppState::new(core); - 20810
let scratch = dir.path().join(".vak/scratch/e1"); - 20811
tokio::fs::create_dir_all(&scratch).await.unwrap(); - 20812
tokio::fs::write(scratch.join("invitation.html"), "<h1>Saved</h1>") - 20813
.await - 20814
.unwrap(); - 20815
let candidate = export_candidate(&state).await; - 20816
let candidate_id = candidate.candidate.candidate_id; - 20817
- 20818
let root = launch_root(&state, "session-1", Some(&candidate_id), dir.path()).unwrap(); - 20819
assert_eq!(root, sandbox_candidates_root(&state).join(&candidate_id)); - 20820
let launch = detect_launch(&root); - 20821
assert_eq!(launch.len(), 1); - 20822
assert_eq!(launch[0].name, "static"); - 20823
assert!(launch_root(&state, "another-session", Some(&candidate_id), dir.path()).is_err()); - 20824
- 20825
let prepared = sandbox_previews_root(&state).join(&candidate_id); - 20826
std::fs::create_dir_all(&prepared).unwrap(); - 20827
std::fs::copy( - 20828
root.join("invitation.html"), - 20829
prepared.join("invitation.html"), - 20830
) - 20831
.unwrap(); - 20832
std::fs::write( - 20833
prepared.join(".vak-candidate-digest"), - 20834
&candidate.candidate_digest, - 20835
) - 20836
.unwrap(); - 20837
assert_eq!( - 20838
launch_root(&state, "session-1", Some(&candidate_id), dir.path()).unwrap(), - 20839
prepared - 20840
); - 20841
- 20842
state - 20843
.sessions - 20844
.lock() - 20845
.unwrap_or_else(std::sync::PoisonError::into_inner) - 20846
.remove("session-1"); - 20847
let response = get_launch( - 20848
State(state), - 20849
Path("session-1".into()), - 20850
axum::extract::Query(LaunchScope { - 20851
candidate_id: Some(candidate_id), - 20852
}), - 20853
) - 20854
.await; - 20855
assert_eq!(response.0["servers"][0]["name"], "static"); - 20856
} - 20857
- 20858
#[tokio::test] - 20859
async fn promotion_uses_frozen_candidate_after_scratch_changes() { - 20860
vak_config::paths::isolate_home_for_tests(); - 20861
let dir = tempfile::tempdir().unwrap(); - 20862
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 20863
core.set_sessions_home(dir.path().join("home")); - 20864
seed_bound_result(&core, "session-1", "exec-1"); - 20865
let state = AppState::new(core); - 20866
let scratch = dir.path().join(".vak/scratch/e1"); - 20867
tokio::fs::create_dir_all(&scratch).await.unwrap(); - 20868
tokio::fs::write(scratch.join("result.txt"), "reviewed") - 20869
.await - 20870
.unwrap(); - 20871
let candidate = export_candidate(&state).await; - 20872
tokio::fs::write(scratch.join("result.txt"), "changed") - 20873
.await - 20874
.unwrap(); - 20875
let preview = read_sandbox_candidate_file( - 20876
State(state.clone()), - 20877
Path(("session-1".into(), candidate.candidate.candidate_id.clone())), - 20878
axum::extract::Query(FileQuery { - 20879
path: "result.txt".into(), - 20880
}), - 20881
) - 20882
.await; - 20883
assert_eq!(preview.status(), StatusCode::OK); - 20884
let preview: serde_json::Value = serde_json::from_slice( - 20885
&axum::body::to_bytes(preview.into_body(), 64 * 1024) - 20886
.await - 20887
.unwrap(), - 20888
) - 20889
.unwrap(); - 20890
assert_eq!(preview["content"], "reviewed"); - 20891
let raw = read_sandbox_candidate_file_raw( - 20892
State(state.clone()), - 20893
Path(("session-1".into(), candidate.candidate.candidate_id.clone())), - 20894
axum::extract::Query(FileQuery { - 20895
path: "result.txt".into(), - 20896
}), - 20897
) - 20898
.await; - 20899
assert_eq!(raw.status(), StatusCode::OK); - 20900
assert_eq!( - 20901
axum::body::to_bytes(raw.into_body(), 64 * 1024) - 20902
.await - 20903
.unwrap() - 20904
.as_ref(), - 20905
b"reviewed" - 20906
); - 20907
let wrong_session = read_sandbox_candidate_file( - 20908
State(state.clone()), - 20909
Path(( - 20910
"another-session".into(), - 20911
candidate.candidate.candidate_id.clone(), - 20912
)), - 20913
axum::extract::Query(FileQuery { - 20914
path: "result.txt".into(), - 20915
}), - 20916
) - 20917
.await; - 20918
assert_eq!(wrong_session.status(), StatusCode::NOT_FOUND); - 20919
let saved_file = candidate.candidate.source_root.join("result.txt"); - 20920
let mut permissions = std::fs::metadata(&saved_file).unwrap().permissions(); - 20921
#[cfg(unix)] - 20922
{ - 20923
use std::os::unix::fs::PermissionsExt; - 20924
permissions.set_mode(permissions.mode() | 0o200); - 20925
} - 20926
#[cfg(not(unix))] - 20927
permissions.set_readonly(false); - 20928
std::fs::set_permissions(&saved_file, permissions).unwrap(); - 20929
tokio::fs::write(&saved_file, "tampered").await.unwrap(); - 20930
let tampered = read_sandbox_candidate_file( - 20931
State(state.clone()), - 20932
Path(("session-1".into(), candidate.candidate.candidate_id.clone())), - 20933
axum::extract::Query(FileQuery { - 20934
path: "result.txt".into(), - 20935
}), - 20936
) - 20937
.await; - 20938
assert_eq!(tampered.status(), StatusCode::CONFLICT); - 20939
tokio::fs::write(&saved_file, "reviewed").await.unwrap(); - 20940
let invalid_comment = comment_on_sandbox_candidate( - 20941
State(state.clone()), - 20942
Path(("session-1".into(), candidate.candidate.candidate_id.clone())), - 20943
axum::Extension(AuthenticatedPrincipal::Operator), - 20944
Json(CandidateCommentBody { - 20945
text: "Change this".into(), - 20946
path: Some("not-reviewed.txt".into()), - 20947
line_start: None, - 20948
line_end: None, - 20949
anchor: None, - 20950
request_id: Some("invalid-comment".into()), - 20951
}), - 20952
) - 20953
.await; - 20954
assert_eq!(invalid_comment.status(), StatusCode::BAD_REQUEST); - 20955
let invalid_range = comment_on_sandbox_candidate( - 20956
State(state.clone()), - 20957
Path(("session-1".into(), candidate.candidate.candidate_id.clone())), - 20958
axum::Extension(AuthenticatedPrincipal::Operator), - 20959
Json(CandidateCommentBody { - 20960
text: "Change this line".into(), - 20961
path: Some("result.txt".into()), - 20962
line_start: None, - 20963
line_end: Some(2), - 20964
anchor: None, - 20965
request_id: Some("invalid-range".into()), - 20966
}), - 20967
) - 20968
.await; - 20969
assert_eq!(invalid_range.status(), StatusCode::BAD_REQUEST); - 20970
let anchor_on_text = comment_on_sandbox_candidate( - 20971
State(state.clone()), - 20972
Path(("session-1".into(), candidate.candidate.candidate_id.clone())), - 20973
axum::Extension(AuthenticatedPrincipal::Operator), - 20974
Json(CandidateCommentBody { - 20975
text: "Change this cell".into(), - 20976
path: Some("result.txt".into()), - 20977
line_start: None, - 20978
line_end: None, - 20979
anchor: Some("Budget!B2".into()), - 20980
request_id: Some("anchor-on-text".into()), - 20981
}), - 20982
) - 20983
.await; - 20984
assert_eq!( - 20985
anchor_on_text.status(), - 20986
StatusCode::BAD_REQUEST, - 20987
"a text file is commented on by line" - 20988
); - 20989
let ledger_path = find_session_on_disk(&state.core, "session-1") - 20990
.unwrap() - 20991
.path() - 20992
.to_path_buf(); - 20993
let mut ledger = vak_session::SessionLog::open(ledger_path).unwrap(); - 20994
ledger - 20995
.append_activity(vak_session::ActivityRecord { - 20996
activity_id: "comment-history-1".into(), - 20997
turn: None, - 20998
kind: vak_session::ActivityKind::CandidateComment, - 20999
status: vak_session::ActivityStatus::Succeeded, - 21000
label: "Candidate comment".into(), - 21001
detail: None, - 21002
data: std::collections::BTreeMap::from([ - 21003
("actor_id".into(), "operator".into()), - 21004
( - 21005
"candidate_id".into(), - 21006
candidate.candidate.candidate_id.clone(), - 21007
), - 21008
("comment".into(), "Keep this reviewed wording".into()), - 21009
("path".into(), "result.txt".into()), - 21010
("line_start".into(), "1".into()), - 21011
]), - 21012
}) - 21013
.unwrap(); - 21014
let history = list_sandbox_candidate_comments( - 21015
State(state.clone()), - 21016
Path(("session-1".into(), candidate.candidate.candidate_id.clone())), - 21017
) - 21018
.await; - 21019
assert_eq!(history.status(), StatusCode::OK); - 21020
let history: serde_json::Value = serde_json::from_slice( - 21021
&axum::body::to_bytes(history.into_body(), 64 * 1024) - 21022
.await - 21023
.unwrap(), - 21024
) - 21025
.unwrap(); - 21026
assert_eq!(history["comments"][0]["text"], "Keep this reviewed wording"); - 21027
assert_eq!(history["comments"][0]["line_start"], 1); - 21028
let response = promote_sandbox_candidate( - 21029
State(state), - 21030
Path("session-1".into()), - 21031
Json(SandboxPromotionBody { - 21032
candidate_id: candidate.candidate.candidate_id, - 21033
files: vec!["result.txt".into()], - 21034
}), - 21035
) - 21036
.await; - 21037
assert_eq!(response.status(), StatusCode::OK); - 21038
assert_eq!( - 21039
tokio::fs::read_to_string(dir.path().join("result.txt")) - 21040
.await - 21041
.unwrap(), - 21042
"reviewed" - 21043
); - 21044
} - 21045
- 21046
#[tokio::test] - 21047
async fn saved_human_comment_revision_fails_before_run_without_provider() { - 21048
vak_config::paths::isolate_home_for_tests(); - 21049
let dir = tempfile::tempdir().unwrap(); - 21050
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 21051
core.set_sessions_home(dir.path().join("home")); - 21052
seed_bound_result(&core, "session-1", "exec-1"); - 21053
let state = AppState::new(core.clone()); - 21054
let scratch = dir.path().join(".vak/scratch/e1"); - 21055
tokio::fs::create_dir_all(&scratch).await.unwrap(); - 21056
tokio::fs::write(scratch.join("result.txt"), "saved candidate") - 21057
.await - 21058
.unwrap(); - 21059
let candidate = export_candidate(&state).await; - 21060
let session_path = core - 21061
.sessions_home() - 21062
.join("sessions") - 21063
.join(vak_core::memory::hash_cwd(core.cwd())) - 21064
.join("session-1.jsonl"); - 21065
let comment_id = "comment-from-asha"; - 21066
let mut log = SessionLog::open(session_path.clone()).unwrap(); - 21067
log.append_activity(vak_session::ActivityRecord { - 21068
activity_id: comment_id.into(), - 21069
turn: None, - 21070
kind: vak_session::ActivityKind::CandidateComment, - 21071
status: vak_session::ActivityStatus::Succeeded, - 21072
label: "Candidate comment".into(), - 21073
detail: None, - 21074
data: std::collections::BTreeMap::from([ - 21075
("actor_id".into(), "person-2".into()), - 21076
("actor_name".into(), "Asha".into()), - 21077
( - 21078
"candidate_id".into(), - 21079
candidate.candidate.candidate_id.clone(), - 21080
), - 21081
( - 21082
"candidate_digest".into(), - 21083
candidate.candidate_digest.clone(), - 21084
), - 21085
("comment".into(), "Please make the opening warmer".into()), - 21086
("path".into(), "result.txt".into()), - 21087
("line_start".into(), "1".into()), - 21088
]), - 21089
}) - 21090
.unwrap(); - 21091
drop(log); - 21092
let historical = SessionLog::open_read_only(session_path.clone()).unwrap(); - 21093
register_handle( - 21094
&state, - 21095
"session-1".into(), - 21096
historical, - 21097
core.cwd().to_path_buf(), - 21098
core.clone(), - 21099
); - 21100
core.set_route("no-such-provider".into(), "no-such-model".into()); - 21101
let response = request_revision_from_candidate_comment( - 21102
State(state.clone()), - 21103
Path(( - 21104
"session-1".into(), - 21105
candidate.candidate.candidate_id.clone(), - 21106
comment_id.into(), - 21107
)), - 21108
) - 21109
.await; - 21110
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE); - 21111
assert!( - 21112
state - 21113
.get("session-1") - 21114
.unwrap() - 21115
.session - 21116
.lock() - 21117
.unwrap() - 21118
.is_some() - 21119
); - 21120
assert!( - 21121
!SessionLog::open_read_only(session_path) - 21122
.unwrap() - 21123
.has_request_admission("revision-from-comment-from-asha") - 21124
); - 21125
} - 21126
- 21127
#[tokio::test(flavor = "multi_thread", worker_threads = 4)] - 21128
async fn human_feedback_produces_new_candidate_without_workspace_write() { - 21129
vak_config::paths::isolate_home_for_tests(); - 21130
let dir = tempfile::tempdir().unwrap(); - 21131
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 21132
core.set_sessions_home(dir.path().join("home")); - 21133
core.set_permission_mode(vak_config::PermissionMode::FullAccess); - 21134
core.set_route("revision-test".into(), "test-model".into()); - 21135
let worker = std::env::current_exe() - 21136
.unwrap() - 21137
.parent() - 21138
.unwrap() - 21139
.parent() - 21140
.unwrap() - 21141
.join("vak-tool-worker"); - 21142
if !worker.is_file() { - 21143
eprintln!("build vak-tool-worker to exercise this broker integration test"); - 21144
return; - 21145
} - 21146
core.set_tool_worker_exe(worker); - 21147
core.set_provider_instance(Arc::new(RevisionProvider { - 21148
replies: Mutex::new(VecDeque::from(vec![ - 21149
revision_message( - 21150
vec![ContentBlock::ToolUse { - 21151
id: "edit-version-two".into(), - 21152
name: "write".into(), - 21153
input: serde_json::json!({"path": "result.txt", "content": "version two"}), - 21154
}], - 21155
StopReason::ToolUse, - 21156
), - 21157
revision_message( - 21158
vec![ContentBlock::text("Prepared the revised draft for review.")], - 21159
StopReason::EndTurn, - 21160
), - 21161
])), - 21162
})); - 21163
seed_bound_result(&core, "session-1", "exec-1"); - 21164
let state = AppState::new(core.clone()); - 21165
let scratch = dir.path().join(".vak/scratch/e1"); - 21166
tokio::fs::create_dir_all(&scratch).await.unwrap(); - 21167
tokio::fs::write(scratch.join("result.txt"), "version one") - 21168
.await - 21169
.unwrap(); - 21170
let first = export_candidate(&state).await; - 21171
let parent_log = find_session_on_disk(&core, "session-1").unwrap(); - 21172
register_handle( - 21173
&state, - 21174
"session-1".into(), - 21175
parent_log, - 21176
core.cwd().to_path_buf(), - 21177
core.clone(), - 21178
); - 21179
- 21180
let response = dispatch_candidate_revision( - 21181
state.clone(), - 21182
first.clone(), - 21183
"comment-1".into(), - 21184
"Replace the text with version two".into(), - 21185
) - 21186
.await; - 21187
assert_eq!(response.status(), StatusCode::ACCEPTED); - 21188
let newer = tokio::time::timeout(Duration::from_secs(30), async { - 21189
loop { - 21190
let records = vak_sandbox::load_records(&sandbox_records_path(&state)).unwrap(); - 21191
if let Some(record) = records.iter().rev().find_map(|record| match record { - 21192
vak_sandbox::DurableRecord::Candidate(candidate) - 21193
if candidate.parent_candidate_id.as_deref() - 21194
== Some(first.candidate.candidate_id.as_str()) => - 21195
{ - 21196
Some(candidate.clone()) - 21197
} - 21198
_ => None, - 21199
}) { - 21200
break record; - 21201
} - 21202
if let Some(failed) = records.iter().rev().find_map(|record| match record { - 21203
vak_sandbox::DurableRecord::CandidateRevision(revision) - 21204
if revision.comment_id == "comment-1" - 21205
&& revision.status == vak_sandbox::CandidateRevisionStatus::Failed => - 21206
{ - 21207
Some(revision.clone()) - 21208
} - 21209
_ => None, - 21210
}) { - 21211
panic!("revision failed: {:?}", failed.detail); - 21212
} - 21213
tokio::time::sleep(Duration::from_millis(50)).await; - 21214
} - 21215
}) - 21216
.await - 21217
.unwrap(); - 21218
assert_eq!( - 21219
tokio::fs::read_to_string(newer.candidate.source_root.join("result.txt")) - 21220
.await - 21221
.unwrap(), - 21222
"version two" - 21223
); - 21224
assert_eq!( - 21225
newer.candidate.files[0].base_hash, - 21226
first.candidate.files[0].base_hash - 21227
); - 21228
assert!( - 21229
!dir.path().join("result.txt").exists(), - 21230
"revision must not touch the original workspace" - 21231
); - 21232
assert!(newer.revision_session_id.is_some()); - 21233
let accepted = promote_sandbox_candidate( - 21234
State(state.clone()), - 21235
Path("session-1".into()), - 21236
Json(SandboxPromotionBody { - 21237
candidate_id: newer.candidate.candidate_id, - 21238
files: vec!["result.txt".into()], - 21239
}), - 21240
) - 21241
.await; - 21242
assert_eq!(accepted.status(), StatusCode::OK); - 21243
assert_eq!( - 21244
tokio::fs::read_to_string(dir.path().join("result.txt")) - 21245
.await - 21246
.unwrap(), - 21247
"version two" - 21248
); - 21249
} - 21250
- 21251
/// A revision of an Office draft edits it with `office_apply`, which - 21252
/// writes a draft under the task copy's `.vak/scratch/` and never the - 21253
/// file itself. That draft is the next version of the same draft: the - 21254
/// revision used to fail with "revision did not change candidate files". - 21255
#[tokio::test(flavor = "multi_thread", worker_threads = 4)] - 21256
async fn office_revision_is_the_next_version_of_the_draft() { - 21257
use sha2::Digest as _; - 21258
vak_config::paths::isolate_home_for_tests(); - 21259
let dir = tempfile::tempdir().unwrap(); - 21260
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 21261
core.set_sessions_home(dir.path().join("home")); - 21262
core.set_route("revision-test".into(), "test-model".into()); - 21263
pin_test_tool_worker(&core); - 21264
let version_one = vak_ooxml::fixtures::docx(); - 21265
let digest: String = sha2::Sha256::digest(&version_one) - 21266
.iter() - 21267
.map(|byte| format!("{byte:02x}")) - 21268
.collect(); - 21269
core.set_provider_instance(Arc::new(RevisionProvider { - 21270
replies: Mutex::new(VecDeque::from(vec![ - 21271
revision_message( - 21272
vec![ContentBlock::ToolUse { - 21273
id: "office-version-two".into(), - 21274
name: "office_apply".into(), - 21275
input: serde_json::json!({ - 21276
"path": "letter.docx", - 21277
"base_digest": digest, - 21278
"ops": [{"op": "replace_paragraph_text", "anchor": "p@1", "text": "Annual Report"}], - 21279
}), - 21280
}], - 21281
StopReason::ToolUse, - 21282
), - 21283
revision_message( - 21284
vec![ContentBlock::text("Retitled the letter.")], - 21285
StopReason::EndTurn, - 21286
), - 21287
])), - 21288
})); - 21289
seed_bound_result(&core, "session-1", "exec-1"); - 21290
let state = AppState::new(core.clone()); - 21291
tokio::fs::write(dir.path().join("letter.docx"), &version_one) - 21292
.await - 21293
.unwrap(); - 21294
let scratch = dir.path().join(".vak/scratch/e1"); - 21295
tokio::fs::create_dir_all(&scratch).await.unwrap(); - 21296
tokio::fs::write(scratch.join("letter.docx"), &version_one) - 21297
.await - 21298
.unwrap(); - 21299
let first = export_candidate(&state).await; - 21300
let parent_log = find_session_on_disk(&core, "session-1").unwrap(); - 21301
register_handle( - 21302
&state, - 21303
"session-1".into(), - 21304
parent_log, - 21305
core.cwd().to_path_buf(), - 21306
core.clone(), - 21307
); - 21308
- 21309
let response = dispatch_candidate_revision( - 21310
state.clone(), - 21311
first.clone(), - 21312
"comment-1".into(), - 21313
"Retitle the letter".into(), - 21314
) - 21315
.await; - 21316
assert_eq!(response.status(), StatusCode::ACCEPTED); - 21317
let newer = tokio::time::timeout(Duration::from_secs(30), async { - 21318
loop { - 21319
let records = vak_sandbox::load_records(&sandbox_records_path(&state)).unwrap(); - 21320
if let Some(failed) = records.iter().rev().find_map(|record| match record { - 21321
vak_sandbox::DurableRecord::CandidateRevision(revision) - 21322
if revision.status == vak_sandbox::CandidateRevisionStatus::Failed => - 21323
{ - 21324
Some(revision.clone()) - 21325
} - 21326
_ => None, - 21327
}) { - 21328
panic!("revision failed: {:?}", failed.detail); - 21329
} - 21330
if let Some(record) = records.iter().rev().find_map(|record| match record { - 21331
vak_sandbox::DurableRecord::Candidate(candidate) - 21332
if candidate.parent_candidate_id.as_deref() - 21333
== Some(first.candidate.candidate_id.as_str()) => - 21334
{ - 21335
Some(candidate.clone()) - 21336
} - 21337
_ => None, - 21338
}) { - 21339
break record; - 21340
} - 21341
tokio::time::sleep(Duration::from_millis(50)).await; - 21342
} - 21343
}) - 21344
.await - 21345
.unwrap(); - 21346
- 21347
let files: Vec<&str> = newer - 21348
.candidate - 21349
.files - 21350
.iter() - 21351
.map(|file| file.path.as_str()) - 21352
.collect(); - 21353
assert_eq!(files, ["letter.docx"]); - 21354
assert_eq!( - 21355
newer.candidate.files[0].base_hash, - 21356
first.candidate.files[0].base_hash - 21357
); - 21358
let version_two = std::fs::read(newer.candidate.source_root.join("letter.docx")).unwrap(); - 21359
let mut package = vak_ooxml::Package::open( - 21360
std::io::Cursor::new(version_two), - 21361
vak_ooxml::Limits::default(), - 21362
) - 21363
.unwrap(); - 21364
let main = package.main_part().to_string(); - 21365
let body = String::from_utf8(package.read_part(&main).unwrap()).unwrap(); - 21366
assert!(body.contains("Annual Report"), "{body}"); - 21367
// One execution's versions are one draft: this is its version 2. - 21368
assert_eq!(newer.execution_id, first.execution_id); - 21369
assert_eq!( - 21370
std::fs::read(dir.path().join("letter.docx")).unwrap(), - 21371
version_one, - 21372
"revision must not touch the original workspace" - 21373
); - 21374
} - 21375
- 21376
#[tokio::test] - 21377
async fn owner_receives_live_comment_refresh_for_open_review() { - 21378
use futures::StreamExt; - 21379
- 21380
vak_config::paths::isolate_home_for_tests(); - 21381
let dir = tempfile::tempdir().unwrap(); - 21382
let core = Core::new_with_trust(dir.path().to_path_buf(), true).unwrap(); - 21383
core.set_sessions_home(dir.path().join("home")); - 21384
seed_bound_result(&core, "session-1", "exec-1"); - 21385
let state = AppState::new(core.clone()); - 21386
let path = core - 21387
.sessions_home() - 21388
.join("sessions") - 21389
.join(vak_core::memory::hash_cwd(core.cwd())) - 21390
.join("session-1.jsonl"); - 21391
let session = SessionLog::open(path).unwrap(); - 21392
register_handle( - 21393
&state, - 21394
"session-1".into(), - 21395
session, - 21396
core.cwd().to_path_buf(), - 21397
core, - 21398
); - 21399
let response = coworking_updates( - 21400
State(state.clone()), - 21401
Path("session-1".into()), - 21402
axum::Extension(AuthenticatedPrincipal::Operator), - 21403
axum::http::HeaderMap::new(), - 21404
) - 21405
.await; - 21406
assert_eq!(response.status(), StatusCode::OK); - 21407
let mut events = response.into_body().into_data_stream(); - 21408
let heartbeat = tokio::time::timeout(std::time::Duration::from_secs(3), events.next()) - 21409
.await - 21410
.unwrap() - 21411
.unwrap() - 21412
.unwrap(); - 21413
assert!( - 21414
std::str::from_utf8(&heartbeat) - 21415
.unwrap() - 21416
.contains("event: heartbeat") - 21417
); - 21418
state - 21419
.get("session-1") - 21420
.unwrap() - 21421
.coworking_comments_tx - 21422
.send(()) - 21423
.unwrap(); - 21424
let refresh = tokio::time::timeout(std::time::Duration::from_secs(3), events.next()) - 21425
.await - 21426
.unwrap() - 21427
.unwrap() - 21428
.unwrap(); - 21429
assert!( - 21430
std::str::from_utf8(&refresh) - 21431
.unwrap() - 21432
.contains("event: refresh") - 21433
); - 21434
} - 21435
- 21436
#[test] - 21437
fn sandbox_execution_ledger_survives_live_bus_loss() { - 21438
let dir = tempfile::tempdir().unwrap(); - 21439
let start = AgentEvent::Sandbox(vak_tools::SandboxEvent::ExecutionStarted { - 21440
execution_id: "child-exec".into(), - 21441
owner_session_id: Some("child-session".into()), - 21442
tool: "bash".into(), - 21443
code_preview: "echo hi".into(), - 21444
language: "bash".into(), - 21445
scratch_dir: ".vak/scratch/child-exec".into(), - 21446
}); - 21447
let finish = AgentEvent::Sandbox(vak_tools::SandboxEvent::ExecutionFinished { - 21448
execution_id: "child-exec".into(), - 21449
exit_code: 0, - 21450
duration_ms: 42, - 21451
artifacts: vec!["result.txt".into()], - 21452
}); - 21453
append_session_sandbox_event(dir.path(), "parent-session", &start); - 21454
append_session_sandbox_event(dir.path(), "parent-session", &finish); - 21455
let path = dir.path().join("sandbox/executions/parent-session.jsonl"); - 21456
let lines = std::fs::read_to_string(path).unwrap(); - 21457
assert_eq!(lines.lines().count(), 2); - 21458
assert!(lines.contains("child-session")); - 21459
assert!(lines.contains("ExecutionFinished")); - 21460
} - 21461
- 21462
#[test] - 21463
fn detect_launch_identifies_vite_package_json() { - 21464
let dir = tempfile::tempdir().unwrap(); - 21465
let pkg = serde_json::json!({ - 21466
"scripts": { "dev": "vite" }, - 21467
"devDependencies": { "vite": "^5.0.0" } - 21468
}); - 21469
std::fs::write(dir.path().join("package.json"), pkg.to_string()).unwrap(); - 21470
let found = detect_launch(dir.path()); - 21471
assert_eq!(found.len(), 1); - 21472
assert_eq!(found[0].name, "dev"); - 21473
assert_eq!(found[0].cmd, "npm"); - 21474
assert_eq!(found[0].args, vec!["run", "dev"]); - 21475
assert_eq!(found[0].port, Some(5173)); - 21476
} - 21477
- 21478
#[test] - 21479
fn detect_launch_uses_the_declared_package_manager() { - 21480
let dir = tempfile::tempdir().unwrap(); - 21481
let pkg = serde_json::json!({ - 21482
"packageManager": "yarn@4.9.2", - 21483
"scripts": { "dev": "vite" }, - 21484
"devDependencies": { "vite": "^7.0.0" } - 21485
}); - 21486
std::fs::write(dir.path().join("package.json"), pkg.to_string()).unwrap(); - 21487
- 21488
let found = detect_launch(dir.path()); - 21489
assert_eq!(found.len(), 1); - 21490
assert_eq!(found[0].cmd, "yarn"); - 21491
assert_eq!(found[0].args, vec!["run", "dev"]); - 21492
assert!(javascript_dependencies_missing(dir.path())); - 21493
std::fs::create_dir(dir.path().join("node_modules")).unwrap(); - 21494
assert!(!javascript_dependencies_missing(dir.path())); - 21495
} - 21496
- 21497
#[test] - 21498
fn dependency_preparation_uses_fixed_lockfile_aware_arguments() { - 21499
let dir = tempfile::tempdir().unwrap(); - 21500
std::fs::write( - 21501
dir.path().join("package.json"), - 21502
r#"{"scripts":{"dev":"vite"},"dependencies":{"vite":"^7"}}"#, - 21503
) - 21504
.unwrap(); - 21505
std::fs::write(dir.path().join("package-lock.json"), "{}").unwrap(); - 21506
let (command, args) = dependency_install_command(dir.path()).unwrap(); - 21507
assert_eq!(command, "npm"); - 21508
assert_eq!( - 21509
args, - 21510
vec!["ci", "--ignore-scripts", "--no-audit", "--no-fund"] - 21511
); - 21512
} - 21513
- 21514
#[test] - 21515
fn workspace_checks_are_declared_from_project_manifests() { - 21516
let source = tempfile::tempdir().unwrap(); - 21517
let target = tempfile::tempdir().unwrap(); - 21518
std::fs::write( - 21519
source.path().join("package.json"), - 21520
r#"{"scripts":{"build":"vite build","test":"vitest run"}}"#, - 21521
) - 21522
.unwrap(); - 21523
let candidate = - 21524
vak_sandbox::candidate_manifest("checks", source.path(), target.path()).unwrap(); - 21525
let checks = planned_workspace_checks(&candidate); - 21526
assert_eq!(checks.len(), 2); - 21527
assert_eq!(checks[0].id, "javascript.build"); - 21528
assert_eq!(checks[0].command, "npm run build"); - 21529
assert_eq!(checks[1].id, "javascript.test"); - 21530
assert_eq!(checks[1].command, "npm run test"); - 21531
} - 21532
- 21533
#[test] - 21534
fn workspace_checks_follow_the_effective_candidate_manifest() { - 21535
let source = tempfile::tempdir().unwrap(); - 21536
let target = tempfile::tempdir().unwrap(); - 21537
std::fs::write( - 21538
target.path().join("package.json"), - 21539
r#"{"scripts":{"test":"old test"}}"#, - 21540
) - 21541
.unwrap(); - 21542
std::fs::write( - 21543
source.path().join("package.json"), - 21544
r#"{"scripts":{"build":"vite build"}}"#, - 21545
) - 21546
.unwrap(); - 21547
let mut candidate = - 21548
vak_sandbox::candidate_manifest("checks", source.path(), target.path()).unwrap(); - 21549
let checks = planned_workspace_checks(&candidate); - 21550
assert_eq!(checks.len(), 1); - 21551
assert_eq!(checks[0].id, "javascript.build"); - 21552
- 21553
candidate.files[0].operation = vak_sandbox::CandidateOperation::Delete; - 21554
assert!(planned_workspace_checks(&candidate).is_empty()); - 21555
- 21556
candidate.files.clear(); - 21557
let inherited = planned_workspace_checks(&candidate); - 21558
assert_eq!(inherited.len(), 1); - 21559
assert_eq!(inherited[0].id, "javascript.test"); - 21560
} - 21561
- 21562
#[test] - 21563
fn workspace_checks_use_the_effective_package_manager_without_command_injection() { - 21564
let source = tempfile::tempdir().unwrap(); - 21565
let target = tempfile::tempdir().unwrap(); - 21566
std::fs::write( - 21567
source.path().join("package.json"), - 21568
r#"{"packageManager":"pnpm@10.0.0; touch /tmp/no","scripts":{"build":"vite build","test":"vitest run"}}"#, - 21569
) - 21570
.unwrap(); - 21571
std::fs::write( - 21572
source.path().join("pnpm-lock.yaml"), - 21573
"lockfileVersion: '9.0'\n", - 21574
) - 21575
.unwrap(); - 21576
let candidate = - 21577
vak_sandbox::candidate_manifest("pnpm-checks", source.path(), target.path()).unwrap(); - 21578
- 21579
let checks = planned_workspace_checks(&candidate); - 21580
assert_eq!(checks.len(), 2); - 21581
assert_eq!(checks[0].command, "pnpm run build"); - 21582
assert_eq!(checks[1].command, "pnpm run test"); - 21583
assert!(checks.iter().all(|check| !check.command.contains("touch"))); - 21584
} - 21585
- 21586
#[test] - 21587
fn detect_launch_identifies_python_fastapi_and_flask() { - 21588
let dir = tempfile::tempdir().unwrap(); - 21589
std::fs::write( - 21590
dir.path().join("main.py"), - 21591
"from fastapi import FastAPI\napp = FastAPI()\n", - 21592
) - 21593
.unwrap(); - 21594
let found = detect_launch(dir.path()); - 21595
assert_eq!(found.len(), 1); - 21596
assert_eq!(found[0].name, "fastapi"); - 21597
assert_eq!(found[0].cmd, "python3"); - 21598
assert_eq!( - 21599
found[0].args, - 21600
vec!["-m", "uvicorn", "main:app", "--reload", "--port", "8000"] - 21601
); - 21602
assert_eq!(found[0].port, Some(8000)); - 21603
} - 21604
- 21605
#[test] - 21606
fn detect_launch_identifies_cargo_and_go() { - 21607
let dir = tempfile::tempdir().unwrap(); - 21608
std::fs::create_dir_all(dir.path().join("src")).unwrap(); - 21609
std::fs::write( - 21610
dir.path().join("Cargo.toml"), - 21611
"[package]\nname = \"demo\"\n", - 21612
) - 21613
.unwrap(); - 21614
std::fs::write(dir.path().join("src/main.rs"), "fn main() {}\n").unwrap(); - 21615
std::fs::write(dir.path().join("go.mod"), "module demo\n").unwrap(); - 21616
- 21617
let found = detect_launch(dir.path()); - 21618
let names: Vec<&str> = found.iter().map(|s| s.name.as_str()).collect(); - 21619
assert!(names.contains(&"cargo")); - 21620
assert!(names.contains(&"go")); - 21621
} - 21622
- 21623
#[test] - 21624
fn detect_launch_falls_back_to_static_html() { - 21625
let dir = tempfile::tempdir().unwrap(); - 21626
std::fs::write(dir.path().join("index.html"), "<h1>Test</h1>\n").unwrap(); - 21627
let found = detect_launch(dir.path()); - 21628
assert_eq!(found.len(), 1); - 21629
assert_eq!(found[0].name, "static"); - 21630
assert_eq!(found[0].cmd, "python3"); - 21631
assert_eq!(found[0].args, vec!["-m", "http.server", "8080"]); - 21632
assert_eq!(found[0].port, Some(8080)); - 21633
} - 21634
- 21635
#[test] - 21636
fn preview_port_probe_reports_owned_listener_as_unavailable() { - 21637
let listener = std::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)).unwrap(); - 21638
let port = listener.local_addr().unwrap().port(); - 21639
assert!(!port_is_available(port)); - 21640
drop(listener); - 21641
assert!(port_is_available(port)); - 21642
} - 21643
- 21644
fn participant(capabilities: &[&str]) -> coworking::VerifiedPrincipal { - 21645
coworking::VerifiedPrincipal { - 21646
grant_id: "grant-1".into(), - 21647
principal_id: "person-2".into(), - 21648
display_name: "Asha".into(), - 21649
conversation_id: "session-1".into(), - 21650
audience_id: "conversation:session-1".into(), - 21651
capabilities: capabilities.iter().map(|value| (*value).into()).collect(), - 21652
} - 21653
} - 21654
- 21655
#[test] - 21656
fn participant_read_scope_is_exact_and_fail_closed() { - 21657
let principal = participant(&["read"]); - 21658
assert!(participant_matches_conversation_audience( - 21659
&principal, - 21660
"session-1", - 21661
Some("conversation:session-1") - 21662
)); - 21663
assert!(!participant_matches_conversation_audience( - 21664
&principal, - 21665
"session-1", - 21666
Some("conversation:other") - 21667
)); - 21668
assert!(!participant_matches_conversation_audience( - 21669
&principal, - 21670
"session-1", - 21671
None - 21672
)); - 21673
assert!(!participant_matches_conversation_audience( - 21674
&principal, - 21675
"session-2", - 21676
Some("conversation:session-1") - 21677
)); - 21678
for path in [ - 21679
"/sessions/session-1/transcript", - 21680
"/sessions/session-1/transcript.md", - 21681
"/sessions/session-1/presentation", - 21682
"/sessions/session-1/results/result-1", - 21683
"/sessions/session-1/sandbox/records", - 21684
"/sessions/session-1/sandbox/candidates/candidate-1/files", - 21685
"/sessions/session-1/sandbox/candidates/candidate-1/files/raw", - 21686
"/sessions/session-1/sandbox/candidates/candidate-1/office-review", - 21687
"/sessions/session-1/sandbox/candidates/candidate-1/office", - 21688
"/sessions/session-1/sandbox/candidates/candidate-1/comments", - 21689
"/sessions/session-1/coworking/updates", - 21690
"/sessions/session-1/coworking/presence", - 21691
"/sessions/session-1/office-workspaces", - 21692
] { - 21693
assert!(participant_read_route_allowed( - 21694
&axum::http::Method::GET, - 21695
path, - 21696
&principal - 21697
)); - 21698
} - 21699
for path in [ - 21700
"/sessions/session-2/transcript", - 21701
"/sessions/session-1/events", - 21702
"/sessions/session-1/run", - 21703
"/sessions/session-1/sandbox/executions", - 21704
"/sessions/session-1/sandbox/promote", - 21705
"/fs/file", - 21706
"/fs/office", - 21707
"/config", - 21708
] { - 21709
assert!(!participant_read_route_allowed( - 21710
&axum::http::Method::GET, - 21711
path, - 21712
&principal - 21713
)); - 21714
} - 21715
assert!(!participant_read_route_allowed( - 21716
&axum::http::Method::POST, - 21717
"/sessions/session-1/sandbox/candidates/candidate-1/comments", - 21718
&principal - 21719
)); - 21720
assert!(participant_read_route_allowed( - 21721
&axum::http::Method::POST, - 21722
"/sessions/session-1/sandbox/candidates/candidate-1/comments", - 21723
&participant(&["read", "comment"]) - 21724
)); - 21725
assert!(participant_read_route_allowed( - 21726
&axum::http::Method::POST, - 21727
"/sessions/session-1/coworking/messages", - 21728
&participant(&["read", "message"]) - 21729
)); - 21730
assert!(participant_read_route_allowed( - 21731
&axum::http::Method::POST,
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.