use super::{
PluginRuntimeTraceEffect, PluginRuntimeTraceEvent, PluginRuntimeTraceEventClass,
PluginRuntimeTraceEventKind, PluginRuntimeTraceProposalOutcome,
PluginRuntimeTraceProposalRejection, PluginRuntimeTraceWorkspaceIoCompletion,
PluginRuntimeTraceWorkspaceObserveTaskOutcome, PluginRuntimeTraceWorkspaceReadOutcome,
PluginRuntimeTraceWorkspaceWriteOutcome,
};
use crate::{
buffer::{BufferEdit, BufferEditKind, BufferEditShape},
ecs::{
components::buffer::{BufferEntity, BufferRevision, ViewEntity},
events::edit::{
PluginBufferEditProposalReceipt, PluginBufferEditProvenance, PluginProposalId,
},
},
fs_utils::FilesystemConfig,
plugin::{
PendingPluginUpdateBatch, PendingPluginWorkspaceIoBatch, PluginCapabilities,
PluginEffectBatchLimit, PluginHandleStore, PluginHostContext, PluginHostImportSession,
PluginIdentity, PluginOperationalEvent, PluginOperationalImport, PluginOperationalQueue,
PluginWorkspaceIoBudget, PluginWorkspaceIoWorkerQueue,
PluginWorkspaceObserveTaskCompletion, PluginWorkspaceObserveTaskHandle,
PluginWorkspaceObserveTaskQueue, WorkspacePathGrant, WorkspacePathRef,
},
text_stream::TextRevision,
};
use bevy::prelude::Entity;
use std::{
fs,
num::{NonZeroU64, NonZeroUsize},
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
#[test]
fn drained_effect_trace_is_redacted_and_structured() {
let identity = identity("trace-effects");
let buffer = BufferEntity(Entity::from_raw_u32(1).expect("entity"));
let view = ViewEntity(Entity::from_raw_u32(2).expect("entity"));
let mut pending =
PendingPluginUpdateBatch::from_validated_limit(identity, PluginEffectBatchLimit::default());
pending
.propose_buffer_edit(
crate::plugin::PluginBufferEditProposal::from_observed_revision(
buffer,
BufferEdit::insert(3, "secret inserted text"),
TextRevision::from(7),
),
)
.expect("buffer edit should queue");
pending
.push_status_text(view, "secret status")
.expect("status should queue");
let report = pending.seal().drain();
let trace = PluginRuntimeTraceEvent::drained_effects(&report);
assert_eq!(trace.identity(), Some("trace-effects"));
assert_eq!(trace.class(), PluginRuntimeTraceEventClass::EffectsDrained);
let PluginRuntimeTraceEventKind::EffectsDrained { effects } = trace.kind() else {
panic!("expected drained effects trace");
};
assert_eq!(effects.len(), 2);
match &effects[0] {
PluginRuntimeTraceEffect::BufferEdit {
base_revision,
edit,
..
} => {
assert_eq!(*base_revision, TextRevision::from(7));
assert_eq!(edit.kind(), BufferEditKind::Insert);
assert_eq!(edit.byte_index(), Some(3));
assert_eq!(
edit.replacement_byte_len(),
Some("secret inserted text".len())
);
}
other @ PluginRuntimeTraceEffect::Status { .. } => {
panic!("expected buffer edit trace, got {other:?}");
}
}
let rendered = trace.to_string();
assert!(rendered.contains("effects-drained"));
assert!(rendered.contains("replacement_byte_len=20"));
assert!(rendered.contains("status info byte_len=13"));
assert!(!format!("{trace:?}").contains("secret"));
assert!(!rendered.contains("secret"));
}
#[test]
fn workspace_io_trace_is_operation_typed_and_redacted() {
let root = temp_dir("trace-workspace-io");
fs::create_dir(root.join("docs")).expect("docs directory should be created");
fs::write(root.join("docs/input.txt"), "secret workspace bytes")
.expect("fixture should be written");
let filesystem = config(&root);
let host = workspace_host();
let mut batch = PendingPluginWorkspaceIoBatch::from_validated_budget(
identity("trace-workspace"),
PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget should validate"),
);
let read = host
.authorize_workspace_observe(
WorkspacePathRef::try_from("docs/input.txt").expect("path should validate"),
)
.expect("read should authorize");
batch.push_read(read).expect("read should queue");
let write = host
.authorize_workspace_artifact_write(
WorkspacePathRef::try_from("docs/output.txt").expect("path should validate"),
)
.expect("write should authorize");
batch
.push_write(write, b"secret write bytes".to_vec())
.expect("write should queue");
let report = batch.seal().execute_synchronous(&filesystem);
let trace = PluginRuntimeTraceEvent::workspace_io_completed(&report);
assert_eq!(trace.identity(), Some("trace-workspace"));
assert_eq!(
trace.class(),
PluginRuntimeTraceEventClass::WorkspaceIoCompleted
);
let PluginRuntimeTraceEventKind::WorkspaceIoCompleted { completions } = trace.kind() else {
panic!("expected workspace I/O trace");
};
assert_eq!(completions.len(), 2);
assert!(matches!(
completions[0],
PluginRuntimeTraceWorkspaceIoCompletion::Read {
outcome: PluginRuntimeTraceWorkspaceReadOutcome::Ok { byte_len: 22, .. },
..
}
));
assert!(matches!(
completions[1],
PluginRuntimeTraceWorkspaceIoCompletion::Write {
outcome: PluginRuntimeTraceWorkspaceWriteOutcome::Ok { .. },
..
}
));
let rendered = trace.to_string();
assert!(rendered.contains("workspace-io-completed"));
assert!(rendered.contains("path_byte_len=14"));
assert!(rendered.contains("byte_len=22"));
assert!(!format!("{trace:?}").contains("secret"));
assert!(!rendered.contains("secret"));
assert!(!rendered.contains(root.to_string_lossy().as_ref()));
assert!(!rendered.contains("input.txt"));
assert!(!rendered.contains("output.txt"));
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn workspace_observe_task_trace_is_guest_shaped_and_redacted() {
let root = temp_dir("trace-workspace-observe-task");
fs::create_dir(root.join("docs")).expect("docs directory should be created");
fs::write(root.join("docs/input.txt"), "secret task bytes").expect("fixture should be written");
let filesystem = config(&root);
let identity = identity("trace-workspace");
let (task_handle, completion) =
workspace_observe_task_completion(&identity, &filesystem, "docs/input.txt");
let trace = PluginRuntimeTraceEvent::workspace_observe_task_completed(&completion);
assert_eq!(trace.identity(), Some("trace-workspace"));
assert_eq!(
trace.class(),
PluginRuntimeTraceEventClass::WorkspaceObserveTaskCompleted
);
let PluginRuntimeTraceEventKind::WorkspaceObserveTaskCompleted {
completion: traced_completion,
} = trace.kind()
else {
panic!("expected workspace observe task trace");
};
assert_eq!(traced_completion.handle(), &task_handle);
assert_eq!(traced_completion.task_id(), task_handle.task_id());
assert_eq!(traced_completion.task_id().get(), 1);
let outcome =
PluginRuntimeTraceWorkspaceObserveTaskOutcome::from(traced_completion.outcome_shape());
assert_eq!(
outcome,
PluginRuntimeTraceWorkspaceObserveTaskOutcome::Bytes { byte_len: 17 }
);
assert!(outcome.is_ok());
let rendered = trace.to_string();
assert_eq!(
rendered,
"plugin \"trace-workspace\" trace workspace-observe-task-completed task_id=1 outcome=bytes byte_len=17"
);
assert!(!format!("{trace:?}").contains("secret"));
assert!(!rendered.contains("secret"));
assert!(!rendered.contains(root.to_string_lossy().as_ref()));
assert!(!rendered.contains("input.txt"));
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn workspace_cancellation_traces_are_redacted_and_structured() {
let root = temp_dir("trace-workspace-cancel");
fs::create_dir(root.join("docs")).expect("docs directory should be created");
fs::write(root.join("docs/retained.txt"), "secret retained task bytes")
.expect("retained fixture should be written");
let filesystem = config(&root);
let identity = identity("trace-cancel");
let host = workspace_host_for(identity.as_str());
let mut workspace_io = PendingPluginWorkspaceIoBatch::from_validated_budget(
identity.clone(),
PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget should validate"),
);
let write = host
.authorize_workspace_artifact_write(
WorkspacePathRef::try_from("docs/secret-canceled.txt").expect("path should validate"),
)
.expect("write should authorize");
workspace_io
.push_write(write, b"secret canceled bytes".to_vec())
.expect("write should queue");
let mut workspace_queue = PluginWorkspaceIoWorkerQueue::default();
workspace_queue
.push(workspace_io.seal())
.expect("sealed workspace I/O should queue");
let workspace_report = workspace_queue.cancel_identity(&identity);
let workspace_trace = PluginRuntimeTraceEvent::from(&workspace_report);
assert_eq!(
workspace_trace.class(),
PluginRuntimeTraceEventClass::WorkspaceIoCanceled
);
let PluginRuntimeTraceEventKind::WorkspaceIoCanceled {
canceled_batches,
canceled_requests,
} = workspace_trace.kind()
else {
panic!("expected workspace I/O cancellation trace");
};
assert_eq!((*canceled_batches, *canceled_requests), (1, 1));
let rendered_workspace = workspace_trace.to_string();
assert_eq!(
rendered_workspace,
"plugin \"trace-cancel\" trace workspace-io-canceled batches=1 requests=1"
);
assert!(!format!("{workspace_trace:?}").contains("secret"));
assert!(!rendered_workspace.contains("secret"));
assert!(!rendered_workspace.contains("secret-canceled.txt"));
let handles = PluginHandleStore::new(identity.clone());
let mut session = PluginHostImportSession::new(
&host,
&handles,
PluginEffectBatchLimit::default(),
PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget should validate"),
)
.expect("session should start");
let mut task_queue = PluginWorkspaceObserveTaskQueue::default();
let _retained_handle = session
.queue_workspace_observe_task(
WorkspacePathRef::try_from("docs/retained.txt").expect("path should validate"),
&mut task_queue,
)
.expect("retained task should reserve");
let _pending_handle = session
.queue_workspace_observe_task(
WorkspacePathRef::try_from("docs/pending-secret.txt").expect("path should validate"),
&mut task_queue,
)
.expect("pending task should reserve");
let observe_tasks = session.seal().into_parts().workspace_observe_tasks;
let _enqueue = observe_tasks
.enqueue(&mut task_queue)
.expect("sealed observe tasks should enqueue");
let _retained = task_queue
.execute_next(&filesystem)
.expect("first task should become retained");
let task_report = task_queue.cancel_identity(&identity);
let task_trace = PluginRuntimeTraceEvent::from(&task_report);
assert_eq!(
task_trace.class(),
PluginRuntimeTraceEventClass::WorkspaceObserveTasksCanceled
);
let PluginRuntimeTraceEventKind::WorkspaceObserveTasksCanceled {
canceled_pending,
discarded_retained,
} = task_trace.kind()
else {
panic!("expected workspace observe task cancellation trace");
};
assert_eq!((*canceled_pending, *discarded_retained), (1, 1));
let rendered_tasks = task_trace.to_string();
assert_eq!(
rendered_tasks,
"plugin \"trace-cancel\" trace workspace-observe-tasks-canceled pending=1 retained=1"
);
assert!(!format!("{task_trace:?}").contains("secret"));
assert!(!rendered_tasks.contains("secret"));
assert!(!rendered_tasks.contains("retained.txt"));
assert!(!rendered_tasks.contains("pending-secret.txt"));
assert!(!rendered_tasks.contains(root.to_string_lossy().as_ref()));
let _cleanup = fs::remove_dir_all(root);
}
#[test]
fn operational_trace_keeps_closed_event_shape() {
let identity = identity("trace-operational");
let event = PluginOperationalEvent::denied_import(
&identity,
PluginOperationalImport::WorkspaceArtifactWrite,
);
let trace = PluginRuntimeTraceEvent::operational(&event);
assert_eq!(trace.identity(), Some("trace-operational"));
assert_eq!(trace.class(), PluginRuntimeTraceEventClass::Operational);
assert_eq!(
trace.to_string(),
"plugin \"trace-operational\" trace operational denied-import import=workspace.artifact_write"
);
assert!(!format!("{trace:?}").contains("docs/secret.txt"));
}
#[test]
fn unscoped_operational_trace_stays_unscoped() {
let event = PluginOperationalEvent::queue_saturated(
PluginOperationalQueue::WorkspaceIo,
count_limit(1),
);
let trace = PluginRuntimeTraceEvent::operational(&event);
assert_eq!(trace.identity(), None);
assert_eq!(
trace.to_string(),
"plugin trace operational queue-saturated queue=workspace-io limit=1"
);
}
#[test]
fn proposal_receipt_trace_redacts_edit_payload_and_owner_error_detail() {
let source = identity("trace-proposal");
let provenance = PluginBufferEditProvenance::buffer_propose_edit(source, TextRevision::from(7))
.with_proposal_id(PluginProposalId::from_nonzero(
NonZeroU64::new(9).expect("non-zero proposal"),
));
let target = BufferEntity(Entity::from_raw_u32(1).expect("entity"));
let edit = BufferEdit::replace_unchecked(0..4, "secret replacement");
let receipt = PluginBufferEditProposalReceipt::owner_applied(
&provenance,
target,
BufferEditShape::from_edit(&edit),
BufferRevision {
revision: TextRevision::from(7),
},
BufferRevision {
revision: TextRevision::from(8),
},
)
.expect("proposal id should produce receipt");
let trace = PluginRuntimeTraceEvent::proposal_receipt(&receipt);
assert_eq!(trace.identity(), Some("trace-proposal"));
let PluginRuntimeTraceEventKind::ProposalReceipt { receipt } = trace.kind() else {
panic!("expected proposal receipt trace");
};
assert_eq!(receipt.proposal(), 9);
assert_eq!(receipt.proposed().kind(), BufferEditKind::Replace);
assert_eq!(
receipt.outcome(),
&PluginRuntimeTraceProposalOutcome::Applied {
previous_revision: TextRevision::from(7),
current_revision: TextRevision::from(8),
}
);
let rendered = trace.to_string();
assert!(rendered.contains("proposal=9"));
assert!(rendered.contains("replacement_byte_len=18"));
assert!(!format!("{trace:?}").contains("secret"));
assert!(!rendered.contains("secret"));
}
#[test]
fn stale_proposal_trace_uses_closed_rejection_shape() {
let source = identity("trace-stale");
let provenance = PluginBufferEditProvenance::buffer_propose_edit(source, TextRevision::from(7))
.with_proposal_id(PluginProposalId::from_nonzero(
NonZeroU64::new(10).expect("non-zero proposal"),
));
let target = BufferEntity(Entity::from_raw_u32(1).expect("entity"));
let edit = BufferEdit::insert(0, "secret");
let reason = crate::ecs::events::edit::BufferEditRejectionReason::StaleRevision {
expected: TextRevision::from(7),
actual: TextRevision::from(9),
};
let receipt = PluginBufferEditProposalReceipt::owner_rejected(
&provenance,
target,
BufferEditShape::from_edit(&edit),
&reason,
)
.expect("proposal id should produce receipt");
let trace = PluginRuntimeTraceEvent::proposal_receipt(&receipt);
let PluginRuntimeTraceEventKind::ProposalReceipt { receipt } = trace.kind() else {
panic!("expected proposal receipt trace");
};
assert_eq!(
receipt.outcome(),
&PluginRuntimeTraceProposalOutcome::Rejected {
owner: super::PluginRuntimeTraceProposalOwner::BufferOwner,
reason: PluginRuntimeTraceProposalRejection::StaleRevision {
expected: TextRevision::from(7),
actual: TextRevision::from(9),
},
}
);
assert!(!trace.to_string().contains("secret"));
}
#[test]
fn individual_discard_reports_adapt_to_update_discard_trace() {
let view = ViewEntity(Entity::from_raw_u32(2).expect("entity"));
let mut effects = PendingPluginUpdateBatch::from_validated_limit(
identity("trace-effects"),
PluginEffectBatchLimit::default(),
);
effects
.push_status_text(view, "secret discarded status")
.expect("status should queue");
let effects_trace = PluginRuntimeTraceEvent::from(&effects.discard());
assert_eq!(
effects_trace.class(),
PluginRuntimeTraceEventClass::UpdateDiscarded
);
assert_eq!(
effects_trace.to_string(),
"plugin \"trace-effects\" trace update-discarded effects=1 workspace_io=0 workspace_observe_tasks=0"
);
assert!(!effects_trace.to_string().contains("secret"));
let host = workspace_host_for("trace-workspace-io");
let mut workspace_io = PendingPluginWorkspaceIoBatch::from_validated_budget(
identity("trace-workspace-io"),
PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget should validate"),
);
let read = host
.authorize_workspace_observe(
WorkspacePathRef::try_from("docs/missing.txt").expect("path should validate"),
)
.expect("read should authorize");
workspace_io.push_read(read).expect("read should queue");
let workspace_trace = PluginRuntimeTraceEvent::from(&workspace_io.discard());
assert_eq!(
workspace_trace.to_string(),
"plugin \"trace-workspace-io\" trace update-discarded effects=0 workspace_io=1 workspace_observe_tasks=0"
);
assert!(!workspace_trace.to_string().contains("missing.txt"));
let task_identity = identity("trace-task");
let task_host = workspace_host_for(task_identity.as_str());
let handles = PluginHandleStore::new(task_identity);
let mut task_queue = PluginWorkspaceObserveTaskQueue::default();
let mut session = PluginHostImportSession::new(
&task_host,
&handles,
PluginEffectBatchLimit::default(),
PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget should validate"),
)
.expect("session should start");
let _task_handle = session
.queue_workspace_observe_task(
WorkspacePathRef::try_from("docs/task.txt").expect("path should validate"),
&mut task_queue,
)
.expect("task should reserve");
let discard = session.discard();
let task_trace = PluginRuntimeTraceEvent::from(discard.workspace_observe_tasks());
assert_eq!(
task_trace.to_string(),
"plugin \"trace-task\" trace update-discarded effects=0 workspace_io=0 workspace_observe_tasks=1"
);
assert!(!task_trace.to_string().contains("task.txt"));
assert_eq!(task_queue.reserved_pending_len(), 0);
}
#[test]
fn owner_discard_report_adapts_to_update_discard_trace() {
let rendered = owner_discarded_trace_line(identity("trace-owner-work"));
assert_eq!(
rendered,
"plugin \"trace-owner-work\" trace update-discarded effects=1 workspace_io=1 workspace_observe_tasks=0"
);
assert!(!rendered.contains("secret"));
assert!(!rendered.contains("owner-output"));
}
#[test]
fn runtime_trace_replay_golden_is_redacted_and_stable() {
let rendered = runtime_trace_replay_lines().join("\n") + "\n";
let golden = fs::read_to_string(runtime_trace_golden_path("v1.redacted.golden"))
.expect("runtime trace golden should read");
assert_eq!(rendered, golden);
assert!(!rendered.contains("secret"));
assert!(!rendered.contains("missing.txt"));
assert!(!rendered.contains("output.txt"));
}
fn runtime_trace_replay_lines() -> Vec<String> {
let identity = identity("trace-replay");
vec![
drained_effect_trace_line(identity.clone()),
discarded_update_trace_line(identity.clone()),
owner_discarded_trace_line(identity.clone()),
rejected_workspace_io_trace_line(identity.clone()),
workspace_io_cancellation_trace_line(&identity),
workspace_observe_task_trace_line(&identity),
workspace_observe_task_cancellation_trace_line(&identity),
PluginRuntimeTraceEvent::operational(&PluginOperationalEvent::denied_import(
&identity,
PluginOperationalImport::WorkspaceArtifactWrite,
))
.to_string(),
PluginRuntimeTraceEvent::operational(&PluginOperationalEvent::queue_saturated(
PluginOperationalQueue::WorkspaceIo,
count_limit(1),
))
.to_string(),
proposal_receipt_trace_line(identity),
]
}
fn drained_effect_trace_line(identity: PluginIdentity) -> String {
let buffer = BufferEntity(Entity::from_raw_u32(1).expect("entity"));
let view = ViewEntity(Entity::from_raw_u32(2).expect("entity"));
let mut pending =
PendingPluginUpdateBatch::from_validated_limit(identity, PluginEffectBatchLimit::default());
pending
.propose_buffer_edit(
crate::plugin::PluginBufferEditProposal::from_observed_revision(
buffer,
BufferEdit::insert(3, "secret inserted text"),
TextRevision::from(7),
),
)
.expect("buffer edit should queue");
pending
.push_status_text(view, "secret status")
.expect("status should queue");
PluginRuntimeTraceEvent::drained_effects(&pending.seal().drain()).to_string()
}
fn discarded_update_trace_line(identity: PluginIdentity) -> String {
let view = ViewEntity(Entity::from_raw_u32(2).expect("entity"));
let host = PluginHostContext::for_test(
identity.as_str(),
PluginCapabilities {
status_publish: true,
workspace_observe: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = PluginHandleStore::new(identity);
let view_handle = handles.issue_view(view).expect("view handle");
let mut session = PluginHostImportSession::new(
&host,
&handles,
PluginEffectBatchLimit::default(),
PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget should validate"),
)
.expect("session should start");
let mut observe_tasks = PluginWorkspaceObserveTaskQueue::default();
session
.push_status_text(view_handle, "secret discarded status")
.expect("status should queue");
session
.queue_workspace_observe(
WorkspacePathRef::try_from("docs/missing.txt").expect("path should validate"),
)
.expect("workspace read should queue");
let _task_handle = session
.queue_workspace_observe_task(
WorkspacePathRef::try_from("docs/task.txt").expect("path should validate"),
&mut observe_tasks,
)
.expect("workspace task should reserve");
PluginRuntimeTraceEvent::update_discarded(&session.discard()).to_string()
}
fn owner_discarded_trace_line(identity: PluginIdentity) -> String {
let view = ViewEntity(Entity::from_raw_u32(3).expect("entity"));
let host = PluginHostContext::for_test(
identity.as_str(),
PluginCapabilities {
status_publish: true,
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
);
let mut handles = PluginHandleStore::new(identity);
let view_handle = handles.issue_view(view).expect("view handle");
let mut session = PluginHostImportSession::new(
&host,
&handles,
PluginEffectBatchLimit::default(),
PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget should validate"),
)
.expect("session should start");
session
.push_status_text(view_handle, "secret owner-discard status")
.expect("status should queue");
session
.queue_workspace_artifact_write(
WorkspacePathRef::try_from("docs/secret-owner-output.txt")
.expect("path should validate"),
b"secret owner-discard bytes".to_vec(),
)
.expect("workspace write should queue");
let update = session.seal().into_owner_and_workspace_observe_tasks();
PluginRuntimeTraceEvent::owner_work_discarded(&update.owner_batches.discard()).to_string()
}
fn rejected_workspace_io_trace_line(identity: PluginIdentity) -> String {
let root = temp_dir("trace-replay-workspace-io");
fs::create_dir(root.join("docs")).expect("docs directory should be created");
let filesystem = config(&root);
let host = workspace_host_for(identity.as_str());
let mut batch = PendingPluginWorkspaceIoBatch::from_validated_budget(
identity,
PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget should validate"),
);
let read = host
.authorize_workspace_observe(
WorkspacePathRef::try_from("docs/missing.txt").expect("path should validate"),
)
.expect("read should authorize");
batch.push_read(read).expect("read should queue");
let write = host
.authorize_workspace_artifact_write(
WorkspacePathRef::try_from("docs/missing/output.txt").expect("path should validate"),
)
.expect("write should authorize");
batch
.push_write(write, b"secret write bytes".to_vec())
.expect("write should queue");
let report = batch.seal().execute_synchronous(&filesystem);
let rendered = PluginRuntimeTraceEvent::workspace_io_completed(&report).to_string();
let _cleanup = fs::remove_dir_all(root);
rendered
}
fn workspace_io_cancellation_trace_line(identity: &PluginIdentity) -> String {
let host = workspace_host_for(identity.as_str());
let mut batch = PendingPluginWorkspaceIoBatch::from_validated_budget(
identity.clone(),
PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget should validate"),
);
let write = host
.authorize_workspace_artifact_write(
WorkspacePathRef::try_from("docs/secret-canceled.txt").expect("path should validate"),
)
.expect("write should authorize");
batch
.push_write(write, b"secret canceled bytes".to_vec())
.expect("write should queue");
let mut queue = PluginWorkspaceIoWorkerQueue::default();
queue
.push(batch.seal())
.expect("sealed workspace I/O should queue");
PluginRuntimeTraceEvent::workspace_io_canceled(&queue.cancel_identity(identity)).to_string()
}
fn workspace_observe_task_trace_line(identity: &PluginIdentity) -> String {
let root = temp_dir("trace-replay-observe-task");
fs::create_dir(root.join("docs")).expect("docs directory should be created");
fs::write(root.join("docs/input.txt"), "secret task bytes").expect("fixture should be written");
let filesystem = config(&root);
let (_task_id, completion) =
workspace_observe_task_completion(identity, &filesystem, "docs/input.txt");
let rendered =
PluginRuntimeTraceEvent::workspace_observe_task_completed(&completion).to_string();
let _cleanup = fs::remove_dir_all(root);
rendered
}
fn workspace_observe_task_cancellation_trace_line(identity: &PluginIdentity) -> String {
let root = temp_dir("trace-replay-observe-task-cancel");
fs::create_dir(root.join("docs")).expect("docs directory should be created");
fs::write(root.join("docs/retained.txt"), "secret retained task bytes")
.expect("fixture should be written");
let filesystem = config(&root);
let host = workspace_host_for(identity.as_str());
let handles = PluginHandleStore::new(identity.clone());
let mut queue = PluginWorkspaceObserveTaskQueue::default();
let mut session = PluginHostImportSession::new(
&host,
&handles,
PluginEffectBatchLimit::default(),
PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget should validate"),
)
.expect("session should start");
for path in ["docs/retained.txt", "docs/pending-secret.txt"] {
let _task_handle = session
.queue_workspace_observe_task(
WorkspacePathRef::try_from(path).expect("path should validate"),
&mut queue,
)
.expect("task handle should reserve");
}
let batches = session.seal().into_parts();
let _enqueue = batches
.workspace_observe_tasks
.enqueue(&mut queue)
.expect("sealed task batch should enqueue");
let _completion = queue
.execute_next(&filesystem)
.expect("task should execute");
let rendered =
PluginRuntimeTraceEvent::workspace_observe_tasks_canceled(&queue.cancel_identity(identity))
.to_string();
let _cleanup = fs::remove_dir_all(root);
rendered
}
fn workspace_observe_task_completion(
identity: &PluginIdentity,
filesystem: &FilesystemConfig,
path: &str,
) -> (
PluginWorkspaceObserveTaskHandle,
PluginWorkspaceObserveTaskCompletion,
) {
let host = workspace_host_for(identity.as_str());
let handles = PluginHandleStore::new(identity.clone());
let mut queue = PluginWorkspaceObserveTaskQueue::default();
let mut session = PluginHostImportSession::new(
&host,
&handles,
PluginEffectBatchLimit::default(),
PluginWorkspaceIoBudget::try_new(4, 64, 64).expect("budget should validate"),
)
.expect("session should start");
let task_resource_authority = session
.queue_workspace_observe_task(
WorkspacePathRef::try_from(path).expect("path should validate"),
&mut queue,
)
.expect("task handle should reserve");
let task_handle = task_resource_authority.handle().clone();
let batches = session.seal().into_parts();
let enqueue = batches
.workspace_observe_tasks
.enqueue(&mut queue)
.expect("sealed task batch should enqueue");
assert_eq!(enqueue.task_handles()[0], task_handle);
let completion = queue.execute_next(filesystem).expect("task should execute");
(task_handle, completion)
}
fn proposal_receipt_trace_line(identity: PluginIdentity) -> String {
let provenance =
PluginBufferEditProvenance::buffer_propose_edit(identity, TextRevision::from(7))
.with_proposal_id(PluginProposalId::from_nonzero(
NonZeroU64::new(9).expect("non-zero proposal"),
));
let target = BufferEntity(Entity::from_raw_u32(1).expect("entity"));
let edit = BufferEdit::replace_unchecked(0..4, "secret replacement");
let receipt = PluginBufferEditProposalReceipt::owner_applied(
&provenance,
target,
BufferEditShape::from_edit(&edit),
BufferRevision {
revision: TextRevision::from(7),
},
BufferRevision {
revision: TextRevision::from(8),
},
)
.expect("proposal id should produce receipt");
PluginRuntimeTraceEvent::proposal_receipt(&receipt).to_string()
}
fn identity(identity: &str) -> PluginIdentity {
PluginIdentity::try_new(identity).expect("test identity should validate")
}
fn count_limit(value: usize) -> NonZeroUsize {
NonZeroUsize::new(value).expect("test count limit should be non-zero")
}
fn workspace_host() -> PluginHostContext {
workspace_host_for("trace-workspace")
}
fn workspace_host_for(identity: &str) -> PluginHostContext {
PluginHostContext::for_test(
identity,
PluginCapabilities {
workspace_observe: vec![WorkspacePathGrant::new("docs")],
workspace_artifact_write: vec![WorkspacePathGrant::new("docs")],
..PluginCapabilities::default()
},
)
}
fn temp_dir_path(name: &str) -> PathBuf {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time should be after unix epoch")
.as_nanos();
std::env::temp_dir().join(format!(
"alma-plugin-trace-{name}-{}-{nanos}",
std::process::id()
))
}
fn temp_dir(name: &str) -> PathBuf {
let path = temp_dir_path(name);
fs::create_dir(&path).expect("temporary directory should be created");
path
}
fn config(root: &Path) -> FilesystemConfig {
FilesystemConfig {
workspace_root: root.canonicalize().expect("root should canonicalize"),
max_file_bytes: 128,
}
}
fn runtime_trace_golden_path(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/goldens/plugin/runtime-trace")
.join(name)
}