fix: adapt T-769 recovery onto current restore lane checkpoint
UNCOMPILED TRANSPORT / PRESERVATION CHECKPOINT, NOT A VALIDATED CANDIDATE. One-time recovery authorization permits publication before validation so the parent can validate this exact source in a separate Workdir. Prior compilation attempts were interrupted by Workdir-session unavailability; cause and test outcome remain unknown. No Cargo build/check/test was started in this bounded checkpoint turn. Merge current lane4ef7b03651into existing sourceb7a22146a0. Selectively adapt typed factory failure and same-operation cleanup/replay while preserving current saved Feature restore and operation-time authorization, logical/deferred Workdirs, T765 Host/schema and T768 replay-safe Subject grant. No T770/T771 implementation. All 19 target-relative paths and added test bodies were source-audited. Workspace fmt check, scoped rustfmt, whitespace/diff checks passed; conflict markers zero. Excluded unrelated formatter changes and untracked compiler artifacts. Local audit: target/t769-checkpoint/AUDIT.md. Handoff to parent: compile and run focused and package/API regressions on this exact source, fix as necessary, then obtain fresh independent review. Existing MR 01a15606-a6ba-7d23-8d60-0874607399e7 only; no new review requested here. No acceptance, approval, adoption, target/develop push, integration, Ticket closure or service update is asserted.
This commit is contained in:
@@ -119,6 +119,7 @@ fn annotated(item: Item) -> session_store::LoggedHistoryEntry {
|
||||
session_store::LoggedHistoryEntry {
|
||||
item: session_store::LoggedItem::from(item),
|
||||
metadata: session_store::LoggedSessionHistoryMetadata {
|
||||
input_metadata: None,
|
||||
entry_id: session_store::LoggedSessionHistoryEntryId::new(),
|
||||
origin: session_store::LoggedSessionHistoryOrigin::LegacyUnknown,
|
||||
derivation: None,
|
||||
@@ -1495,11 +1496,9 @@ async fn post_compact_pause_bills_partial_without_rearming_or_consuming_correlat
|
||||
let session_id = worker.session_id();
|
||||
|
||||
let runtime_tmp = tempfile::tempdir().unwrap();
|
||||
let bash_output_dir = runtime_tmp.path().join("bash-output");
|
||||
let (handle, shutdown_receiver) =
|
||||
WorkerController::spawn(worker, runtime_tmp.path(), &bash_output_dir)
|
||||
.await
|
||||
.unwrap();
|
||||
let (handle, shutdown_receiver) = WorkerController::spawn(worker, runtime_tmp.path())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut rx = handle.subscribe();
|
||||
handle
|
||||
.send(Method::submit_text(
|
||||
@@ -1705,11 +1704,9 @@ async fn controller_does_not_repeat_a_persisted_mid_run_compaction_failure() {
|
||||
let (worker, store) = make_worker_with_manifest_and_store(MID_TURN_MANIFEST_TOML, client).await;
|
||||
let session_id = worker.session_id();
|
||||
let runtime_tmp = tempfile::tempdir().unwrap();
|
||||
let bash_output_dir = runtime_tmp.path().join("bash-output");
|
||||
let (handle, shutdown_receiver) =
|
||||
WorkerController::spawn(worker, runtime_tmp.path(), &bash_output_dir)
|
||||
.await
|
||||
.unwrap();
|
||||
let (handle, shutdown_receiver) = WorkerController::spawn(worker, runtime_tmp.path())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut rx = handle.subscribe();
|
||||
|
||||
handle
|
||||
@@ -1830,11 +1827,9 @@ async fn manual_compact_cancel_clears_progress_before_returning_idle() {
|
||||
let source_session = worker.session_id();
|
||||
let source_segment = worker.segment_id();
|
||||
let runtime_tmp = tempfile::tempdir().unwrap();
|
||||
let bash_output_dir = runtime_tmp.path().join("bash-output");
|
||||
let (handle, shutdown_receiver) =
|
||||
WorkerController::spawn(worker, runtime_tmp.path(), &bash_output_dir)
|
||||
.await
|
||||
.unwrap();
|
||||
let (handle, shutdown_receiver) = WorkerController::spawn(worker, runtime_tmp.path())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut rx = handle.subscribe();
|
||||
|
||||
handle
|
||||
@@ -1996,8 +1991,7 @@ async fn controller_compact_method_publishes_progress_and_clear() {
|
||||
]);
|
||||
let worker = make_worker_with_manifest(MANUAL_ONLY_MANIFEST_TOML, client).await;
|
||||
let runtime_tmp = tempfile::tempdir().unwrap();
|
||||
let bash_output_dir = runtime_tmp.path().join("bash-output");
|
||||
let (handle, _shutdown) = WorkerController::spawn(worker, runtime_tmp.path(), &bash_output_dir)
|
||||
let (handle, _shutdown) = WorkerController::spawn(worker, runtime_tmp.path())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut rx = handle.subscribe();
|
||||
@@ -2219,10 +2213,48 @@ fn summary_entry(
|
||||
origin: session_store::LoggedSessionHistoryOrigin,
|
||||
) -> session_store::LoggedHistoryEntry {
|
||||
let mut entry = annotated(item);
|
||||
entry.metadata.input_metadata = matches!(
|
||||
&entry.item,
|
||||
session_store::LoggedItem::Message {
|
||||
role: session_store::LoggedRole::System,
|
||||
..
|
||||
}
|
||||
)
|
||||
.then(|| {
|
||||
session_store::HistoryInputMetadata::new(
|
||||
session_store::HistoryInputUsePolicy {
|
||||
recording_material: session_store::InputEvidenceUse::Exclude,
|
||||
memory_evidence: session_store::InputEvidenceUse::Exclude,
|
||||
observation: session_store::InputObservationUse::Hidden,
|
||||
compaction: match origin {
|
||||
session_store::LoggedSessionHistoryOrigin::CompactionSummary => {
|
||||
session_store::InputCompactionUse::OpeningSummary
|
||||
}
|
||||
session_store::LoggedSessionHistoryOrigin::JournalContext => {
|
||||
session_store::InputCompactionUse::ReferenceInput
|
||||
}
|
||||
_ => session_store::InputCompactionUse::Retain,
|
||||
},
|
||||
},
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
entry.metadata.origin = origin;
|
||||
entry
|
||||
}
|
||||
|
||||
fn compaction_use(
|
||||
metadata: &session_store::LoggedSessionHistoryMetadata,
|
||||
) -> session_store::InputCompactionUse {
|
||||
metadata
|
||||
.input_metadata
|
||||
.as_ref()
|
||||
.map(|input| input.usage().compaction)
|
||||
.unwrap_or(session_store::InputCompactionUse::Retain)
|
||||
}
|
||||
|
||||
fn long_summary(label: &str) -> String {
|
||||
format!(
|
||||
"## Completed Tasks\n{label}: investigated the transport boundary.\n\n\
|
||||
@@ -2358,7 +2390,8 @@ async fn full_initial_summary_survives_small_overview_budgets_and_both_retention
|
||||
assert_eq!(
|
||||
replacement
|
||||
.iter()
|
||||
.filter(|entry| entry.metadata.origin == CompactionSummary)
|
||||
.filter(|entry| compaction_use(&entry.metadata)
|
||||
== session_store::InputCompactionUse::OpeningSummary)
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
@@ -2603,7 +2636,9 @@ async fn three_chained_compactions_transport_each_complete_predecessor_without_r
|
||||
let seed = replacement_seed(&fixture.worker);
|
||||
let inherited = seed
|
||||
.iter()
|
||||
.filter(|entry| entry.metadata.origin == CompactionSummary)
|
||||
.filter(|entry| {
|
||||
compaction_use(&entry.metadata) == session_store::InputCompactionUse::OpeningSummary
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(inherited.len(), 1);
|
||||
assert_eq!(
|
||||
@@ -2891,7 +2926,7 @@ impl worker::subjektiv::SubjektivHost for CompactionJournalHost {
|
||||
}
|
||||
|
||||
fn journal_startup_granted(&self) -> bool {
|
||||
true
|
||||
false
|
||||
}
|
||||
|
||||
fn journal_startup_context(
|
||||
@@ -2995,8 +3030,8 @@ fn compaction_snapshot(generation: usize) -> subjektiv::journal::startup::Journa
|
||||
fn journal_body(worker: &Worker<MockClient, TestStore>) -> String {
|
||||
let seed = replacement_seed(worker);
|
||||
assert_eq!(
|
||||
seed[1].metadata.origin,
|
||||
session_store::LoggedSessionHistoryOrigin::JournalContext
|
||||
compaction_use(&seed[1].metadata),
|
||||
session_store::InputCompactionUse::ReferenceInput
|
||||
);
|
||||
let item = Item::from(seed[1].item.clone());
|
||||
assert!(matches!(
|
||||
@@ -3008,8 +3043,8 @@ fn journal_body(worker: &Worker<MockClient, TestStore>) -> String {
|
||||
));
|
||||
assert_eq!(
|
||||
seed.iter()
|
||||
.filter(|entry| entry.metadata.origin
|
||||
== session_store::LoggedSessionHistoryOrigin::JournalContext)
|
||||
.filter(|entry| compaction_use(&entry.metadata)
|
||||
== session_store::InputCompactionUse::ReferenceInput)
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
@@ -3118,20 +3153,34 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai
|
||||
projection.source_digest = "a".repeat(64);
|
||||
let initial_catalog = Arc::new(worker::PromptCatalog::from_projection(projection).unwrap());
|
||||
fixture.worker.prompts().store(initial_catalog.clone());
|
||||
let legacy_startup = session_store::LoggedSystemHistoryEntry {
|
||||
item: session_store::SystemItem::JournalStartup {
|
||||
body: "OLD_TYPED_STARTUP_JOURNAL".into(),
|
||||
snapshot: serde_json::json!({"journals": []}),
|
||||
snapshot_sha256: "legacy-startup-digest".into(),
|
||||
prompt_provenance: session_store::PromptRenderProvenance {
|
||||
workspace_id: None,
|
||||
source_digest: "legacy-source".into(),
|
||||
projection_digest: "legacy-projection".into(),
|
||||
logical_name: "internal.subjektiv_journal_startup".into(),
|
||||
},
|
||||
},
|
||||
metadata: summary_entry(Item::system_message(""), DerivedSummary).metadata,
|
||||
};
|
||||
// Already migrated generic input with unrelated provenance/payload; Core
|
||||
// must select it by explicit policy, not identify a Journal Feature.
|
||||
let legacy_startup = session_store::LoggedSystemHistoryEntry::from_input(
|
||||
DerivedSummary,
|
||||
None,
|
||||
session_store::SystemInputEnvelope::new(
|
||||
"OLD_TYPED_STARTUP_JOURNAL",
|
||||
session_store::HistoryInputMetadata::new(
|
||||
session_store::HistoryInputUsePolicy {
|
||||
recording_material: session_store::InputEvidenceUse::Exclude,
|
||||
memory_evidence: session_store::InputEvidenceUse::Exclude,
|
||||
observation: session_store::InputObservationUse::Hidden,
|
||||
compaction: session_store::InputCompactionUse::ReferenceInput,
|
||||
},
|
||||
Some(
|
||||
session_store::FeatureInputPayload::encode(
|
||||
"renamed-owner:reference",
|
||||
8,
|
||||
&serde_json::json!({"private": "retained opaque context"}),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
);
|
||||
let legacy_id = legacy_startup.metadata.entry_id.clone();
|
||||
fixture
|
||||
.restore_seed_with_system(
|
||||
@@ -3165,7 +3214,7 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai
|
||||
.unwrap()
|
||||
.annotation
|
||||
.origin,
|
||||
JournalContext
|
||||
DerivedSummary
|
||||
);
|
||||
let session = fixture.worker.session_id();
|
||||
let original_segment = fixture.worker.segment_id();
|
||||
@@ -3190,8 +3239,8 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai
|
||||
.unwrap()
|
||||
.annotation
|
||||
.origin,
|
||||
JournalContext,
|
||||
"legacy startup classification survives a normal turn"
|
||||
DerivedSummary,
|
||||
"generic source provenance survives a normal turn unchanged"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
@@ -3242,7 +3291,7 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai
|
||||
assert_eq!(host.reads.lock().unwrap().len(), index + 1);
|
||||
let history = fixture.worker.history();
|
||||
let overview = input
|
||||
.split("## Previous Journal reference (historical, not automatically retained)")
|
||||
.split("## Previous reference input (historical, not automatically retained)")
|
||||
.next()
|
||||
.unwrap();
|
||||
for old in &old_bodies {
|
||||
@@ -3335,6 +3384,17 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai
|
||||
.run_text("normal post-compaction turn")
|
||||
.await
|
||||
.unwrap();
|
||||
let frozen_seed = replacement_seed(&fixture.worker)
|
||||
.into_iter()
|
||||
.filter(|entry| {
|
||||
matches!(
|
||||
compaction_use(&entry.metadata),
|
||||
session_store::InputCompactionUse::ReferenceInput
|
||||
| session_store::InputCompactionUse::OpeningSummary
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(frozen_seed.len(), 2);
|
||||
let records = fixture
|
||||
.worker
|
||||
.store()
|
||||
@@ -3374,6 +3434,20 @@ async fn journal_three_compactions_freeze_one_snapshot_preserve_full_summary_tai
|
||||
.run_text("normal turn after typed history restore")
|
||||
.await
|
||||
.unwrap();
|
||||
for frozen in frozen_seed {
|
||||
let replayed = fixture
|
||||
.worker
|
||||
.session_history()
|
||||
.entries()
|
||||
.iter()
|
||||
.find(|entry| entry.annotation.entry_id == frozen.metadata.entry_id)
|
||||
.expect("frozen entry identity survives re-restore");
|
||||
assert_eq!(replayed.item, Item::from(frozen.item));
|
||||
assert_eq!(
|
||||
replayed.annotation, frozen.metadata,
|
||||
"origin, derivation, public allowlist, policy and private payload survive atomic seed/restore"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
host.reads.lock().unwrap().len(),
|
||||
3,
|
||||
@@ -3444,27 +3518,41 @@ async fn journal_absent_disabled_unavailable_and_over_byte_budget_keep_conversat
|
||||
)
|
||||
.await;
|
||||
fixture.worker.compact(0).await.unwrap();
|
||||
let body = journal_body(&fixture.worker);
|
||||
let expected = match case {
|
||||
"no-host" | "disabled" => "disabled",
|
||||
"absent" => "absent",
|
||||
_ => "unavailable",
|
||||
};
|
||||
assert!(
|
||||
body.contains(&format!("Journal read status: {expected}.")),
|
||||
"{case}"
|
||||
);
|
||||
let input = first_compactor_input(&observed.requests()[1]);
|
||||
assert_frozen_journal_transport(&input, &body);
|
||||
assert_journal_provenance(
|
||||
&body,
|
||||
expected_json.as_deref(),
|
||||
&fixture.worker.prompts().load_full(),
|
||||
(case != "no-host").then_some(COMPACTION_HOST_WORKSPACE_ID),
|
||||
);
|
||||
let body = if matches!(case, "disabled" | "no-host") {
|
||||
assert_eq!(
|
||||
replacement_seed(&fixture.worker)
|
||||
.iter()
|
||||
.filter(|entry| compaction_use(&entry.metadata)
|
||||
== session_store::InputCompactionUse::ReferenceInput)
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
assert!(!input.contains("Journal reference frozen at this compaction boundary"));
|
||||
String::new()
|
||||
} else {
|
||||
let body = journal_body(&fixture.worker);
|
||||
let expected = if case == "absent" {
|
||||
"absent"
|
||||
} else {
|
||||
"unavailable"
|
||||
};
|
||||
assert!(
|
||||
body.contains(&format!("Journal read status: {expected}.")),
|
||||
"{case}"
|
||||
);
|
||||
assert_frozen_journal_transport(&input, &body);
|
||||
assert_journal_provenance(
|
||||
&body,
|
||||
expected_json.as_deref(),
|
||||
&fixture.worker.prompts().load_full(),
|
||||
Some(COMPACTION_HOST_WORKSPACE_ID),
|
||||
);
|
||||
body
|
||||
};
|
||||
assert_full_initial_summary(&input, &inherited);
|
||||
assert!(input.contains("UNRECORDED_FALLBACK_GOAL"), "{case}");
|
||||
assert!(input.contains("## Previous Journal reference (historical, not automatically retained)\nPREVIOUS_JOURNAL_DELEGATED_CONTEXT"));
|
||||
assert!(input.contains("## Previous reference input (historical, not automatically retained)\nPREVIOUS_JOURNAL_DELEGATED_CONTEXT"));
|
||||
assert!(
|
||||
fixture.worker.history().iter().all(|item| !item
|
||||
.as_text()
|
||||
@@ -3567,3 +3655,132 @@ async fn journal_snapshot_counts_against_compactor_hard_input_cap_without_mutati
|
||||
);
|
||||
}
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn identical_reference_and_retained_inputs_preserve_distinct_metadata_after_compaction() {
|
||||
use session_store::*;
|
||||
let client = MockClient::new(vec![
|
||||
single_text_events("fixture"),
|
||||
single_text_events("fork checkpoint"),
|
||||
write_summary_tool_use_events("review-summary", "replacement summary"),
|
||||
single_text_events("done"),
|
||||
single_text_events("restore checkpoint"),
|
||||
]);
|
||||
let mut fixture = SummaryInputFixture::new(MANUAL_ONLY_MANIFEST_TOML, client).await;
|
||||
let make = |producer: &str, compaction| {
|
||||
LoggedSystemHistoryEntry::from_input(
|
||||
LoggedSessionHistoryOrigin::Provided {
|
||||
producer: producer.into(),
|
||||
},
|
||||
None,
|
||||
SystemInputEnvelope::new(
|
||||
"identical model context",
|
||||
HistoryInputMetadata::new(
|
||||
HistoryInputUsePolicy {
|
||||
recording_material: InputEvidenceUse::Exclude,
|
||||
memory_evidence: InputEvidenceUse::Exclude,
|
||||
observation: InputObservationUse::Hidden,
|
||||
compaction,
|
||||
},
|
||||
Some(
|
||||
FeatureInputPayload::encode(
|
||||
"review:owner",
|
||||
1,
|
||||
&serde_json::json!({"producer": producer}),
|
||||
)
|
||||
.unwrap(),
|
||||
),
|
||||
None,
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.to_history_entry()
|
||||
};
|
||||
let historical = make("old-reference", InputCompactionUse::ReferenceInput);
|
||||
let retained = make("retained-context", InputCompactionUse::Retain);
|
||||
let repeated = make("second-retained-context", InputCompactionUse::Retain);
|
||||
assert_ne!(historical.metadata.entry_id, retained.metadata.entry_id);
|
||||
fixture
|
||||
.restore_seed(
|
||||
vec![historical.clone(), retained.clone(), repeated.clone()],
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
let session = fixture.worker.session_id();
|
||||
let original = fixture.worker.segment_id();
|
||||
fixture
|
||||
.worker
|
||||
.store()
|
||||
.append(
|
||||
session,
|
||||
original,
|
||||
&LogEntry::Extension {
|
||||
ts: 2,
|
||||
domain: "test.fork_drift".into(),
|
||||
payload: serde_json::json!({}),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
fixture.worker.run_text("fork checkpoint").await.unwrap();
|
||||
let fork = fixture.worker.segment_id();
|
||||
assert_ne!(fork, original);
|
||||
let forked = replacement_seed(&fixture.worker);
|
||||
for expected in [&historical, &retained, &repeated] {
|
||||
assert!(forked.contains(expected));
|
||||
}
|
||||
fixture.worker.compact(1_000_000).await.unwrap();
|
||||
let seed = replacement_seed(&fixture.worker);
|
||||
let matching: Vec<_> = seed.iter().filter(|e| e.item == retained.item).collect();
|
||||
assert_eq!(matching.len(), 2);
|
||||
assert_eq!(
|
||||
matching[0].metadata, retained.metadata,
|
||||
"a retained logical entry must not inherit identity/policy/payload from an equal-body reference that was removed"
|
||||
);
|
||||
|
||||
assert_eq!(matching[1].metadata, repeated.metadata);
|
||||
let compacted = fixture.worker.segment_id();
|
||||
let reopened = FsStore::new(fixture._store_dir.path()).unwrap();
|
||||
let records = reopened.read_all(session, compacted).unwrap();
|
||||
assert!(
|
||||
matches!(&records[0], LogEntry::AnnotatedSegmentStart { compacted_from: Some(source), .. } if source.segment_id == fork)
|
||||
);
|
||||
fixture.worker.run_text("restore checkpoint").await.unwrap();
|
||||
let records = fixture.worker.store().read_all(session, compacted).unwrap();
|
||||
let snapshot = public_snapshot::project_current_session_snapshot(&records);
|
||||
let checkpoint = snapshot
|
||||
.entries
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|entry| {
|
||||
matches!(
|
||||
entry.data,
|
||||
protocol::SessionSnapshotEntryData::UserInput { .. }
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
fixture
|
||||
.worker
|
||||
.rewind_to(checkpoint.entry_id.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
for expected in [&retained, &repeated] {
|
||||
assert!(
|
||||
fixture
|
||||
.worker
|
||||
.session_history()
|
||||
.entries()
|
||||
.iter()
|
||||
.any(|entry| entry.annotation == expected.metadata
|
||||
&& entry.item == Item::from(expected.item.clone()))
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
!fixture
|
||||
.worker
|
||||
.session_history()
|
||||
.entries()
|
||||
.iter()
|
||||
.any(|entry| entry.annotation.entry_id == historical.metadata.entry_id)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -225,8 +225,14 @@ impl WorkspaceClient for AvailableWorkspaceClient {
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
_request: WorkspaceRequest,
|
||||
request: WorkspaceRequest,
|
||||
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||
if request.path.ends_with("/authorization/check") {
|
||||
return Ok(WorkspaceResponse {
|
||||
status: 200,
|
||||
body: "{}".into(),
|
||||
});
|
||||
}
|
||||
Err(WorkspaceClientError::Unavailable("test".to_string()))
|
||||
}
|
||||
}
|
||||
@@ -249,8 +255,14 @@ impl WorkspaceClient for NoopWorkspaceClient {
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
_request: WorkspaceRequest,
|
||||
request: WorkspaceRequest,
|
||||
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||
if request.path.ends_with("/authorization/check") {
|
||||
return Ok(WorkspaceResponse {
|
||||
status: 200,
|
||||
body: "{}".into(),
|
||||
});
|
||||
}
|
||||
Err(WorkspaceClientError::Unavailable(
|
||||
"test client does not execute requests".to_string(),
|
||||
))
|
||||
@@ -311,8 +323,7 @@ async fn spawn_controller(worker: Worker<MockClient, TestStore>) -> WorkerHandle
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let runtime_base = tmp.path().to_owned();
|
||||
std::mem::forget(tmp);
|
||||
let bash_output_dir = runtime_base.join("bash-output");
|
||||
let (handle, _shutdown_rx) = WorkerController::spawn(worker, &runtime_base, &bash_output_dir)
|
||||
let (handle, _shutdown_rx) = WorkerController::spawn(worker, &runtime_base)
|
||||
.await
|
||||
.unwrap();
|
||||
handle
|
||||
@@ -342,13 +353,9 @@ async fn canonical_worker_handle_uploads_and_submits_attachment() {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let (handle, shutdown_rx) = WorkerController::spawn(
|
||||
worker,
|
||||
&root.path().join("runtime"),
|
||||
&root.path().join("bash-output"),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let (handle, shutdown_rx) = WorkerController::spawn(worker, &root.path().join("runtime"))
|
||||
.await
|
||||
.unwrap();
|
||||
let mut rx = handle.subscribe();
|
||||
let file = handle
|
||||
.upload_file_with_context(
|
||||
@@ -408,24 +415,20 @@ async fn canonical_worker_handle_uploads_and_submits_attachment() {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn controller_grants_read_scope_for_exact_bash_output_directory() {
|
||||
async fn controller_does_not_grant_or_create_a_model_visible_bash_output_directory() {
|
||||
let worker = make_worker(MockClient::new(simple_text_events())).await;
|
||||
let shared_scope = worker.scope().clone();
|
||||
let runtime_base = tempfile::tempdir().unwrap();
|
||||
let worker_tmp = tempfile::tempdir().unwrap();
|
||||
let bash_output_dir = worker_tmp.path().join("worker-1").join("bash-output");
|
||||
let original_rules = shared_scope.snapshot().allow_rules().to_vec();
|
||||
|
||||
let (handle, shutdown_rx) =
|
||||
WorkerController::spawn(worker, runtime_base.path(), &bash_output_dir)
|
||||
.await
|
||||
.unwrap();
|
||||
let (handle, shutdown_rx) = WorkerController::spawn(worker, runtime_base.path())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(bash_output_dir.is_dir());
|
||||
assert!(shared_scope.snapshot().allow_rules().iter().any(|rule| {
|
||||
rule.target == bash_output_dir
|
||||
&& rule.permission == manifest::Permission::Read
|
||||
&& rule.recursive
|
||||
}));
|
||||
assert_eq!(
|
||||
shared_scope.snapshot().allow_rules(),
|
||||
original_rules.as_slice()
|
||||
);
|
||||
assert!(!handle.runtime_dir.path().join("bash-output").exists());
|
||||
|
||||
handle
|
||||
@@ -451,9 +454,9 @@ async fn shutdown_closes_bound_workdir_session() {
|
||||
.start_command(CommandRequest {
|
||||
command: "sleep 30".to_owned(),
|
||||
timeout_secs: 60,
|
||||
output_limit: 1024,
|
||||
|
||||
transport_byte_budget: workdir::MAX_COMMAND_TRANSPORT_BYTES,
|
||||
cwd: workdir::WorkdirPath::root(),
|
||||
spill_dir: None,
|
||||
tool_call_id: None,
|
||||
})
|
||||
.await
|
||||
@@ -461,11 +464,9 @@ async fn shutdown_closes_bound_workdir_session() {
|
||||
worker.bind_single_workdir_session(Some(Arc::clone(&session)));
|
||||
|
||||
let runtime_base = tempfile::tempdir().unwrap();
|
||||
let bash_output_dir = runtime_base.path().join("bash-output");
|
||||
let (handle, shutdown_rx) =
|
||||
WorkerController::spawn(worker, runtime_base.path(), &bash_output_dir)
|
||||
.await
|
||||
.unwrap();
|
||||
let (handle, shutdown_rx) = WorkerController::spawn(worker, runtime_base.path())
|
||||
.await
|
||||
.unwrap();
|
||||
handle
|
||||
.send(Method::Shutdown {
|
||||
command: worker_command(&handle),
|
||||
@@ -501,9 +502,9 @@ async fn controller_filters_workdir_command_not_owned_by_worker() {
|
||||
.start_command(CommandRequest {
|
||||
command: "sleep 0.2".to_owned(),
|
||||
timeout_secs: 5,
|
||||
output_limit: 1024,
|
||||
|
||||
transport_byte_budget: workdir::MAX_COMMAND_TRANSPORT_BYTES,
|
||||
cwd: workdir::WorkdirPath::root(),
|
||||
spill_dir: None,
|
||||
tool_call_id: Some("foreign-tool-call".into()),
|
||||
})
|
||||
.await
|
||||
@@ -529,7 +530,10 @@ async fn controller_filters_workdir_command_not_owned_by_worker() {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(output.status, workdir::CommandStatus::Completed);
|
||||
assert_eq!(
|
||||
output.terminal.unwrap().status,
|
||||
workdir::CommandStatus::Completed
|
||||
);
|
||||
handle
|
||||
.send(Method::Shutdown {
|
||||
command: worker_command(&handle),
|
||||
@@ -738,9 +742,8 @@ async fn controller_startup_failure_closes_bound_workdir_session() {
|
||||
let invalid_runtime_base = runtime_base.path().join("not-a-directory");
|
||||
std::fs::write(&invalid_runtime_base, "file").unwrap();
|
||||
|
||||
let bash_output_dir = runtime_base.path().join("bash-output");
|
||||
assert!(
|
||||
WorkerController::spawn(worker, &invalid_runtime_base, &bash_output_dir)
|
||||
WorkerController::spawn(worker, &invalid_runtime_base)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
@@ -749,9 +752,9 @@ async fn controller_startup_failure_closes_bound_workdir_session() {
|
||||
.start_command(CommandRequest {
|
||||
command: "printf unreachable".to_owned(),
|
||||
timeout_secs: 5,
|
||||
output_limit: 1024,
|
||||
|
||||
transport_byte_budget: workdir::MAX_COMMAND_TRANSPORT_BYTES,
|
||||
cwd: workdir::WorkdirPath::root(),
|
||||
spill_dir: None,
|
||||
tool_call_id: None,
|
||||
})
|
||||
.await,
|
||||
@@ -926,11 +929,7 @@ mod socket_startup_failure {
|
||||
|
||||
let result = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(5),
|
||||
WorkerController::spawn_runtime_managed_run(
|
||||
worker,
|
||||
&run_dir,
|
||||
&root.path().join("bash-output"),
|
||||
),
|
||||
WorkerController::spawn_runtime_managed_run(worker, &run_dir),
|
||||
)
|
||||
.await
|
||||
.expect("socket startup and rollback must settle");
|
||||
@@ -953,9 +952,8 @@ mod socket_startup_failure {
|
||||
.start_command(CommandRequest {
|
||||
command: "printf unreachable".to_owned(),
|
||||
timeout_secs: 5,
|
||||
output_limit: 1024,
|
||||
transport_byte_budget: workdir::MAX_COMMAND_TRANSPORT_BYTES,
|
||||
cwd: workdir::WorkdirPath::root(),
|
||||
spill_dir: None,
|
||||
tool_call_id: None,
|
||||
})
|
||||
.await,
|
||||
@@ -1117,6 +1115,12 @@ impl WorkspaceClient for JournalWorkspaceClient {
|
||||
&self,
|
||||
request: WorkspaceRequest,
|
||||
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||
if request.path.ends_with("/authorization/check") {
|
||||
return Ok(WorkspaceResponse {
|
||||
status: 200,
|
||||
body: "{}".into(),
|
||||
});
|
||||
}
|
||||
self.requests.lock().unwrap().push(request.clone());
|
||||
let (status, body) = if request.path.ends_with("/subjektiv/journal/job/finish") {
|
||||
(
|
||||
@@ -1758,8 +1762,7 @@ permission = "write"
|
||||
let client = MockClient::new(simple_text_events());
|
||||
let worker = make_worker_with_pwd_and_manifest(client, manifest).await.0;
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let bash_output_dir = tmp.path().join("bash-output");
|
||||
let result = WorkerController::spawn(worker, tmp.path(), &bash_output_dir).await;
|
||||
let result = WorkerController::spawn(worker, tmp.path()).await;
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"feature exposure must not imply delegation authority"
|
||||
@@ -3132,11 +3135,9 @@ async fn status_json_reflects_worker_name() {
|
||||
async fn shutdown_closes_method_admission_before_terminal_confirmation() {
|
||||
let worker = make_worker(MockClient::new(simple_text_events())).await;
|
||||
let runtime_base = tempfile::tempdir().unwrap();
|
||||
let bash_output_dir = runtime_base.path().join("bash-output");
|
||||
let (handle, mut shutdown_rx) =
|
||||
WorkerController::spawn(worker, runtime_base.path(), &bash_output_dir)
|
||||
.await
|
||||
.unwrap();
|
||||
let (handle, mut shutdown_rx) = WorkerController::spawn(worker, runtime_base.path())
|
||||
.await
|
||||
.unwrap();
|
||||
handle
|
||||
.send(Method::Shutdown {
|
||||
command: worker_command(&handle),
|
||||
@@ -3171,11 +3172,9 @@ async fn shutdown_joins_socket_server_with_active_connection() {
|
||||
|
||||
let worker = make_worker(MockClient::new(simple_text_events())).await;
|
||||
let runtime_base = tempfile::tempdir().unwrap();
|
||||
let bash_output_dir = runtime_base.path().join("bash-output");
|
||||
let (handle, shutdown_rx) =
|
||||
WorkerController::spawn(worker, runtime_base.path(), &bash_output_dir)
|
||||
.await
|
||||
.unwrap();
|
||||
let (handle, shutdown_rx) = WorkerController::spawn(worker, runtime_base.path())
|
||||
.await
|
||||
.unwrap();
|
||||
let socket_path = handle.runtime_dir.socket_path();
|
||||
let _connection = UnixStream::connect(&socket_path).await.unwrap();
|
||||
|
||||
@@ -3292,7 +3291,7 @@ async fn socket_worker_event_turn_ended_while_idle_auto_starts_turn() {
|
||||
Ok(Some(Event::SystemItem { ref item, .. }))
|
||||
if item.get("kind").and_then(|k| k.as_str()) == Some("worker_event")
|
||||
&& item
|
||||
.pointer("/event/worker_name")
|
||||
.pointer("/presentation/event/worker_name")
|
||||
.and_then(|v| v.as_str()) == Some("child") =>
|
||||
{
|
||||
saw_worker_event_echo = true;
|
||||
|
||||
@@ -14,7 +14,7 @@ use protocol::{
|
||||
InvocationArgumentDescriptor, InvocationArgumentType, InvocationArgumentValue,
|
||||
InvocationCompletion, InvocationValue, Segment,
|
||||
};
|
||||
use session_store::{CombinedStore, FsStore, FsWorkerStore, LogEntry, SystemItem};
|
||||
use session_store::{CombinedStore, FsStore, FsWorkerStore, LogEntry};
|
||||
use worker::feature::{
|
||||
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureInvocationContext,
|
||||
FeatureInvocationHandler, FeatureInvocationHandlerError, FeatureModule, FeatureRegistryBuilder,
|
||||
@@ -224,10 +224,20 @@ fn results(worker: &Worker<Trace, TestStore>) -> Vec<FeatureInvocationResult> {
|
||||
.0
|
||||
.into_iter()
|
||||
.filter_map(|entry| match entry {
|
||||
LogEntry::AnnotatedSystemItem { entry, .. } => match entry.item {
|
||||
SystemItem::FeatureInvocationResult { result, .. } => Some(result),
|
||||
_ => None,
|
||||
},
|
||||
LogEntry::AnnotatedSystemItem { entry, .. } => {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct SavedResult {
|
||||
result: FeatureInvocationResult,
|
||||
}
|
||||
entry
|
||||
.metadata
|
||||
.input_metadata
|
||||
.as_ref()?
|
||||
.feature_payload()?
|
||||
.decode::<SavedResult>("feature:invocation-result", 1)
|
||||
.ok()
|
||||
.map(|input| input.result)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
|
||||
Reference in New Issue
Block a user