10 Commits
32 changed files with 10415 additions and 78 deletions
Generated
+6
View File
@@ -6707,15 +6707,21 @@ name = "yoi-e2e"
version = "0.0.0"
dependencies = [
"chrono",
"config-source",
"libc",
"manifest",
"protocol",
"reqwest",
"serde",
"serde_json",
"server-api",
"session-store",
"sha2 0.11.0",
"tempfile",
"tungstenite 0.29.0",
"webauthn-authenticator-rs",
"webauthn-rs-proto",
"worker-runtime",
"yoi-workspace-server",
]
+2
View File
@@ -19,6 +19,8 @@ required-features = ["ws-server", "fs-store"]
[features]
default = ["ws-server", "fs-store"]
fs-store = ["dep:fs4"]
# Offline fixture tooling only; never enabled by the product defaults.
test-support = ["fs-store", "ws-server"]
http-server = ["dep:axum", "dep:tower", "dep:reqwest"]
ws-server = ["http-server", "axum/ws", "dep:futures", "tokio/sync"]
+31
View File
@@ -8,3 +8,34 @@ Manages Worker creation, stopping, and Restore, execution connections within the
- [`execution`] — execution backends and operation-specific results.
- [`working_directory`] — repository materialization, session resources, and safe deletion.
- [`workspace_issuer`] — Workspace signature verification and replay prevention.
## Offline stopped-Worker fixtures
The nondefault `test-support` feature exposes `test_support::StoppedWorkerTemplate`.
It is intentionally limited to current-schema, stopped, attachment-free artificial
conversations with one Segment, text history, and no pending execution/activation.
It does not add a product endpoint or change create/restore authorization.
1. With the source Runtime offline, call `StoppedWorkerTemplate::extract(worker_dir)`.
The source must have its existing Runtime owner lock and identity. Extraction is
read-only; the returned serde template excludes source auth, saved Manifest,
request/config references, run directories, and Runtime operation history.
2. Initialize **new** Runtime identity and Workspace trust in a fresh destination
using normal setup. Preserve the template's Runtime and Workspace IDs.
3. Call `template.install(&options, request, &resolved_manifest)` before starting
the Runtime. Supply a fresh typed `CreateWorkerRequest` (including config/profile
references and trusted memory settings) and `manifest::WorkerManifest` resolved
for that environment. Its Worker name must equal `template.worker_name()`.
4. Start normally and use the ordinary explicit restore operation. Installation
itself creates no execution and leaves the Worker stopped with a fresh guard.
The template preserves the original Session manifest/JSONL bytes, including stable
entry IDs, timestamps, and settled submission receipts. It rejects resumable queues
and Feature invocation history. A saved system prompt is required for restore;
checkpoint receipts must match the Worker's typed receipt schema. Preserved records
must use canonical writer field shapes: unknown/duplicate fields (including nested
fields), discarded optional fields, and opaque Feature history payloads are rejected,
not stripped or rewritten. Portability/secret-marker checks do not replace
review of the supplied artificial conversation or its independently verified hashes.
The installer refuses any destination content other than initialized `auth.toml`;
interrupted publication is fail-closed, not an overwrite/recovery facility.
+4
View File
@@ -39,6 +39,10 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, AtomicU64, Ordering};
#[cfg(feature = "test-support")]
#[path = "test_support.rs"]
pub mod test_support;
const SCHEMA_VERSION: u32 = 12;
// Frozen source schemas: do not move their meaning when adding a migration.
const SOURCE_SCHEMA_VERSION: u32 = 11;
+5 -1
View File
@@ -3206,9 +3206,13 @@ mod tests {
.await
.expect_err("close must be terminal");
assert!(matches!(
closed_error,
closed_error.classification_source(),
workdir::WorkdirError::Unavailable(_)
));
assert_eq!(
closed_error.failure_reason(),
Some(workdir::WorkdirFailureReason::SessionClosed)
);
assert!(
state
.workdir_sessions
+2
View File
@@ -32,6 +32,8 @@ pub mod workspace_issuer;
#[cfg(feature = "http-server")]
pub mod workspace_request;
#[cfg(feature = "test-support")]
pub use fs_store::test_support;
#[cfg(feature = "fs-store")]
pub use fs_store::{FsRuntimeStore, FsRuntimeStoreOptions};
pub use management::RuntimeOptions;
+859
View File
@@ -0,0 +1,859 @@
//! Offline support for the approved, attachment-free stopped Worker fixture.
//!
//! Enabled only by `test-support`. This is not an import API: extraction accepts a
//! current-schema, settled, single-Segment aggregate; installation accepts only a
//! fresh Runtime directory containing its newly initialized `auth.toml`. No
//! Runtime or execution backend is constructed. Original Session bytes are never
//! rewritten. The caller must separately review/attest the artificial conversation;
//! structural checks and secret-marker checks cannot prove arbitrary prose safe.
use super::*;
use crate::catalog::ProfileSourceArchiveSource;
use crate::workspace_issuer::{
WorkspaceIssuerTrustRecord, WorkspaceIssuerTrustState, validate_workspace_issuer_trust_records,
};
use session_store::{
SegmentId, SessionId, WorkerActiveSegmentRef, WorkerMetadata, WorkerSessionStore,
};
#[cfg(test)]
#[path = "test_support_tests.rs"]
mod tests;
const TEMPLATE_VERSION: u32 = 1;
const MAX_SESSION_BYTES: usize = 8 * 1024 * 1024;
/// Portable fixture, intentionally without Debug (Session material is private).
/// Fields are private and every deserialized template is validated on install.
/// Serialize this value to retain the fixture; do not serialize its source aggregate.
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct StoppedWorkerTemplate {
template_version: u32,
runtime_schema_version: u32,
runtime_id: String,
workspace_id: String,
worker_id: WorkerId,
created_at_ms: Option<u64>,
session_id: SessionId,
segment_id: SegmentId,
session_manifest: Vec<u8>,
segment_jsonl: Vec<u8>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct SessionManifest {
schema_version: u32,
session_id: SessionId,
}
// Read-only projection: never retain source key material in a template.
#[derive(Deserialize)]
struct AuthIdentity {
identity_id: String,
}
#[derive(Deserialize)]
struct SourceAuth {
identity: AuthIdentity,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct InitializedAuth {
identity: crate::auth::RuntimeIdentityMaterial,
workspace_issuers: Vec<WorkspaceIssuerTrustRecord>,
#[serde(default)]
archived_legacy_issuers: Vec<String>,
}
#[derive(Deserialize)]
struct GuardState {
active_request_id: Option<String>,
owners: BTreeMap<String, GuardOwner>,
}
#[derive(Deserialize)]
struct GuardOwner {
receipt: Option<serde::de::IgnoredAny>,
}
fn reject(message: &'static str) -> RuntimeError {
RuntimeError::InvalidRequest(format!("stopped Worker fixture: {message}"))
}
fn io<T>(result: std::io::Result<T>) -> Result<T, RuntimeError> {
result.map_err(|_| reject("filesystem operation failed"))
}
fn decode<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T, RuntimeError> {
serde_json::from_slice(bytes).map_err(|_| reject("invalid typed document"))
}
fn no_symlink_components(path: &Path) -> Result<(), RuntimeError> {
let mut current = PathBuf::new();
for part in path.components() {
if matches!(part, std::path::Component::ParentDir) {
return Err(reject("parent traversal is unsupported"));
}
current.push(part);
let metadata = io(fs::symlink_metadata(&current))?;
if metadata.file_type().is_symlink() {
return Err(reject("symlinks are unsupported"));
}
}
Ok(())
}
fn check_tree(path: &Path, budget: &mut usize) -> Result<(), RuntimeError> {
if *budget == 0 {
return Err(reject("file count exceeds fixture limit"));
}
*budget -= 1;
let metadata = io(fs::symlink_metadata(path))?;
if metadata.is_dir() {
for entry in io(fs::read_dir(path))? {
check_tree(&io(entry)?.path(), budget)?;
}
} else if !metadata.is_file() {
return Err(reject("symlinks and special files are unsupported"));
}
Ok(())
}
fn regular_file(path: &Path) -> Result<(), RuntimeError> {
no_symlink_components(path)?;
if !io(fs::symlink_metadata(path))?.is_file() {
return Err(reject("expected a regular file"));
}
Ok(())
}
fn bytes(path: &Path) -> Result<Vec<u8>, RuntimeError> {
regular_file(path)?;
let mut options = OpenOptions::new();
options.read(true);
#[cfg(target_os = "linux")]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOATIME | libc::O_NOFOLLOW);
}
let file = io(options.open(path))?;
let mut result = Vec::new();
io(file
.take(MAX_SESSION_BYTES as u64 + 1)
.read_to_end(&mut result))?;
if result.len() > MAX_SESSION_BYTES {
return Err(reject("file exceeds fixture limit"));
}
Ok(result)
}
fn text(path: &Path) -> Result<String, RuntimeError> {
String::from_utf8(bytes(path)?).map_err(|_| reject("invalid UTF-8 document"))
}
fn portable_id(id: &str) -> bool {
!id.is_empty()
&& id.len() <= 256
&& id
.bytes()
.all(|c| c.is_ascii_alphanumeric() || b"-_".contains(&c))
}
impl StoppedWorkerTemplate {
pub fn runtime_id(&self) -> &str {
&self.runtime_id
}
pub fn workspace_id(&self) -> &str {
&self.workspace_id
}
pub fn worker_id(&self) -> WorkerId {
self.worker_id
}
pub fn worker_name(&self) -> String {
runtime_worker_name(self.worker_id)
}
pub fn session_id(&self) -> SessionId {
self.session_id
}
pub fn segment_id(&self) -> SegmentId {
self.segment_id
}
/// Extract without migration, rewriting metadata, or copying credentials,
/// resolved manifests, execution-operation receipts, or run directories.
/// The source Runtime must be offline; its existing owner lock is held for
/// the entire read. The path must be `<runtime>/workers/<WorkerId>`.
pub fn extract(worker_dir: &Path) -> Result<Self, RuntimeError> {
no_symlink_components(worker_dir)?;
let workers = worker_dir
.parent()
.ok_or_else(|| reject("invalid aggregate path"))?;
if workers.file_name().and_then(|s| s.to_str()) != Some(WORKERS_DIR) {
return Err(reject("invalid aggregate path"));
}
let root = workers
.parent()
.ok_or_else(|| reject("invalid Runtime path"))?;
// Require the existing lock, rather than creating a file in the source.
let lock_path = root.with_file_name(format!(
".{}.runtime-owner.lock",
root.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| reject("invalid Runtime path"))?
));
regular_file(&lock_path)?;
let lock = io(OpenOptions::new().read(true).open(lock_path))?;
if !io(FileExt::try_lock_shared(&lock))? {
return Err(reject("source Runtime is not offline"));
}
let _lock = RuntimeStoreOwnerLock { file: lock };
check_tree(worker_dir, &mut 256)?;
let snapshot: RuntimeSnapshot = decode(&bytes(&root.join(RUNTIME_FILE))?)?;
snapshot
.validate(&root.join(RUNTIME_FILE))
.map_err(|_| reject("unsupported Runtime schema"))?;
if !snapshot.subject_create_recovery_fences.is_empty()
|| !snapshot.subject_releases.is_empty()
{
return Err(reject("source has lifecycle operations"));
}
let record: WorkerAggregateRecord = decode(&bytes(&worker_dir.join(WORKER_FILE))?)?;
let record = record
.validate(&worker_dir.join(WORKER_FILE))
.map_err(|_| reject("invalid Worker schema or identity"))?;
let guard: GuardState = decode(
&serde_json::to_vec(&record.restore_guard)
.map_err(|_| reject("invalid restore guard"))?,
)?;
if record.status != WorkerStatus::Stopped
|| guard.active_request_id.is_some()
|| guard.owners.values().any(|owner| owner.receipt.is_none())
{
return Err(reject("Worker is not settled and stopped"));
}
let PersistedWorkerExecutionState::Available(execution) = &record.execution_state else {
return Err(reject("pending or unavailable execution metadata"));
};
// A stopped aggregate may retain the empty historical binding marker.
// It is deliberately not copied to the installed Worker.
if execution.restore_intent != WorkerRestoreIntent::Explicit {
return Err(reject("Worker requires explicit restore"));
}
if !record.workdir_attachments.is_empty() || !record.logical_workdir_attachments.is_empty()
{
return Err(reject("attachments are unsupported"));
}
check_request(&execution.request, false)?;
let workspace_id = record
.workspace_id
.ok_or_else(|| reject("missing Workspace identity"))?;
let metadata: WorkerMetadata = decode(&bytes(&worker_dir.join(WORKER_METADATA_FILE))?)?;
crate::worker_backend::validate_fixture_restore_metadata(&metadata, &execution.request)
.map_err(|_| reject("invalid source restore Manifest or settings"))?;
if metadata.worker_name != runtime_worker_name(record.worker_id)
|| metadata.workspace_id.as_deref() != Some(&workspace_id)
|| !metadata.spawned_children.is_empty()
|| !metadata.reclaimed_children.is_empty()
|| !metadata.peers.is_empty()
|| metadata.subjektiv_session_attribution.is_some()
{
return Err(reject("unsupported or inconsistent Worker metadata"));
}
let active = metadata
.active
.ok_or_else(|| reject("missing active Session"))?;
let segment_id = active
.segment_id
.ok_or_else(|| reject("unmaterialized Session"))?;
let source_auth: SourceAuth = toml::from_str(&text(&root.join("auth.toml"))?)
.map_err(|_| reject("missing Runtime identity"))?;
let session = worker_dir.join("session");
let fixture = Self {
template_version: TEMPLATE_VERSION,
runtime_schema_version: SCHEMA_VERSION,
runtime_id: source_auth.identity.identity_id,
workspace_id,
worker_id: record.worker_id,
created_at_ms: record.created_at_ms,
session_id: active.session_id,
segment_id,
session_manifest: bytes(&session.join("session.json"))?,
segment_jsonl: bytes(&session.join("segments").join(format!("{segment_id}.jsonl")))?,
};
fixture.validate_shape()?;
fixture.validate_session(&session)?;
Ok(fixture)
}
/// Install after normal identity/trust initialization, before Runtime start.
/// `request` and `manifest` supply NEW backend/config/profile references and
/// freshly resolved policy, not a launch. The saved source Manifest is never
/// reused. `manifest.worker.name` must equal `worker_name()`.
/// Only `auth.toml` may already exist. The owner lock fences live Runtimes;
/// staged files are synced and published without overwriting any Worker.
/// An interrupted publication fails closed as an incomplete Runtime layout.
pub fn install(
&self,
options: &FsRuntimeStoreOptions,
request: CreateWorkerRequest,
manifest: &manifest::WorkerManifest,
) -> Result<(), RuntimeError> {
self.validate_shape()?;
check_request(&request, true)?;
if manifest.worker.name != self.worker_name() {
return Err(reject("resolved Manifest Worker name mismatch"));
}
let mut metadata = WorkerMetadata::new(
self.worker_name(),
Some(WorkerActiveSegmentRef {
session_id: self.session_id,
segment_id: Some(self.segment_id),
}),
)
.with_workspace_id(&self.workspace_id);
metadata.resolved_manifest_snapshot = Some(
manifest::write_persisted_worker_manifest_snapshot(manifest)
.map_err(|_| reject("invalid resolved Manifest"))?,
);
crate::worker_backend::validate_fixture_restore_metadata(&metadata, &request)
.map_err(|_| reject("resolved Manifest does not match fresh request settings"))?;
if options.runtime_id != self.runtime_id
|| request.worker_id != self.worker_id
|| request
.workspace_api
.as_ref()
.map(|api| api.workspace_id.as_str())
!= Some(&self.workspace_id)
{
return Err(reject("installation identity mismatch"));
}
no_symlink_components(&options.root)?;
check_tree(&options.root, &mut 256)?;
// Validate the lock location before the ordinary owner-lock opener.
let lock_path = options.root.with_file_name(format!(
".{}.runtime-owner.lock",
options
.root
.file_name()
.and_then(|s| s.to_str())
.ok_or_else(|| reject("invalid Runtime path"))?
));
if fs::symlink_metadata(&lock_path).is_ok() {
regular_file(&lock_path)?;
}
let lock = acquire_runtime_store_owner_lock(&options.root)?;
validate_fresh_runtime_store_layout(&options.root)
.map_err(|_| reject("destination is not fresh; refusing overwrite"))?;
let auth: InitializedAuth = toml::from_str(&text(&options.root.join("auth.toml"))?)
.map_err(|_| reject("initialize Runtime identity and trust before install"))?;
if auth.identity.identity_id != self.runtime_id
|| !auth.archived_legacy_issuers.is_empty()
|| auth.identity.signing_key().is_err()
{
return Err(reject("invalid destination Runtime identity"));
}
validate_workspace_issuer_trust_records(&auth.workspace_issuers)
.map_err(|_| reject("invalid destination Workspace trust"))?;
let api = request
.workspace_api
.as_ref()
.expect("validated Workspace binding");
if !auth.workspace_issuers.iter().any(|trust| {
trust.workspace_id == self.workspace_id
&& trust.backend_url == api.base_url
&& trust.state == WorkspaceIssuerTrustState::Active
}) {
return Err(reject("request does not match initialized Workspace trust"));
}
let stage = options
.root
.join(format!(".fixture-{}", uuid::Uuid::now_v7()));
io(fs::create_dir(&stage))?;
let store = FsRuntimeStore {
root: stage.clone(),
_owner_lock: Some(lock),
worker_write_fault: Arc::new(AtomicU8::new(AtomicWriteFault::None as u8)),
};
let result = (|| {
let worker = PersistedWorkerRecord {
restore_guard: crate::runtime::RestoreGuard::default(),
worker_ref: WorkerRef::new(self.worker_id),
worker_id: self.worker_id,
profile: request.profile.clone(),
display_name: request.display_name.clone(),
profile_source: request.profile_source.reference(),
config_bundle: request.config_bundle.clone(),
created_at_ms: self.created_at_ms,
status: WorkerStatus::Stopped,
last_finished_submission_request_id: None,
workspace_id: Some(self.workspace_id.clone()),
workdir_attachments: Vec::new(),
logical_workdir_attachments: Vec::new(),
execution_state: PersistedWorkerExecutionState::Available(
PersistedWorkerExecution {
request,
binding: None,
restore_intent: WorkerRestoreIntent::Explicit,
},
),
};
WorkerAggregateRecord::from_persisted(&worker)
.validate(&store.worker_dir(&self.worker_id).join(WORKER_FILE))
.map_err(|_| reject("invalid installation request"))?;
store.write_worker_record(&worker)?;
let worker_dir = store.worker_dir(&self.worker_id);
atomic_write_json(
&worker_dir.join(WORKER_METADATA_FILE),
&metadata,
"write fixture metadata",
)?;
let session = worker_dir.join("session");
io(fs::create_dir_all(session.join("segments")))?;
write_new(&session.join("session.json"), &self.session_manifest)?;
write_new(
&session
.join("segments")
.join(format!("{}.jsonl", self.segment_id)),
&self.segment_jsonl,
)?;
self.validate_session(&session)?;
store.write_runtime_snapshot(&PersistedRuntimeState {
subject_create_recovery_fences: BTreeMap::new(),
subject_releases: Vec::new(),
// Match normal store initialization: this is catalog availability,
// not a process or Worker execution start.
display_name: options.display_name.clone(),
status: RuntimeStatus::Running,
next_diagnostic_id: 1,
workers: BTreeMap::new(),
workspace_owners: BTreeMap::new(),
diagnostics: Vec::new(),
})?;
let loaded = store.load_runtime_state()?;
if loaded.workers.len() != 1 || !loaded.diagnostics.is_empty() {
return Err(reject("staged aggregate failed ordinary store roundtrip"));
}
sync_runtime_tree(&stage)?;
// No destination existed when checked under the owner lock. Hard-link
// the snapshot last with create-new semantics; never replace a file.
// Reserve the destination catalog with create-new semantics. Even
// an empty preexisting workers directory must never be replaced.
let destination_workers = options.root.join(WORKERS_DIR);
io(fs::create_dir(&destination_workers))?;
io(fs::rename(
store.worker_dir(&self.worker_id),
destination_workers.join(self.worker_id.to_string()),
))?;
sync_directory(&destination_workers, "publish stopped Worker aggregate")?;
io(fs::hard_link(
stage.join(RUNTIME_FILE),
options.root.join(RUNTIME_FILE),
))?;
sync_directory(&options.root, "publish stopped fixture")?;
Ok(())
})();
// Only remove our private staging directory, never existing destination data.
let cleanup = fs::remove_dir_all(&stage);
result?;
io(cleanup)?;
sync_directory(&options.root, "finish stopped fixture")
}
fn validate_shape(&self) -> Result<(), RuntimeError> {
if self.template_version != TEMPLATE_VERSION
|| self.runtime_schema_version != SCHEMA_VERSION
{
return Err(reject("unsupported template schema"));
}
if !portable_id(&self.runtime_id)
|| !portable_id(&self.workspace_id)
|| self.session_id.get_version_num() != 7
|| self.segment_id.get_version_num() != 7
|| self.session_manifest.len() > MAX_SESSION_BYTES
|| self.segment_jsonl.len() > MAX_SESSION_BYTES
{
return Err(reject("invalid portable identity or size"));
}
let manifest: SessionManifest = decode(&self.session_manifest)?;
if manifest.session_id != self.session_id
|| manifest.schema_version == 0
|| !self.segment_jsonl.ends_with(b"\n")
{
return Err(reject("Session identity or committed boundary mismatch"));
}
// Read-only JSON traversal, never mutation/reconstruction of Session bytes.
// Reject known secret/host markers even in otherwise valid typed records.
for line in self
.segment_jsonl
.split(|b| *b == b'\n')
.filter(|line| !line.is_empty())
{
let _: PortableJson = decode(line)?;
validate_preserved_record(line)?;
}
Ok(())
}
fn validate_history_entry(
&self,
entry: &session_store::LoggedHistoryEntry,
ids: &mut std::collections::BTreeSet<String>,
) -> Result<(), RuntimeError> {
if !portable_id(&entry.metadata.entry_id.0)
|| !ids.insert(entry.metadata.entry_id.0.clone())
{
return Err(reject("duplicate or invalid entry identity"));
}
if entry
.metadata
.input_metadata
.as_ref()
.is_some_and(|metadata| {
metadata.feature_payload().is_some() || metadata.public_item().is_some()
})
{
// These are opaque owner-specific payloads, not part of the narrow
// artificial conversation contract. Never preserve unchecked JSON.
return Err(reject("Feature history payloads are unsupported"));
}
if !matches!(
entry.item,
session_store::LoggedItem::Message { .. }
| session_store::LoggedItem::AssistantResponseBoundary { .. }
) {
return Err(reject("only text conversation records are supported"));
}
if let session_store::LoggedSessionHistoryOrigin::ModelOutput { worker } =
&entry.metadata.origin
{
// Session producer IDs identify the original execution, not the
// Workspace catalog Worker. Preserve them; do not rebind/rewrite them.
if !portable_id(&worker.worker_id)
|| worker
.workspace_id
.as_ref()
.is_some_and(|id| id != &self.workspace_id)
|| worker
.runtime_id
.as_ref()
.is_some_and(|id| id != &self.runtime_id)
{
return Err(reject("Session origin identity mismatch"));
}
}
Ok(())
}
fn validate_session(&self, root: &Path) -> Result<(), RuntimeError> {
for entry in io(fs::read_dir(root))? {
let entry = io(entry)?;
match entry.file_name().to_str() {
Some("session.json" | "segments") => {}
Some("artifacts") => check_empty_artifacts(&entry.path())?,
_ => return Err(reject("unsupported Session files")),
}
}
let entries = io(fs::read_dir(root.join("segments")))?.collect::<Result<Vec<_>, _>>();
let entries = io(entries)?;
if entries.len() != 1
|| entries[0].file_name() != format!("{}.jsonl", self.segment_id).as_str()
{
return Err(reject("only the fixed single-Segment fixture is supported"));
}
let store = WorkerSessionStore::open_read_only(root)
.map_err(|_| reject("unsupported Session schema"))?;
let logs = store
.read_all_read_only_bounded(self.session_id, self.segment_id, MAX_SESSION_BYTES as u64)
.map_err(|_| reject("invalid canonical Session records"))?
.0;
if !matches!(logs.first(), Some(session_store::LogEntry::AnnotatedSegmentStart {
session_id, system_prompt: Some(_), forked_from: None, compacted_from: None, ..
}) if *session_id == self.session_id)
{
return Err(reject(
"missing system prompt, invalid Segment seed, or unsupported lineage",
));
}
let mut ids = std::collections::BTreeSet::new();
for (index, log) in logs.iter().enumerate() {
use session_store::LogEntry;
match log {
LogEntry::AnnotatedSegmentStart { history, .. }
if index == 0 && history.is_empty() => {}
LogEntry::AnnotatedUserInput {
history,
extensions,
..
} => {
for extension in extensions {
validate_settled_activation(extension)?;
}
for entry in history {
self.validate_history_entry(entry, &mut ids)?;
}
}
LogEntry::AnnotatedAssistantItem { entry, .. } => {
self.validate_history_entry(entry, &mut ids)?
}
LogEntry::RunCompleted {
entry_id: Some(id), ..
} => {
if !portable_id(&id.0) || !ids.insert(id.0.clone()) {
return Err(reject("duplicate or invalid entry identity"));
}
}
LogEntry::Invoke { .. } | LogEntry::LlmUsage { .. } | LogEntry::TurnEnd { .. } => {}
_ => {
return Err(reject(
"unsupported conversation record or pending operation",
));
}
}
}
let state = session_store::collect_state(&logs);
if state.last_run_interrupted || state.active_run_turn_count.is_some() {
return Err(reject("Session has pending execution"));
}
for segments in &state.user_segments {
if segments
.iter()
.any(|segment| !matches!(segment, protocol::Segment::Text { .. }))
{
return Err(reject("Session attachments are unsupported"));
}
}
Ok(())
}
}
// Compare *fields*, not re-encoded bytes or scalar values (e.g. f32 JSON
// normalization). Serde's permissive/untagged/flattened compatibility readers
// can drop unknown data without surfacing it to an ignored-field callback. A
// retained field must survive the owner's typed projection at every depth.
// This intentionally accepts only canonical writer shapes: even an explicit
// empty optional field omitted by that writer is rejected, not repaired.
// The projection is validation-only; installation always writes original bytes.
fn validate_preserved_record(bytes: &[u8]) -> Result<(), RuntimeError> {
let original: serde_json::Value = decode(bytes)?;
let record: session_store::LogEntry = decode(bytes)?;
let projection =
serde_json::to_value(&record).map_err(|_| reject("invalid typed Session record"))?;
require_preserved_fields(&original, &projection)
}
fn require_preserved_fields(
original: &serde_json::Value,
projection: &serde_json::Value,
) -> Result<(), RuntimeError> {
use serde_json::Value;
match (original, projection) {
(Value::Object(original), Value::Object(projection)) => {
for (key, value) in original {
let projected = projection
.get(key)
.ok_or_else(|| reject("unknown or noncanonical Session field"))?;
require_preserved_fields(value, projected)?;
}
}
(Value::Array(original), Value::Array(projection))
if original.len() == projection.len() =>
{
for (value, projected) in original.iter().zip(projection) {
require_preserved_fields(value, projected)?;
}
}
(Value::Null, Value::Null)
| (Value::Bool(_), Value::Bool(_))
| (Value::Number(_), Value::Number(_))
| (Value::String(_), Value::String(_)) => {}
_ => return Err(reject("noncanonical Session field shape")),
}
Ok(())
}
// Exact receipt wire fields owned by worker::PendingActivationState. Keep these
// fixture-only mirrors strict; do not change the production compatibility reader.
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct SubmissionReceipt {
submission_request_id: String,
source_namespace: String,
submission_id: String,
payload_digest: String,
disposition: protocol::SubmissionDisposition,
}
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct NotificationReceipt {
notification_request_id: String,
source_namespace: String,
payload_digest: String,
}
// The approved conversation has committed submission checkpoints in its original
// Session records. Keep their receipt bytes, but reject any resumable queue or
// Feature invocation history. This projection confers no execution authority.
fn validate_settled_activation(
extension: &session_store::SessionExtension,
) -> Result<(), RuntimeError> {
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Checkpoint {
#[serde(rename = "next_activation_sequence")]
_next_activation_sequence: u64,
activating: Option<serde::de::IgnoredAny>,
activating_notification: Vec<serde::de::IgnoredAny>,
pending: Vec<serde::de::IgnoredAny>,
pending_notifications: Vec<serde::de::IgnoredAny>,
#[serde(rename = "receipts")]
_receipts: Vec<SubmissionReceipt>,
#[serde(rename = "notification_receipts")]
_notification_receipts: Vec<NotificationReceipt>,
invocation_receipts: Vec<serde::de::IgnoredAny>,
invocation_recovery_blocked: bool,
}
if extension.domain != "worker.pending_activations.v1" {
return Err(reject("unsupported Session extension"));
}
let checkpoint: Checkpoint = serde_json::from_value(extension.payload.clone())
.map_err(|_| reject("invalid Session activation checkpoint"))?;
if checkpoint.activating.is_some()
|| !checkpoint.activating_notification.is_empty()
|| !checkpoint.pending.is_empty()
|| !checkpoint.pending_notifications.is_empty()
|| !checkpoint.invocation_receipts.is_empty()
|| checkpoint.invocation_recovery_blocked
{
return Err(reject(
"Session has pending activation or invocation history",
));
}
Ok(())
}
fn check_request(request: &CreateWorkerRequest, install: bool) -> Result<(), RuntimeError> {
if !request.workdir_attachment_requests.is_empty()
|| !request.workdir_attachments.is_empty()
|| request.backend_job.is_some()
|| request.subjektiv_attached
|| request.worker_observation_enabled
|| !request.worker_observation_grants.is_empty()
|| (install && request.initial_input.is_some())
{
return Err(reject(
"unsupported capabilities, attachments, or initial input",
));
}
if install
&& (request.create_fingerprint.trim().is_empty()
|| request.config_bundle.is_none()
|| !matches!(
request.profile_source,
ProfileSourceArchiveSource::WorkspaceConfig { .. }
))
{
return Err(reject(
"fresh config/profile references and create fingerprint are required",
));
}
Ok(())
}
fn check_empty_artifacts(root: &Path) -> Result<(), RuntimeError> {
for entry in io(fs::read_dir(root))? {
let entry = io(entry)?;
let metadata = io(entry.metadata())?;
if metadata.is_dir() {
check_empty_artifacts(&entry.path())?;
} else if entry.file_name() != ".aggregate.lock" || metadata.len() != 0 {
return Err(reject("Session artifacts are unsupported"));
}
}
Ok(())
}
fn write_new(path: &Path, bytes: &[u8]) -> Result<(), RuntimeError> {
let mut file = io(OpenOptions::new().write(true).create_new(true).open(path))?;
io(file.write_all(bytes))?;
io(file.sync_all())
}
// A streaming validation visitor, not a generic document available to callers.
struct PortableJson;
impl<'de> Deserialize<'de> for PortableJson {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Visitor;
impl<'de> serde::de::Visitor<'de> for Visitor {
type Value = PortableJson;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("portable fixture JSON")
}
fn visit_bool<E: serde::de::Error>(self, _: bool) -> Result<PortableJson, E> {
Ok(PortableJson)
}
fn visit_i64<E: serde::de::Error>(self, _: i64) -> Result<PortableJson, E> {
Ok(PortableJson)
}
fn visit_u64<E: serde::de::Error>(self, _: u64) -> Result<PortableJson, E> {
Ok(PortableJson)
}
fn visit_f64<E: serde::de::Error>(self, _: f64) -> Result<PortableJson, E> {
Ok(PortableJson)
}
fn visit_unit<E: serde::de::Error>(self) -> Result<PortableJson, E> {
Ok(PortableJson)
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<PortableJson, E> {
let lower = value.to_ascii_lowercase();
if [
"://",
"private key",
"bearer ",
"sk-",
"api_key",
"access_token",
"refresh_token",
"private_key",
"authorization",
]
.iter()
.any(|marker| lower.contains(marker))
|| value
.split_whitespace()
.any(|word| word.starts_with('/') || word.contains(":\\"))
{
return Err(E::custom("nonportable or sensitive fixture material"));
}
Ok(PortableJson)
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(
self,
mut seq: A,
) -> Result<PortableJson, A::Error> {
while seq.next_element::<PortableJson>()?.is_some() {}
Ok(PortableJson)
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<PortableJson, A::Error> {
let mut keys = std::collections::BTreeSet::new();
while let Some(key) = map.next_key::<String>()? {
// A duplicate key can hide discarded data even inside an
// otherwise opaque extension payload. Never retain it.
if !keys.insert(key.clone()) {
return Err(serde::de::Error::custom("duplicate fixture JSON field"));
}
serde::de::Visitor::visit_str::<A::Error>(Visitor, &key)?;
map.next_value::<PortableJson>()?;
}
Ok(PortableJson)
}
}
deserializer.deserialize_any(Visitor)
}
}
@@ -0,0 +1,885 @@
use super::*;
use crate::auth::RuntimeIdentityMaterial;
use crate::catalog::{ProfileSelector, WorkspaceApiRef};
use crate::profile_archive::ProfileSourceGraphSummary;
use session_store::{LogEntry, Store};
use sha2::{Digest, Sha256};
const RUNTIME: &str = "fixture-runtime";
const WORKSPACE: &str = "fixture-workspace";
const NEW_URL: &str = "http://127.0.0.1:34567";
#[derive(Serialize)]
#[serde(untagged)]
enum ReceiptFixture<T> {
Valid(T),
Malformed(u8),
}
#[derive(Default, Serialize)]
struct CheckpointFixture {
next_activation_sequence: u64,
activating: Option<u8>,
activating_notification: Vec<u8>,
pending: Vec<u8>,
pending_notifications: Vec<u8>,
receipts: Vec<ReceiptFixture<SubmissionReceipt>>,
notification_receipts: Vec<ReceiptFixture<NotificationReceipt>>,
invocation_receipts: Vec<u8>,
invocation_recovery_blocked: bool,
}
fn settled_checkpoint() -> CheckpointFixture {
CheckpointFixture {
next_activation_sequence: 2,
receipts: vec![ReceiptFixture::Valid(SubmissionReceipt {
submission_request_id: "completed-submission-request".into(),
source_namespace: "fixture-source".into(),
submission_id: "completed-submission".into(),
payload_digest: "fixture-payload-digest".into(),
disposition: protocol::SubmissionDisposition::Started,
})],
notification_receipts: vec![ReceiptFixture::Valid(NotificationReceipt {
notification_request_id: "completed-notification-request".into(),
source_namespace: "fixture-source".into(),
payload_digest: "fixture-notification-digest".into(),
})],
..Default::default()
}
}
fn checkpoint_extension(checkpoint: &CheckpointFixture) -> session_store::SessionExtension {
session_store::SessionExtension::new(
"worker.pending_activations.v1",
serde_json::to_value(checkpoint).unwrap(),
)
}
fn worker_id() -> WorkerId {
"01a1657d-b762-7791-87ae-a49f28cadbce".parse().unwrap()
}
fn resolved() -> manifest::WorkerManifest {
let mut config = manifest::WorkerManifestConfig::resolution_defaults();
config.worker.name = Some(runtime_worker_name(worker_id()));
manifest::WorkerManifest::try_from(config).unwrap()
}
fn request() -> CreateWorkerRequest {
CreateWorkerRequest {
worker_id: worker_id(),
create_fingerprint: "new-create-fingerprint".into(),
profile: ProfileSelector::Builtin("default".into()),
display_name: Some("fixture".into()),
profile_source: ProfileSourceArchiveSource::WorkspaceConfig {
archive: ProfileSourceArchiveRef {
id: "new-profile".into(),
digest: "new-profile-digest".into(),
size_bytes: 1,
source_graph: ProfileSourceGraphSummary {
source_count: 1,
total_source_bytes: 1,
entrypoints: BTreeMap::from([("builtin:default".into(), "default.dcl".into())]),
import_count: 0,
},
},
},
config_bundle: Some(ConfigBundleRef {
id: "new-config".into(),
digest: "new-config-digest".into(),
}),
workspace_api: Some(WorkspaceApiRef {
workspace_id: WORKSPACE.into(),
base_url: NEW_URL.into(),
}),
initial_input: None,
workdir_attachment_requests: Vec::new(),
workdir_attachments: Vec::new(),
worker_observation_enabled: false,
worker_observation_grants: Vec::new(),
memory_settings: Some(manifest::WorkspaceMemorySettingsSnapshot {
workspace_id: WORKSPACE.into(),
language: "en".into(),
}),
subjektiv_attached: false,
backend_job: None,
}
}
fn initialize(root: &Path, runtime_id: &str, workspace_id: &str, url: &str) {
#[derive(Serialize)]
struct Auth {
identity: RuntimeIdentityMaterial,
workspace_issuers: Vec<WorkspaceIssuerTrustRecord>,
}
let issuer = RuntimeIdentityMaterial::generate("fixture-issuer").unwrap();
let digest = Sha256::digest(crate::auth::decode_public_key(&issuer.public_key).unwrap());
let fingerprint = format!(
"sha256:{}",
digest
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
);
let trust = WorkspaceIssuerTrustRecord::from_bundle(
server_api::WorkspacePublicIdentityBundle {
workspace_id: workspace_id.into(),
backend_url: url.into(),
key_id: "fixture-issuer".into(),
algorithm: "ed25519".into(),
public_key: issuer.public_key,
public_key_fingerprint: fingerprint,
},
"fixture-trust".into(),
100,
)
.unwrap();
fs::create_dir_all(root).unwrap();
fs::write(
root.join("auth.toml"),
toml::to_string(&Auth {
identity: RuntimeIdentityMaterial::generate(runtime_id).unwrap(),
workspace_issuers: vec![trust],
})
.unwrap(),
)
.unwrap();
}
fn record(request: CreateWorkerRequest) -> PersistedWorkerRecord {
PersistedWorkerRecord {
restore_guard: crate::runtime::RestoreGuard::default(),
worker_ref: WorkerRef::new(request.worker_id),
worker_id: request.worker_id,
profile: request.profile.clone(),
display_name: request.display_name.clone(),
profile_source: request.profile_source.reference(),
config_bundle: request.config_bundle.clone(),
created_at_ms: Some(123456),
status: WorkerStatus::Stopped,
last_finished_submission_request_id: Some("old-submission-operation".into()),
workspace_id: Some(WORKSPACE.into()),
workdir_attachments: vec![],
logical_workdir_attachments: vec![],
execution_state: PersistedWorkerExecutionState::Available(PersistedWorkerExecution {
request,
binding: Some(PersistedWorkerExecutionBinding {}),
restore_intent: WorkerRestoreIntent::Explicit,
}),
}
}
fn source() -> (tempfile::TempDir, PathBuf, StoppedWorkerTemplate) {
let temp = tempfile::tempdir().unwrap();
let root = temp.path().join("runtime");
initialize(&root, RUNTIME, WORKSPACE, "http://127.0.0.1:23456");
drop(
crate::Runtime::with_fs_store(FsRuntimeStoreOptions::new(&root).with_runtime_id(RUNTIME))
.unwrap(),
);
let opened = FsRuntimeStore::open_or_create(root.clone(), RUNTIME).unwrap();
let mut old = request();
old.workspace_api.as_mut().unwrap().base_url = "http://127.0.0.1:23456".into();
old.config_bundle.as_mut().unwrap().id = "old-config".into();
opened.store.write_worker_record(&record(old)).unwrap();
let worker = opened.store.worker_dir(&worker_id());
let session_id = "01a1657d-b800-7777-8888-111111111111".parse().unwrap();
let segment_id = "01a1657d-b818-77c1-bd73-176172a4bd82".parse().unwrap();
let mut metadata = WorkerMetadata::new(
runtime_worker_name(worker_id()),
Some(WorkerActiveSegmentRef {
session_id,
segment_id: Some(segment_id),
}),
)
.with_workspace_id(WORKSPACE)
.with_workspace_root(PathBuf::from("/old/host/path"));
metadata.resolved_manifest_snapshot =
Some(manifest::write_persisted_worker_manifest_snapshot(&resolved()).unwrap());
atomic_write_json(
&worker.join(WORKER_METADATA_FILE),
&metadata,
"test metadata",
)
.unwrap();
let sessions = WorkerSessionStore::new(worker.join("session")).unwrap();
let history = session_store::LoggedHistoryEntry {
item: session_store::LoggedItem::Message {
role: session_store::LoggedRole::Assistant,
content: vec![session_store::LoggedContentPart::Text {
text: "artificial fixture reply".into(),
}],
},
metadata: session_store::LoggedSessionHistoryMetadata {
entry_id: session_store::LoggedSessionHistoryEntryId("fixed-entry-id".into()),
origin: session_store::LoggedSessionHistoryOrigin::ModelOutput {
worker: session_store::LoggedWorkerSubject {
workspace_id: Some(WORKSPACE.into()),
runtime_id: Some(RUNTIME.into()),
worker_id: worker_id().to_string(),
},
},
derivation: None,
input_metadata: None,
},
};
sessions
.create_segment(
session_id,
segment_id,
&[
LogEntry::AnnotatedSegmentStart {
ts: 123,
session_id,
system_prompt: Some("Harmless artificial fixture instructions.".into()),
config: Default::default(),
history: vec![],
forked_from: None,
compacted_from: None,
},
LogEntry::Invoke {
ts: 124,
trigger: protocol::InvokeKind::UserSend,
},
LogEntry::AnnotatedUserInput {
ts: 124,
segments: vec![protocol::Segment::text(
"Harmless artificial fixture input.",
)],
extensions: vec![checkpoint_extension(&settled_checkpoint())],
history: vec![session_store::LoggedHistoryEntry {
item: session_store::LoggedItem::Message {
role: session_store::LoggedRole::User,
content: vec![session_store::LoggedContentPart::Text {
text: "Harmless artificial fixture input.".into(),
}],
},
metadata: session_store::LoggedSessionHistoryMetadata {
entry_id: session_store::LoggedSessionHistoryEntryId(
"fixed-user-id".into(),
),
origin: session_store::LoggedSessionHistoryOrigin::BackendInstruction {
operation_id: None,
},
derivation: None,
input_metadata: None,
},
}],
},
LogEntry::AnnotatedAssistantItem {
ts: 125,
entry: history,
},
LogEntry::LlmUsage {
ts: 125,
history_len: 2,
input_total_tokens: 1,
cache_read_tokens: 0,
cache_write_tokens: 0,
output_tokens: 1,
},
LogEntry::TurnEnd {
ts: 126,
turn_count: 1,
},
LogEntry::RunCompleted {
entry_id: Some(session_store::LoggedSessionHistoryEntryId(
"fixed-terminal-id".into(),
)),
ts: 126,
interrupted: false,
result: agen::EngineResult::Finished,
active_run_turn_count: None,
},
],
)
.unwrap();
fs::create_dir_all(worker.join("runs/old-run")).unwrap();
fs::write(worker.join("runs/old-run/private.log"), "not exported").unwrap();
drop(opened);
let template = StoppedWorkerTemplate::extract(&worker).unwrap();
(temp, worker, template)
}
fn destination(template: &StoppedWorkerTemplate) -> (tempfile::TempDir, FsRuntimeStoreOptions) {
let temp = tempfile::tempdir().unwrap();
let root = temp.path().join("runtime");
initialize(
&root,
template.runtime_id(),
template.workspace_id(),
NEW_URL,
);
(
temp,
FsRuntimeStoreOptions::new(root).with_runtime_id(template.runtime_id()),
)
}
fn assert_fresh(options: &FsRuntimeStoreOptions) {
assert_eq!(fs::read_dir(&options.root).unwrap().count(), 1);
assert!(options.root.join("auth.toml").is_file());
}
#[test]
fn extraction_install_roundtrip_preserves_bytes_and_is_inactive() {
let (_source, worker, template) = source();
let original_worker = fs::read(worker.join(WORKER_FILE)).unwrap();
let original_metadata = fs::read(worker.join(WORKER_METADATA_FILE)).unwrap();
let exported = serde_json::to_vec(&template).unwrap();
let exported_text = String::from_utf8(exported.clone()).unwrap();
for forbidden in [
"old-config",
"old-submission-operation",
"restore_guard",
"private_key",
"http://",
"/old/host",
"private.log",
"resolved_manifest_snapshot",
] {
assert!(!exported_text.contains(forbidden));
}
let template: StoppedWorkerTemplate = serde_json::from_slice(&exported).unwrap();
let (_target, options) = destination(&template);
let auth_before = fs::read(options.root.join("auth.toml")).unwrap();
template.install(&options, request(), &resolved()).unwrap();
assert_eq!(
fs::read(options.root.join("auth.toml")).unwrap(),
auth_before
);
assert_eq!(fs::read(worker.join(WORKER_FILE)).unwrap(), original_worker);
assert_eq!(
fs::read(worker.join(WORKER_METADATA_FILE)).unwrap(),
original_metadata
);
let installed = options
.root
.join(WORKERS_DIR)
.join(template.worker_id().to_string());
assert_eq!(
fs::read(installed.join("session/session.json")).unwrap(),
template.session_manifest
);
assert_eq!(
fs::read(installed.join(format!("session/segments/{}.jsonl", template.segment_id())))
.unwrap(),
template.segment_jsonl
);
assert!(!installed.join("runs").exists());
let metadata: WorkerMetadata =
decode(&fs::read(installed.join(WORKER_METADATA_FILE)).unwrap()).unwrap();
assert_eq!(metadata.active.unwrap().session_id, template.session_id());
assert!(metadata.workspace_root.is_none() && metadata.resolved_manifest_snapshot.is_some());
let opened = FsRuntimeStore::open_or_create(options.root.clone(), RUNTIME).unwrap();
let state = opened.state.unwrap();
assert!(state.diagnostics.is_empty());
let saved = state.workers.get(&worker_id()).unwrap();
assert_eq!(saved.status, WorkerStatus::Stopped);
assert!(saved.last_finished_submission_request_id.is_none());
let PersistedWorkerExecutionState::Available(execution) = &saved.execution_state else {
panic!("not available")
};
assert_eq!(execution.restore_intent, WorkerRestoreIntent::Explicit);
assert!(execution.binding.is_none());
assert_eq!(execution.request, request());
// Fresh restore guard, no operation owner/receipt history.
assert_ne!(
serde_json::to_vec(&saved.restore_guard).unwrap(),
serde_json::to_vec(
&decode::<WorkerAggregateRecord>(&original_worker)
.unwrap()
.restore_guard
)
.unwrap()
);
let guard: GuardState = decode(&serde_json::to_vec(&saved.restore_guard).unwrap()).unwrap();
assert!(guard.active_request_id.is_none() && guard.owners.is_empty());
drop(opened.store);
struct NoExecution;
impl crate::execution::WorkerExecutionBackend for NoExecution {
fn backend_id(&self) -> &str {
"fixture-no-execution"
}
fn spawn_worker(
&self,
_: crate::execution::WorkerExecutionSpawnRequest,
) -> crate::execution::WorkerExecutionSpawnResult {
panic!("fixture executed")
}
fn dispatch_input(
&self,
_: &WorkerRef,
_: crate::interaction::WorkerInput,
) -> crate::execution::WorkerExecutionResult {
panic!("fixture submitted")
}
fn restore_worker(
&self,
_: crate::execution::WorkerExecutionRestoreRequest,
) -> crate::execution::WorkerExecutionSpawnResult {
panic!("fixture auto-restored")
}
}
let runtime =
crate::Runtime::with_fs_store_and_execution_backend(options.clone(), Arc::new(NoExecution))
.unwrap();
assert_eq!(runtime.list_workers().unwrap().len(), 1);
assert_eq!(runtime.summary().unwrap().active_worker_count, 0);
assert_eq!(
runtime
.worker_detail(&WorkerRef::new(worker_id()))
.unwrap()
.status,
WorkerStatus::Stopped
);
drop(runtime);
let runtime = crate::Runtime::with_fs_store(options).unwrap();
assert_eq!(
runtime.list_workers().unwrap()[0].status,
WorkerStatus::Stopped
);
}
#[test]
fn install_rejects_schema_identity_input_and_overwrite() {
let (_source, _, template) = source();
for change in [
"template",
"runtime-schema",
"session-schema",
"session-id",
"segment-id",
"runtime-id",
"workspace-id",
"worker-id",
"origin-id",
"duplicate-id",
"truncated",
] {
let (_target, options) = destination(&template);
let mut invalid = template.clone();
match change {
"template" => invalid.template_version += 1,
"runtime-schema" => invalid.runtime_schema_version += 1,
"session-schema" => {
invalid.session_manifest = format!(
"{{\"schema_version\":999,\"session_id\":\"{}\"}}",
invalid.session_id
)
.into_bytes()
}
"session-id" => invalid.session_id = uuid::Uuid::now_v7(),
"segment-id" => invalid.segment_id = uuid::Uuid::nil(), // no external attestation: valid new Segment ID is otherwise portable
"runtime-id" => invalid.runtime_id = "other-runtime".into(),
"workspace-id" => invalid.workspace_id = "other-workspace".into(),
"worker-id" => invalid.worker_id = WorkerId::now_v7(),
"origin-id" => {
invalid.segment_jsonl = String::from_utf8(invalid.segment_jsonl)
.unwrap()
.replace(RUNTIME, "other-runtime")
.into_bytes()
}
"duplicate-id" => {
let last = invalid
.segment_jsonl
.split_inclusive(|b| *b == b'\n')
.last()
.unwrap()
.to_vec();
invalid.segment_jsonl.extend(last);
}
"truncated" => {
invalid.segment_jsonl.pop();
}
_ => unreachable!(),
}
assert!(
invalid.install(&options, request(), &resolved()).is_err(),
"accepted {change}"
);
assert_fresh(&options);
}
let (_target, options) = destination(&template);
let mut launch = request();
launch.initial_input = Some(crate::interaction::WorkerInput::user("do not execute"));
assert!(template.install(&options, launch, &resolved()).is_err());
assert_fresh(&options);
template.install(&options, request(), &resolved()).unwrap();
let worker_path = options
.root
.join(WORKERS_DIR)
.join(worker_id().to_string())
.join(WORKER_FILE);
let before = fs::read(&worker_path).unwrap();
assert!(template.install(&options, request(), &resolved()).is_err());
assert_eq!(fs::read(worker_path).unwrap(), before);
}
#[test]
fn rejects_uninitialized_untrusted_live_and_partial_destination() {
let (_source, _, template) = source();
let (_target, options) = destination(&template);
let held = acquire_runtime_store_owner_lock(&options.root).unwrap();
assert!(template.install(&options, request(), &resolved()).is_err());
assert_fresh(&options);
drop(held);
initialize(&options.root, "wrong-runtime", WORKSPACE, NEW_URL);
assert!(template.install(&options, request(), &resolved()).is_err());
initialize(&options.root, RUNTIME, WORKSPACE, "http://127.0.0.1:23456");
assert!(template.install(&options, request(), &resolved()).is_err());
fs::remove_file(options.root.join("auth.toml")).unwrap();
assert!(template.install(&options, request(), &resolved()).is_err());
initialize(&options.root, RUNTIME, WORKSPACE, NEW_URL);
fs::create_dir(options.root.join(WORKERS_DIR)).unwrap();
assert!(template.install(&options, request(), &resolved()).is_err());
}
#[test]
fn extraction_rejects_nonstopped_pending_schema_and_metadata_mismatch() {
for change in [
"running",
"pending",
"guard-pending",
"attachment",
"schema",
"worker-id",
"metadata-workspace",
"metadata-session",
"artifact",
"secret",
"url",
"path",
] {
let (_source, worker, _) = source();
let mut aggregate: WorkerAggregateRecord =
decode(&fs::read(worker.join(WORKER_FILE)).unwrap()).unwrap();
match change {
"running" => aggregate.status = WorkerStatus::Running,
"pending" => {
aggregate.execution_state = WorkerExecutionStateRecord::ReconciliationRequired(
PersistedWorkerLifecycleOperation::Restore(PersistedWorkerRestoreOperation {
operation_id: WorkerLifecycleOperationId::new(),
mode: PersistedWorkerRestoreMode::Explicit,
request: request(),
binding: None,
last_settled_status: WorkerStatus::Stopped,
}),
)
}
"guard-pending" => aggregate.restore_guard = serde_json::from_str(r#"{"observation_token":"old-token","active_request_id":"pending-restore","owners":{}}"#).unwrap(),
"attachment" => aggregate.logical_workdir_attachments.push(LogicalWorkdirAttachment {
alias: workdir::WorkdirAttachmentAlias::new("source").unwrap(),
working_directory_id: "old-workdir".into(),
capabilities: workdir::WorkdirSessionCapabilities::READ_ONLY,
}),
"schema" => aggregate.schema_version += 1,
"worker-id" => aggregate.worker_id = WorkerId::now_v7(),
"metadata-workspace" | "metadata-session" => {
let mut metadata: WorkerMetadata =
decode(&fs::read(worker.join(WORKER_METADATA_FILE)).unwrap()).unwrap();
if change == "metadata-workspace" {
metadata.workspace_id = Some("wrong-workspace".into());
} else {
metadata.active.as_mut().unwrap().session_id = uuid::Uuid::now_v7();
}
atomic_write_json(
&worker.join(WORKER_METADATA_FILE),
&metadata,
"test mismatch",
)
.unwrap();
}
"artifact" => {
fs::create_dir_all(worker.join("session/artifacts/paste")).unwrap();
fs::write(
worker.join("session/artifacts/paste/payload"),
"unsupported",
)
.unwrap();
}
"secret" | "url" | "path" => {
let segment = fs::read_dir(worker.join("session/segments"))
.unwrap()
.next()
.unwrap()
.unwrap()
.path();
let replacement = match change {
"secret" => "Bearer secret",
"url" => "https://old-host.invalid",
_ => "/old/host/path",
};
let text = fs::read_to_string(&segment)
.unwrap()
.replace("artificial fixture reply", replacement);
fs::write(segment, text).unwrap();
}
_ => unreachable!(),
}
atomic_write_json(&worker.join(WORKER_FILE), &aggregate, "test aggregate").unwrap();
assert!(
StoppedWorkerTemplate::extract(&worker).is_err(),
"accepted {change}"
);
}
let (_source, worker, _) = source();
let held =
acquire_runtime_store_owner_lock(worker.parent().unwrap().parent().unwrap()).unwrap();
assert!(StoppedWorkerTemplate::extract(&worker).is_err());
drop(held);
}
#[test]
fn fresh_manifest_and_memory_settings_are_required() {
let (_source, _, template) = source();
let (_target, options) = destination(&template);
let mut manifest = resolved();
manifest.worker.name = "wrong-worker".into();
assert!(template.install(&options, request(), &manifest).is_err());
let mut binding = request();
binding.memory_settings = None;
assert!(template.install(&options, binding, &resolved()).is_err());
let mut binding = request();
binding.memory_settings.as_mut().unwrap().workspace_id = "other-workspace".into();
assert!(template.install(&options, binding, &resolved()).is_err());
assert_fresh(&options);
}
#[test]
fn settled_checkpoints_preserve_receipts_but_refuse_pending_work() {
let mut checkpoint = settled_checkpoint();
assert!(validate_settled_activation(&checkpoint_extension(&checkpoint)).is_ok());
checkpoint.pending.push(1);
assert!(validate_settled_activation(&checkpoint_extension(&checkpoint)).is_err());
checkpoint.pending.clear();
checkpoint.activating = Some(1);
assert!(validate_settled_activation(&checkpoint_extension(&checkpoint)).is_err());
checkpoint.activating = None;
checkpoint.invocation_receipts.push(1);
assert!(validate_settled_activation(&checkpoint_extension(&checkpoint)).is_err());
checkpoint.invocation_receipts.clear();
checkpoint.invocation_recovery_blocked = true;
assert!(validate_settled_activation(&checkpoint_extension(&checkpoint)).is_err());
}
// Deliberately corrupt only test-owned synthetic source files, never the lab.
fn assert_rejected_session_bytes(worker: &Path, template: &StoppedWorkerTemplate, bytes: Vec<u8>) {
let path = worker.join(format!("session/segments/{}.jsonl", template.segment_id()));
fs::write(path, &bytes).unwrap();
let extraction_error = StoppedWorkerTemplate::extract(worker)
.err()
.expect("invalid source accepted");
assert!(
!extraction_error
.to_string()
.contains("synthetic-discarded-material")
);
let (_target, options) = destination(template);
let mut invalid = template.clone();
invalid.segment_jsonl = bytes;
let installation_error = invalid
.install(&options, request(), &resolved())
.unwrap_err();
assert!(
!installation_error
.to_string()
.contains("synthetic-discarded-material")
);
assert_fresh(&options);
}
fn synthetic_logs(template: &StoppedWorkerTemplate) -> Vec<LogEntry> {
template
.segment_jsonl
.split(|byte| *byte == b'\n')
.filter(|line| !line.is_empty())
.map(|line| serde_json::from_slice(line).unwrap())
.collect()
}
fn encode_synthetic_logs(logs: &[LogEntry]) -> Vec<u8> {
let mut bytes = Vec::new();
for log in logs {
serde_json::to_writer(&mut bytes, log).unwrap();
bytes.push(b'\n');
}
bytes
}
#[tokio::test]
async fn missing_system_prompt_matches_worker_restore_rejection() {
let (_source, worker_dir, template) = source();
let mut logs = synthetic_logs(&template);
assert!(session_store::collect_state(&logs).system_prompt.is_some());
let LogEntry::AnnotatedSegmentStart { system_prompt, .. } = &mut logs[0] else {
panic!("missing seed")
};
*system_prompt = None;
assert_rejected_session_bytes(&worker_dir, &template, encode_synthetic_logs(&logs));
// The owning Worker constructor rejects this before model-client creation,
// allocation, or execution. Do not start a controller or submit any input.
let store = session_store::CombinedStore::new(
WorkerSessionStore::open_read_only(worker_dir.join("session")).unwrap(),
session_store::WorkerAggregateStore::open_read_only(&worker_dir, template.worker_name())
.unwrap(),
);
let restored = worker::Worker::restore_from_manifest(
template.session_id(),
template.segment_id(),
resolved(),
store,
worker::PromptCatalogSource::builtins_only(),
)
.await;
assert!(matches!(
restored,
Err(worker::WorkerError::SegmentSystemPromptMissing { .. })
));
}
#[test]
fn malformed_submission_and_notification_receipts_fail_extraction_and_install() {
let (_source, worker, template) = source();
for notification in [false, true] {
let mut checkpoint = settled_checkpoint();
if notification {
checkpoint.notification_receipts =
vec![ReceiptFixture::Malformed(1), ReceiptFixture::Malformed(2)];
} else {
checkpoint.receipts = vec![ReceiptFixture::Malformed(1), ReceiptFixture::Malformed(2)];
}
let mut logs = synthetic_logs(&template);
for log in &mut logs {
if let LogEntry::AnnotatedUserInput { extensions, .. } = log {
*extensions = vec![checkpoint_extension(&checkpoint)];
}
}
assert_rejected_session_bytes(&worker, &template, encode_synthetic_logs(&logs));
}
}
#[test]
fn unknown_fields_at_every_retained_record_depth_are_rejected_without_secret_diagnostics() {
let (_source, worker, template) = source();
let original = String::from_utf8(template.segment_jsonl.clone()).unwrap();
// These values are harmless synthetic markers, not lab/private material.
// The compatibility decoder normally discards many of these fields.
let locations = [
r#""kind":"annotated_segment_start""#,
r#""kind":"invoke""#,
r#""kind":"annotated_user_input""#,
r#""kind":"annotated_assistant_item""#,
r#""kind":"llm_usage""#,
r#""kind":"turn_end""#,
r#""kind":"run_completed""#,
r#""max_tokens":null"#,
r#""kind":"message","role":"assistant""#,
r#""kind":"text","text":"artificial fixture reply""#,
r#""kind":"text","content":"Harmless artificial fixture input.""#,
r#""entry_id":"fixed-entry-id""#,
r#""kind":"model_output""#,
r#""worker_id":"01a1657d-b762-7791-87ae-a49f28cadbce""#,
r#""domain":"worker.pending_activations.v1""#,
r#""next_activation_sequence":2"#,
r#""submission_request_id":"completed-submission-request""#,
r#""notification_request_id":"completed-notification-request""#,
];
for location in locations {
assert!(original.contains(location));
for extra in [
r#", "reasoning":{"text":"synthetic-discarded-material"}"#,
r#", "encrypted_content":"synthetic-discarded-material""#,
] {
let altered = original.replacen(location, &format!("{location}{extra}"), 1);
assert_rejected_session_bytes(&worker, &template, altered.into_bytes());
}
}
let duplicate = original.replacen(
r#""role":"assistant""#,
r#""role":"assistant","role":"assistant""#,
1,
);
assert_rejected_session_bytes(&worker, &template, duplicate.into_bytes());
}
#[test]
fn opaque_feature_payloads_cannot_bypass_strict_session_validation() {
let (_source, worker, template) = source();
let mut logs = synthetic_logs(&template);
for log in &mut logs {
if let LogEntry::AnnotatedUserInput { history, .. } = log {
if let session_store::LoggedItem::Message { role, .. } = &mut history[0].item {
*role = session_store::LoggedRole::System;
}
history[0].metadata.input_metadata = Some(
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::Retain,
},
Some(
session_store::FeatureInputPayload::encode(
"fixture.test",
1,
&BTreeMap::from([("reasoning", "harmless synthetic payload")]),
)
.unwrap(),
),
None,
)
.unwrap(),
);
}
}
assert_rejected_session_bytes(&worker, &template, encode_synthetic_logs(&logs));
}
#[cfg(unix)]
#[test]
fn refuses_source_and_destination_symlinks() {
let (_source, worker, template) = source();
let (_target, options) = destination(&template);
std::os::unix::fs::symlink(worker.join(WORKER_FILE), options.root.join("linked")).unwrap();
assert!(template.install(&options, request(), &resolved()).is_err());
fs::remove_file(options.root.join("linked")).unwrap();
std::os::unix::fs::symlink(worker.join(WORKER_FILE), worker.join("runs/link")).unwrap();
assert!(StoppedWorkerTemplate::extract(&worker).is_err());
}
/// Opt-in only: source path supplied explicitly; assertions never display content.
#[test]
#[ignore = "requires the approved artificial lab aggregate via YOI_STOPPED_FIXTURE_SOURCE"]
fn approved_lab_extract_install() {
let path = PathBuf::from(
std::env::var_os("YOI_STOPPED_FIXTURE_SOURCE").expect("explicit source required"),
);
let template = StoppedWorkerTemplate::extract(&path).unwrap();
let (_target, options) = destination(&template);
let mut bind = request();
bind.worker_id = template.worker_id();
bind.workspace_api.as_mut().unwrap().workspace_id = template.workspace_id().into();
bind.memory_settings.as_mut().unwrap().workspace_id = template.workspace_id().into();
let mut manifest = resolved();
manifest.worker.name = template.worker_name();
template.install(&options, bind, &manifest).unwrap();
let installed = options
.root
.join(WORKERS_DIR)
.join(template.worker_id().to_string());
for relative in [
"session/session.json".to_string(),
format!("session/segments/{}.jsonl", template.segment_id()),
] {
assert!(
fs::read(path.join(&relative)).unwrap() == fs::read(installed.join(relative)).unwrap()
);
}
let runtime = crate::Runtime::with_fs_store(options).unwrap();
assert_eq!(runtime.list_workers().unwrap().len(), 1);
assert_eq!(runtime.summary().unwrap().active_worker_count, 0);
}
@@ -1389,6 +1389,15 @@ fn bind_workspace_memory_settings(
Ok(())
}
/// Read-only validation shared with the offline, feature-gated fixture installer.
#[cfg(feature = "test-support")]
pub(crate) fn validate_fixture_restore_metadata(
metadata: &session_store::WorkerMetadata,
request: &CreateWorkerRequest,
) -> Result<(), String> {
ProfileRuntimeWorkerFactory::manifest_for_restore(metadata, request).map(|_| ())
}
fn validate_worker_memory_settings(
manifest: &manifest::WorkerManifest,
request: &CreateWorkerRequest,
+4 -3
View File
@@ -71,9 +71,10 @@ pub use session_history::{
};
pub use shared_state::WorkerSharedState;
pub use worker::{
LocalWorkingDirectory, SubjektivSessionAttributionLifecycle, Worker, WorkerError,
WorkerFilesystemAuthority, WorkerRunResult, WorkerWorkspaceContext, WorkspaceBinaryRequest,
WorkspaceBinaryResponse, WorkspaceClient, WorkspaceClientError, WorkspaceId, WorkspaceIdError,
LocalWorkingDirectory, ReviewerChildWorkspaceClient, ReviewerContext,
SubjektivSessionAttributionLifecycle, Worker, WorkerError, WorkerFilesystemAuthority,
WorkerRunResult, WorkerWorkspaceContext, WorkspaceBinaryRequest, WorkspaceBinaryResponse,
WorkspaceClient, WorkspaceClientError, WorkspaceId, WorkspaceIdError,
WorkspacePromptCatalogResolution, WorkspaceRequest, WorkspaceRequestMethod, WorkspaceResponse,
WorkspaceServerOperation, WorkspaceWorkerDiscoveryRequest, apply_worker_manifest,
marker_workspace_client, unavailable_workspace_client,
+65 -9
View File
@@ -1332,15 +1332,28 @@ impl WorkspaceClient for ReviewerChildWorkspaceClient {
mut request: WorkspaceRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
let workspace_id = self.workspace_id().unwrap_or_default();
let ticket_id = &self.context.ticket_id;
let ticket_query_path = format!("/api/w/{workspace_id}/tickets/query");
let ticket_show_path = format!("/api/w/{workspace_id}/tickets/{ticket_id}/show");
let ticket_show_prefix = format!("/api/w/{workspace_id}/tickets/");
// ShowTicket uses POST for bounded read parameters. Like Ticket query
// and GET reads, it must allow related Tickets through the bound Host's
// read authority. Only review submission is restricted to the attested MR.
// Match one literal reference segment, not arbitrary POST routes.
let is_ticket_show = request
.path
.strip_prefix(&ticket_show_prefix)
.and_then(|path| path.strip_suffix("/show"))
.is_some_and(|reference| {
!reference.is_empty()
&& reference
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
});
let merge_request_id = &self.context.merge_request_id;
let review_path =
format!("/api/w/{workspace_id}/merge-requests/{merge_request_id}/reviews");
let read_allowed = request.method == WorkspaceRequestMethod::Get
|| (request.method == WorkspaceRequestMethod::Post
&& (request.path == ticket_query_path || request.path == ticket_show_path));
&& (request.path == ticket_query_path || is_ticket_show));
if request.method == WorkspaceRequestMethod::Post && request.path == review_path {
let body = request.body.take().ok_or_else(|| {
WorkspaceClientError::Request("review submission requires a JSON body".to_string())
@@ -1868,16 +1881,11 @@ mod reviewer_client_tests {
}
#[test]
fn reviewer_child_client_rejects_other_ticket_and_mutation_posts() {
fn reviewer_child_client_rejects_mutation_posts() {
let inner: Arc<dyn WorkspaceClient> = Arc::new(RecordingWorkspaceClient::default());
let client = reviewer_client(inner);
for request in [
WorkspaceRequest::json(
WorkspaceRequestMethod::Post,
"/api/w/ws/tickets/T2/show",
"{}".to_string(),
),
WorkspaceRequest::json(
WorkspaceRequestMethod::Post,
"/api/w/ws/tickets/T1/comments",
@@ -1894,6 +1902,54 @@ mod reviewer_client_tests {
}
}
#[test]
fn reviewer_ticket_reads_reject_route_injection_and_mutations_without_transport() {
let inner = Arc::new(RecordingWorkspaceClient::default());
let client = reviewer_client(inner.clone());
for path in [
"/api/w/other/tickets/T-1/show",
"/api/w/ws/tickets//show",
"/api/w/ws/tickets/T-1/comments",
"/api/w/ws/tickets/T-1/posts",
"/api/w/ws/tickets/T-1/show/",
"/api/w/ws/tickets/T-1/show?query=1",
"/api/w/ws/tickets/T-1?query=1/show",
"/api/w/ws/tickets/T-1#fragment/show",
"/api/w/ws/tickets/../show",
"/api/w/ws/tickets/T-1/../T-2/show",
"/api/w/ws/tickets/T%2D1/show",
"/api/w/ws/tickets/T-1%2fcomments/show",
"/api/w/ws/merge-requests/MR2/reviews",
] {
assert!(
client
.execute(WorkspaceRequest::json(
WorkspaceRequestMethod::Post,
path,
"{}",
))
.is_err(),
"{path}"
);
}
for method in [
WorkspaceRequestMethod::Put,
WorkspaceRequestMethod::Patch,
WorkspaceRequestMethod::Delete,
] {
assert!(
client
.execute(WorkspaceRequest::json(
method,
"/api/w/ws/tickets/T-1/show",
"{}"
))
.is_err()
);
}
assert!(inner.requests.lock().unwrap().is_empty());
}
#[test]
fn reviewer_child_client_injects_capability_only_for_attested_review() {
let inner = Arc::new(RecordingWorkspaceClient::default());
+4
View File
@@ -10,6 +10,10 @@ autobins = false
name = "yoi-server"
path = "src/main.rs"
[features]
# Offline fixture preparation only; build product binaries without this feature.
test-support = []
[dependencies]
api-macros = { workspace = true, features = ["axum"] }
async-trait.workspace = true
+8 -1
View File
@@ -8866,7 +8866,14 @@ mod tests {
})
.await
.expect_err("closed session must reject local operation without another request");
assert!(matches!(error, WorkdirError::Unavailable(_)));
assert!(matches!(
error.classification_source(),
WorkdirError::Unavailable(_)
));
assert_eq!(
error.failure_reason(),
Some(workdir::WorkdirFailureReason::SessionClosed)
);
server.join().expect("mock remote server finished");
}
+2
View File
@@ -25,6 +25,8 @@ pub mod runtime_subscription;
pub mod server;
pub mod skills;
pub mod store;
#[cfg(feature = "test-support")]
pub use store::fixture_support as test_support;
pub mod subjektiv;
#[cfg(test)]
mod ticket_browse_performance_tests;
+6 -2
View File
@@ -37791,9 +37791,13 @@ mod tests {
)))
.unwrap();
}
let error = write
.await
.unwrap()
.expect_err("mismatched or lost mutation response must remain outcome-unknown");
assert!(matches!(
write.await.unwrap(),
Err(workdir::WorkdirError::OutcomeUnknown(_))
error.classification_source(),
workdir::WorkdirError::OutcomeUnknown(_)
));
}
}
@@ -120,6 +120,14 @@ struct TicketSurface {
}
impl TicketSurface {
async fn new(root: &Path, client: Arc<TicketRouterClient>, wip: bool) -> Self {
Self::with_access(root, client, wip, TicketFeatureAccess::workflow()).await
}
async fn with_access(
root: &Path,
client: Arc<dyn WorkspaceClient>,
wip: bool,
access: TicketFeatureAccess,
) -> Self {
let manifest = worker::WorkerManifest::from_toml("[worker]\nname=\"ticket-tools\"\n[model]\nscheme=\"anthropic\"\nmodel_id=\"unused\"\n[engine]\n[scope]\nallow=[]\n").unwrap();
let engine =
agen::Engine::<_, agen::state::Mutable, worker::SessionHistoryMetadata>::new_annotated(
@@ -138,22 +146,18 @@ impl TicketSurface {
)
.await
.unwrap();
let report =
host.install_features(worker::feature::FeatureRegistryBuilder::new().with_module(
TicketFeature::new(client.clone(), TicketFeatureAccess::workflow()),
));
let report = host.install_features(
worker::feature::FeatureRegistryBuilder::new()
.with_module(TicketFeature::new(client.clone(), access)),
);
assert!(!report.has_errors());
let handle = host.engine().tool_server_handle();
handle.flush_pending();
let tools = [
"TicketAssignmentTrackingQuery",
"TicketAssignmentAnswer",
"TicketImplementationResult",
"TicketImplementationResultQuery",
]
.into_iter()
.map(|name| (name.into(), handle.get_tool(name).unwrap().1))
.collect();
let tools = access
.tool_names()
.into_iter()
.map(|name| (name.into(), handle.get_tool(name).unwrap().1))
.collect();
if !wip {
return Self { tools, wip };
}
@@ -162,7 +166,7 @@ impl TicketSurface {
worker::feature::builtin::ticket::mount_workspace_http_ticket_wip(
&mut mounts,
client,
TicketFeatureAccess::workflow(),
access,
None,
&route,
)
@@ -189,8 +193,9 @@ impl TicketSurface {
if !self.wip {
return execute_tool(self.tools[name].clone(), input, context).await;
}
let path = format!("/tickets/{}", input["ticket"].as_str().unwrap());
input.as_object_mut().unwrap().remove("ticket");
let reference_field = if name == "ShowTicket" { "id" } else { "ticket" };
let path = format!("/tickets/{}", input[reference_field].as_str().unwrap());
input.as_object_mut().unwrap().remove(reference_field);
// Observe the real item representation before invoking a mutation. Tracking
// reads alone do not establish the Ticket WIP object validator. Explicit
// refresh also reconciles stale observations after an unknown result;
@@ -201,6 +206,9 @@ impl TicketSurface {
ToolExecutionContext::direct(),
)
.await?;
if name == "ShowTicket" {
return execute_tool(self.tools["Invoke"].clone(), serde_json::json!({"path":path,"interface":inspected["interfaces"][0]["reference"],"operation":"read","arguments":input}), context).await;
}
execute_tool(self.tools["Invoke"].clone(), serde_json::json!({"path":path,"interface":inspected["interfaces"][0]["reference"],"operation":"read","arguments":{}}), ToolExecutionContext::direct()).await?;
let inspected = execute_tool(
self.tools["Inspect"].clone(),
@@ -364,6 +372,210 @@ async fn ticket_public_roundtrip_observed_answer_replays_without_changing_waitin
}
}
// Reviewer policy, tool projection, reference resolution, authentication and
// persistence remain production code. Only the synchronous HTTP transport is
// replaced by signed in-process router dispatch. Child spawning and MR attestation
// have their own suites; this fixture does not claim to exercise those boundaries.
fn reviewer_client(inner: Arc<TicketRouterClient>, bound_ticket: &str) -> Arc<dyn WorkspaceClient> {
Arc::new(worker::ReviewerChildWorkspaceClient::new(
inner,
worker::ReviewerContext {
ticket_id: bound_ticket.into(),
merge_request_id: "unsubmitted-review-candidate".into(),
},
"unused-review-capability".into(),
))
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn reviewer_reads_saved_related_tickets_by_key_and_id_and_pages_their_threads() {
for wip in [false, true] {
let dir = tempfile::tempdir().unwrap();
let (api, _runtime, identity, source, bound_id) = tracking_fixture(dir.path()).await;
let backend = browser_ticket_backend(&api).unwrap();
let mut related = ticket::NewTicket::new("Related requirements");
related.body = ticket::MarkdownText::new("Requirements from a different Ticket");
let related = backend.create(related).unwrap();
backend
.add_ticket_relation(
bound_id.clone().into(),
ticket::NewTicketRelation {
kind: ticket::TicketRelationKind::DependsOn,
target: related.id.clone(),
note: None,
author: None,
},
)
.unwrap();
for body in ["Earlier decision", "Latest decision"] {
backend
.add_event(
related.id.clone().into(),
ticket::NewTicketEvent::new(ticket::TicketEventKind::Comment, body),
)
.unwrap();
}
let bound = backend.show(bound_id.clone().into()).unwrap();
let related_before = backend.show(related.id.clone().into()).unwrap();
let bound_key = bound.meta.resource_key.as_deref().unwrap();
let related_key = related.resource_key.as_deref().unwrap();
let transport = ticket_client(&api, identity, &source);
// One client must retain its review binding while following related Tickets.
let client = reviewer_client(transport.clone(), &bound_id);
let surface =
TicketSurface::with_access(dir.path(), client, wip, TicketFeatureAccess::review())
.await;
for (reference, expected_key, expected_title, expected_body) in [
(
bound_key,
bound_key,
"Tracking authority",
bound.document.body.as_str(),
),
(
related_key,
related_key,
"Related requirements",
related_before.document.body.as_str(),
),
(
related.id.as_str(),
related_key,
"Related requirements",
related_before.document.body.as_str(),
),
(
bound_id.as_str(),
bound_key,
"Tracking authority",
bound.document.body.as_str(),
),
] {
let shown = surface
.call(
"ShowTicket",
serde_json::json!({"id":reference}),
ToolExecutionContext::direct(),
)
.await
.unwrap();
assert_eq!(shown["ticket"], expected_key);
assert_eq!(shown["title"], expected_title);
assert_eq!(shown["body"], expected_body);
if expected_key == bound_key {
assert!(
shown["relations"]["outgoing"]
.as_array()
.unwrap()
.iter()
.any(|relation| relation["ticket"] == related_key
&& relation["kind"] == "depends_on")
);
}
}
let first = surface
.call(
"ShowTicket",
serde_json::json!({"id":related_key,"event_limit":1}),
ToolExecutionContext::direct(),
)
.await
.unwrap();
assert_eq!(first["thread"].as_array().unwrap().len(), 1);
assert_eq!(first["thread"][0]["body"], "Latest decision");
assert_eq!(first["event_page"]["has_more"], true);
let second = surface.call("ShowTicket", serde_json::json!({"id":related_key,"event_limit":1,"event_cursor":first["event_page"]["next_cursor"]}), ToolExecutionContext::direct()).await.unwrap();
assert_eq!(second["thread"].as_array().unwrap().len(), 1);
assert_eq!(second["thread"][0]["body"], "Earlier decision");
assert_eq!(backend.show(bound_id.into()).unwrap(), bound);
assert_eq!(backend.show(related.id.into()).unwrap(), related_before);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn reviewer_read_uses_live_backend_membership_and_cannot_mutate_saved_ticket() {
for wip in [false, true] {
let dir = tempfile::tempdir().unwrap();
let (api, _runtime, identity, source, ticket) = tracking_fixture(dir.path()).await;
let backend = browser_ticket_backend(&api).unwrap();
let before = backend.show(ticket.clone().into()).unwrap();
let transport = ticket_client(&api, identity, &source);
let client = reviewer_client(transport.clone(), &ticket);
let surface = TicketSurface::with_access(
dir.path(),
client.clone(),
wip,
TicketFeatureAccess::review(),
)
.await;
surface
.call(
"ShowTicket",
serde_json::json!({"id":ticket}),
ToolExecutionContext::direct(),
)
.await
.unwrap();
let request_count = transport.requests.lock().unwrap().len();
for (method, path, body) in [
(
WorkspaceRequestMethod::Post,
format!("/api/w/{TEST_WORKSPACE_ID}/tickets/{ticket}/comments"),
serde_json::json!({"body":"unauthorized comment"}),
),
(
WorkspaceRequestMethod::Patch,
format!("/api/w/{TEST_WORKSPACE_ID}/tickets/{ticket}"),
serde_json::json!({"title":"unauthorized title"}),
),
(
WorkspaceRequestMethod::Post,
format!("/api/w/another-workspace/tickets/{ticket}/show"),
serde_json::json!({}),
),
(
WorkspaceRequestMethod::Post,
format!("/api/w/{TEST_WORKSPACE_ID}/merge-requests/other-candidate/reviews"),
serde_json::json!({"verdict":"approved"}),
),
] {
let client = client.clone();
let error = tokio::task::spawn_blocking(move || {
client.execute(WorkspaceRequest::json(method, path, body.to_string()))
})
.await
.unwrap()
.unwrap_err();
assert!(error.to_string().contains("read-only"), "{error}");
}
assert_eq!(transport.requests.lock().unwrap().len(), request_count);
assert_eq!(backend.show(ticket.clone().into()).unwrap(), before);
// Keep a valid Runtime signature but select an unregistered Worker.
// This must fail at the real Backend, not the Reviewer route filter.
*transport.source.lock().unwrap() =
RuntimeWorkerRef::new(source.runtime_id, uuid::Uuid::now_v7().to_string());
let error = surface
.call(
"ShowTicket",
serde_json::json!({"id":ticket}),
ToolExecutionContext::direct(),
)
.await
.unwrap_err();
assert!(error.to_string().contains("401"), "{error}");
assert!(
transport
.requests
.lock()
.unwrap()
.iter()
.skip(request_count)
.any(|(_, status, _)| *status == 401)
);
assert_eq!(backend.show(ticket.into()).unwrap(), before);
}
}
fn report_counts(api: &WorkspaceApi) -> (i64, i64) {
let conn = rusqlite::Connection::open(&api.config.database_path).unwrap();
(
+38 -4
View File
@@ -2589,6 +2589,10 @@ pub struct SqliteWorkspaceStore {
db_executor: crate::db_executor::DbExecutor,
}
#[cfg(feature = "test-support")]
#[path = "test_support.rs"]
pub mod fixture_support;
impl SqliteWorkspaceStore {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let conn = Connection::open(path)?;
@@ -2900,6 +2904,33 @@ impl SqliteWorkspaceStore {
request_fingerprint: &str,
singleton_key: Option<&str>,
current_memory_settings: &WorkspaceMemorySettingsRecord,
) -> Result<WorkerCreateReservation> {
self.reserve_worker_create_with(
workspace_id,
runtime_id,
allocation_key,
request_fingerprint,
singleton_key,
current_memory_settings,
|_| Ok(None),
|_, _| Ok(()),
)
}
// Keep reservation validation, Memory binding and writes in one transaction. The
// nondefault test-support module supplies fixed identity / offline catalog hooks;
// ordinary creation always supplies a newly generated ID and no completion hook.
#[allow(clippy::too_many_arguments)]
fn reserve_worker_create_with(
&self,
workspace_id: &str,
runtime_id: &str,
allocation_key: &str,
request_fingerprint: &str,
singleton_key: Option<&str>,
current_memory_settings: &WorkspaceMemorySettingsRecord,
prepare: impl FnOnce(&rusqlite::Transaction<'_>) -> Result<Option<WorkerId>>,
complete: impl FnOnce(&rusqlite::Transaction<'_>, &WorkerCreateReservation) -> Result<()>,
) -> Result<WorkerCreateReservation> {
if allocation_key.trim().is_empty() || request_fingerprint.trim().is_empty() {
return Err(Error::InvalidInput(
@@ -2912,6 +2943,7 @@ impl SqliteWorkspaceStore {
validate_workspace_memory_settings_record(current_memory_settings, workspace_id)?;
self.with_conn_mut(|conn| {
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
let fixed_worker_id = prepare(&tx)?;
let existing = tx
.query_row(
"SELECT worker_id, runtime_id, request_fingerprint, create_fingerprint, \
@@ -3094,7 +3126,7 @@ impl SqliteWorkspaceStore {
validate_workspace_memory_settings_snapshot(&snapshot, workspace_id)?;
let create_fingerprint =
bound_worker_create_fingerprint(request_fingerprint, &snapshot);
let worker_id = WorkerId::now_v7();
let worker_id = fixed_worker_id.unwrap_or_else(WorkerId::now_v7);
let worker_id_text = worker_id.to_string();
let now = chrono::Utc::now().to_rfc3339();
let singleton_generation = match singleton_owner.as_ref() {
@@ -3183,13 +3215,15 @@ impl SqliteWorkspaceStore {
));
}
}
tx.commit()?;
Ok(WorkerCreateReservation {
let reservation = WorkerCreateReservation {
worker_id,
create_fingerprint,
memory_settings: snapshot,
singleton,
})
};
complete(&tx, &reservation)?;
tx.commit()?;
Ok(reservation)
})
}
+688
View File
@@ -0,0 +1,688 @@
//! Offline Backend fixture preparation, available only with `test-support`.
//!
//! Use a new isolated database and the existing account, Workspace signing-identity /
//! `ControlPlaneStore::create_workspace_bootstrap`, and Runtime binding APIs first.
//! Generate fresh account credentials and signing keys for each environment; no fixture
//! credentials are accepted here. Install Profile configuration through the existing
//! config-source mechanisms before starting the product processes.
//!
//! This module seeds Backend ownership only. It neither writes Runtime storage nor
//! launches a Worker, fabricates Runtime observations, changes authorization, or sends
//! initial input. The caller must separately install the matching stopped Runtime
//! fixture. Build product binaries in a separate Cargo invocation **without** this
//! feature (Cargo features unify within an invocation).
use super::*;
use crate::hosts::{WorkerSpawnIntent, WorkerSpawnRequest, worker_spawn_create_fingerprint};
use worker_runtime::catalog::ProfileSelector;
/// One ordinary, attachment-free Worker from an externally validated fixture.
///
/// IDs must match the Runtime fixture. `request` is the unresolved canonical create
/// intent, not a Runtime launch request containing credentials or execution grants.
/// The Profile selector must already be installed by the harness. `initial_submit`
/// must be empty: saved Session contents belong to the Runtime fixture, not new input.
/// Singleton/service, Ticket-assigned and attachment-bearing Workers are deliberately
/// unsupported.
pub struct FixedWorkerSeed<'a> {
pub workspace_id: &'a str,
pub runtime_id: &'a str,
pub worker_id: WorkerId,
pub allocation_key: &'a str,
pub request: &'a WorkerSpawnRequest,
}
/// Insert a fixed Worker catalog entry and completed creation reservation atomically.
///
/// Uses the normal reservation validators, authoritative Memory snapshot, fingerprint
/// binding, resource-key allocator and immediate transaction. Even an exact repeat is
/// rejected: this is not an upsert/import/recovery API. Existing allocations, Worker
/// identities and residual lifecycle/attachment records are never adopted or repaired.
/// Returns the creation binding needed to check the corresponding Runtime fixture.
pub fn seed_fixed_worker(
store: &SqliteWorkspaceStore,
seed: FixedWorkerSeed<'_>,
) -> Result<WorkerCreateReservation> {
validate_identifier("workspace_id", seed.workspace_id)?;
validate_identifier("runtime_id", seed.runtime_id)?;
let request = seed.request;
if !request.initial_submit.is_empty() {
return Err(Error::InvalidInput(
"fixture Worker cannot contain initial_submit; offline seeding accepts no new input"
.into(),
));
}
if !matches!(request.intent, WorkerSpawnIntent::WorkspaceCoding)
|| request.singleton_key.is_some()
|| request.ticket_assignment.is_some()
|| request.resolved_subjektiv_attached
|| !request.workdir_attachment_requests.is_empty()
|| !request.resolved_workdir_attachment_requests.is_empty()
|| !request.resolved_workdir_attachments.is_empty()
|| request.resolved_config_bundle.is_some()
|| request.resolved_workspace_api.is_some()
|| request.resolved_memory_settings.is_some()
|| request.resolved_worker_observation_enabled
|| !request.resolved_worker_observation_grants.is_empty()
|| request.resolved_control_operation.is_some()
|| request.resolved_execution_grant.is_some()
{
return Err(Error::InvalidInput(
"fixture Worker must be ordinary unresolved create intent without singleton, assignments, attachments or grants".into(),
));
}
// Builtin selectors already carry their canonical `builtin:` prefix.
let profile = match &request.profile {
ProfileSelector::Builtin(name) | ProfileSelector::Named(name) => name,
};
validate_non_empty("fixture Profile selector", profile)?;
let worker_id = seed.worker_id.to_string();
let display_name = request
.requested_worker_name
.as_deref()
.unwrap_or(&worker_id);
validate_non_empty("fixture Worker display name", display_name)?;
let fingerprint = worker_spawn_create_fingerprint(request).map_err(Error::InvalidInput)?;
let memory = store.get_workspace_memory_settings(seed.workspace_id)?;
store.reserve_worker_create_with(
seed.workspace_id,
seed.runtime_id,
seed.allocation_key,
&fingerprint,
None,
&memory,
|tx| {
require_seed_target(tx, &seed)?;
Ok(Some(seed.worker_id))
},
|tx, reservation| {
let now = chrono::Utc::now().to_rfc3339();
tx.execute(
"INSERT INTO worker_registry (
workspace_id, runtime_id, worker_id, display_name, profile,
retention_state, created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, 'normal', ?6, ?6)",
params![seed.workspace_id, seed.runtime_id, worker_id, display_name, profile, now],
)?;
let changed = tx.execute(
"UPDATE worker_create_reservations SET state = 'created', updated_at = ?4
WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3 AND state = 'reserved'",
params![seed.workspace_id, seed.runtime_id, reservation.worker_id.to_string(), now],
)?;
if changed != 1 {
return Err(Error::Store("fixture creation reservation disappeared".into()));
}
Ok(())
},
)
}
fn require_seed_target(tx: &rusqlite::Transaction<'_>, seed: &FixedWorkerSeed<'_>) -> Result<()> {
let active: bool = tx.query_row(
"SELECT EXISTS(SELECT 1 FROM workspaces w
JOIN workspace_runtime_bindings r ON r.workspace_id = w.workspace_id
JOIN workspace_signing_identities s ON s.workspace_id = w.workspace_id
WHERE w.workspace_id = ?1 AND w.state = 'active'
AND r.runtime_id = ?2 AND r.revoked_at IS NULL
AND r.authentication_mode = 'workspace_identity' AND s.state = 'active')",
params![seed.workspace_id, seed.runtime_id],
|row| row.get(0),
)?;
if !active {
return Err(Error::InvalidInput(
"fixture requires an active bootstrapped Workspace and authenticated Runtime binding"
.into(),
));
}
let worker_id = seed.worker_id.to_string();
let allocation_exists: bool = tx.query_row(
"SELECT EXISTS(SELECT 1 FROM worker_create_reservations
WHERE workspace_id = ?1 AND allocation_key = ?2)",
params![seed.workspace_id, seed.allocation_key],
|row| row.get(0),
)?;
if allocation_exists {
return Err(Error::InvalidInput(
"fixture Worker allocation already exists".into(),
));
}
// Check by WorkerId, not just the proposed Runtime tuple: orphaned and terminal
// ownership is still a collision. These tables are owned by this store schema.
for table in [
"worker_create_reservations",
"worker_registry",
"worker_registry_observations",
"worker_registry_projection_removals",
"worker_removal_operations",
"worker_singleton_owners",
"worker_workdir_links",
"worker_restore_intents",
"worker_tombstones",
"worker_orphan_diagnostics",
"worker_registry_projection_diagnostics",
"worker_session_archives",
"worker_diagnostics_archives",
"ticket_assignment_worker_tombstones",
] {
let exists: bool = tx.query_row(
&format!("SELECT EXISTS(SELECT 1 FROM {table} WHERE worker_id = ?1)"),
[&worker_id],
|row| row.get(0),
)?;
if exists {
return Err(Error::InvalidInput(format!(
"fixture Worker identity already exists in {table}"
)));
}
}
// A resource key without a catalog/reservation must not be silently reused.
let mut statement = tx.prepare(
"SELECT resource_id FROM workspace_resource_keys WHERE resource_kind = 'worker'",
)?;
let resources = statement.query_map([], |row| row.get::<_, String>(0))?;
for resource in resources {
let resource = resource?;
let existing = resource
.is_ascii()
.then(|| parse_worker_resource_id(&resource))
.flatten()
.ok_or_else(|| {
Error::InvalidInput("unknown fixture Worker resource identity collision".into())
})?;
if existing.worker_id == worker_id {
return Err(Error::InvalidInput(
"fixture Worker resource identity already exists".into(),
));
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hosts::{
WorkerSpawnAcceptanceRequirement, WorkerSpawnWorkingDirectoryAttachmentRequest,
WorkerSpawnWorkingDirectoryRequest,
};
use crate::workspace_signing_identity::{
InMemoryWorkspaceSigningMaterialStore, WorkspaceSigningIdentityService,
};
const WORKSPACE: &str = "0197a949-4b6b-7f2a-9d9a-1f87e3a4c501";
const RUNTIME: &str = "fixture-runtime";
const WORKER: &str = "0197a949-4b6b-7f2a-9d9a-1f87e3a4c502";
// Exercises the same public bootstrap APIs the offline harness uses. All account
// and key identities are newly generated, even across stores with fixed fixture IDs.
fn bootstrap() -> SqliteWorkspaceStore {
let store = SqliteWorkspaceStore::in_memory_for_test().unwrap();
let account = AccountRecord {
account_id: format!("fixture-account-{}", Uuid::now_v7()),
kind: "user".into(),
handle: format!("fixture-{}", Uuid::now_v7()),
display_name: "Fixture owner".into(),
created_at: "2026-10-24T00:00:00Z".into(),
updated_at: "2026-10-24T00:00:00Z".into(),
};
store.upsert_account(&account).unwrap();
let identities = WorkspaceSigningIdentityService::new(
Arc::new(store.clone()),
Arc::new(InMemoryWorkspaceSigningMaterialStore::default()),
);
let (identity, operation) = identities
.prepare_workspace_creation(
"fixture-bootstrap",
"fixture-bootstrap",
WORKSPACE,
&account.account_id,
)
.unwrap();
store
.create_workspace_bootstrap(
&WorkspaceBootstrapRecord {
operation_key: "fixture-bootstrap".into(),
request_fingerprint: "fixture-bootstrap".into(),
workspace: WorkspaceRecord {
workspace_id: WORKSPACE.into(),
owner_account_id: account.account_id,
display_name: "Fixture".into(),
state: "active".into(),
created_at: account.created_at,
updated_at: account.updated_at,
},
repository: None,
},
&identity,
&operation,
)
.unwrap();
let runtime_key = worker_runtime::auth::RuntimeIdentityMaterial::generate(RUNTIME).unwrap();
store
.upsert_workspace_runtime_binding(
WorkspaceRuntimeBinding {
workspace_id: WORKSPACE.into(),
runtime_id: RUNTIME.into(),
display_name: "Fixture Runtime".into(),
base_url: "http://127.0.0.1:12345".into(),
public_key: runtime_key.public_key,
public_key_fingerprint: String::new(),
binding_id: String::new(),
authentication_mode: WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity,
created_at: "2026-10-24T00:00:00Z".into(),
updated_at: "2026-10-24T00:00:00Z".into(),
revoked_at: None,
},
false,
)
.unwrap();
store
}
fn request() -> WorkerSpawnRequest {
WorkerSpawnRequest {
intent: WorkerSpawnIntent::WorkspaceCoding,
requested_worker_name: Some("Fixture Worker".into()),
singleton_key: None,
acceptance: WorkerSpawnAcceptanceRequirement::SocketReady,
profile: ProfileSelector::Builtin("builtin:coder".into()),
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachment_requests: Vec::new(),
resolved_workdir_attachment_requests: Vec::new(),
resolved_workdir_attachments: Vec::new(),
resolved_config_bundle: None,
resolved_workspace_api: None,
resolved_memory_settings: None,
resolved_subjektiv_attached: false,
resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_execution_grant: None,
}
}
fn seed(request: &WorkerSpawnRequest) -> FixedWorkerSeed<'_> {
FixedWorkerSeed {
workspace_id: WORKSPACE,
runtime_id: RUNTIME,
worker_id: WORKER.parse().unwrap(),
allocation_key: "fixture-allocation",
request,
}
}
fn worker() -> RuntimeWorkerRef {
RuntimeWorkerRef::new(RUNTIME, WORKER)
}
#[test]
fn fixed_ids_are_isolated_and_seed_has_no_runtime_observation() {
let a = bootstrap();
let b = bootstrap();
let request = request();
let first = seed_fixed_worker(&a, seed(&request)).unwrap();
let second = seed_fixed_worker(&b, seed(&request)).unwrap();
assert_eq!(first, second);
assert_eq!(first.worker_id.to_string(), WORKER);
assert_eq!(first.memory_settings.workspace_id, WORKSPACE);
assert_eq!(first.memory_settings.language, "English");
assert!(first.singleton.is_none());
assert_ne!(
a.get_workspace_signing_identity(WORKSPACE)
.unwrap()
.unwrap()
.public_key,
b.get_workspace_signing_identity(WORKSPACE)
.unwrap()
.unwrap()
.public_key
);
assert_ne!(
a.get_workspace_runtime_binding(WORKSPACE, RUNTIME)
.unwrap()
.unwrap()
.public_key,
b.get_workspace_runtime_binding(WORKSPACE, RUNTIME)
.unwrap()
.unwrap()
.public_key
);
for store in [&a, &b] {
assert_eq!(
store
.worker_resource_key(WORKSPACE, &worker())
.unwrap()
.as_deref(),
Some("W-1")
);
assert_eq!(
store
.resolve_worker_resource_reference(WORKSPACE, "W-1")
.unwrap(),
Some(worker())
);
assert!(
!store
.has_active_worker_create_reservation(WORKSPACE, &worker())
.unwrap()
);
let registry = store
.get_worker_registry(WORKSPACE, &worker())
.unwrap()
.unwrap();
assert_eq!(registry.profile.as_deref(), Some("builtin:coder"));
assert_eq!(registry.display_name, "Fixture Worker");
assert_eq!(registry.retention_state, "normal");
let projection = store
.with_conn(|conn| read_worker_registry_projection(conn, WORKSPACE, &worker()))
.unwrap()
.unwrap();
assert!(projection.observation.is_none());
assert!(projection.job.is_none());
}
}
#[test]
fn initial_submit_is_refused_without_registration_or_allocation() {
let store = bootstrap();
let mut request = request();
// Even an empty text segment is new input; the vector itself must be empty.
for text in ["new input must not be seeded", ""] {
request.initial_submit = vec![protocol::Segment::text(text)];
let error = seed_fixed_worker(&store, seed(&request)).unwrap_err();
assert!(
matches!(error, Error::InvalidInput(ref message) if message.contains("initial_submit"))
);
assert!(
store
.get_worker_registry(WORKSPACE, &worker())
.unwrap()
.is_none()
);
assert!(
store
.worker_resource_key(WORKSPACE, &worker())
.unwrap()
.is_none()
);
assert!(
!store
.has_active_worker_create_reservation(WORKSPACE, &worker())
.unwrap()
);
}
// The rejected attempts must not consume the allocation or resource counter.
request.initial_submit.clear();
seed_fixed_worker(&store, seed(&request)).unwrap();
assert_eq!(
store
.worker_resource_key(WORKSPACE, &worker())
.unwrap()
.as_deref(),
Some("W-1")
);
}
#[test]
fn exact_replay_changed_input_and_cross_runtime_identity_are_refused() {
let store = bootstrap();
let mut request = request();
seed_fixed_worker(&store, seed(&request)).unwrap();
let before = store.get_worker_registry(WORKSPACE, &worker()).unwrap();
assert!(seed_fixed_worker(&store, seed(&request)).is_err());
request.requested_worker_name = Some("Must not overwrite".into());
assert!(seed_fixed_worker(&store, seed(&request)).is_err());
let mut new_allocation = seed(&request);
new_allocation.allocation_key = "another-allocation";
assert!(seed_fixed_worker(&store, new_allocation).is_err());
let mut same_allocation = seed(&request);
same_allocation.worker_id = WorkerId::now_v7();
assert!(seed_fixed_worker(&store, same_allocation).is_err());
assert_eq!(
store.get_worker_registry(WORKSPACE, &worker()).unwrap(),
before
);
let other = bootstrap();
let mut record = before.unwrap();
record.worker.runtime_id = "different-runtime".into();
other.upsert_worker_registry(&record).unwrap();
assert!(seed_fixed_worker(&other, seed(&request)).is_err());
assert!(
other
.get_worker_registry(WORKSPACE, &worker())
.unwrap()
.is_none()
);
}
#[test]
fn reserved_and_orphaned_resource_identities_are_not_adopted() {
let store = bootstrap();
let request = request();
let memory = store.get_workspace_memory_settings(WORKSPACE).unwrap();
let ordinary = store
.reserve_worker_create(WORKSPACE, RUNTIME, "ordinary", "fingerprint", None, &memory)
.unwrap();
let mut collision = seed(&request);
collision.worker_id = ordinary.worker_id;
assert!(seed_fixed_worker(&store, collision).is_err());
// Store-level fault setup only: the external harness never writes SQL.
store
.with_conn(|conn| {
allocate_resource_key(
conn,
WORKSPACE,
WorkspaceResourceKind::Worker,
&worker_resource_id(&worker()),
"now",
)?;
Ok(())
})
.unwrap();
assert!(seed_fixed_worker(&store, seed(&request)).is_err());
assert!(
store
.get_worker_registry(WORKSPACE, &worker())
.unwrap()
.is_none()
);
}
#[test]
fn unknown_resource_identity_and_retained_removal_fence_are_refused() {
let request = request();
let malformed = bootstrap();
malformed
.with_conn(|conn| {
allocate_resource_key(
conn,
WORKSPACE,
WorkspaceResourceKind::Worker,
"unknown-identity",
"now",
)?;
Ok(())
})
.unwrap();
assert!(matches!(
seed_fixed_worker(&malformed, seed(&request)),
Err(Error::InvalidInput(_))
));
assert!(
malformed
.get_worker_registry(WORKSPACE, &worker())
.unwrap()
.is_none()
);
let removed = bootstrap();
removed.with_conn(|conn| {
conn.execute(
"INSERT INTO worker_registry_projection_removals(workspace_id, runtime_id, worker_id) VALUES (?1, ?2, ?3)",
params![WORKSPACE, RUNTIME, WORKER],
)?;
Ok(())
}).unwrap();
assert!(matches!(
seed_fixed_worker(&removed, seed(&request)),
Err(Error::InvalidInput(_))
));
assert!(
removed
.worker_resource_key(WORKSPACE, &worker())
.unwrap()
.is_none()
);
}
#[test]
fn unknown_workspace_runtime_and_revoked_binding_are_refused() {
let store = bootstrap();
let request = request();
let mut unknown = seed(&request);
unknown.workspace_id = "unknown";
assert!(seed_fixed_worker(&store, unknown).is_err());
let mut unknown = seed(&request);
unknown.runtime_id = "unknown";
assert!(seed_fixed_worker(&store, unknown).is_err());
store
.revoke_workspace_runtime_binding(WORKSPACE, RUNTIME, "2026-10-24T01:00:00Z")
.unwrap();
assert!(seed_fixed_worker(&store, seed(&request)).is_err());
assert!(
store
.worker_resource_key(WORKSPACE, &worker())
.unwrap()
.is_none()
);
}
#[test]
fn unsupported_intent_is_refused_without_partial_registration() {
let store = bootstrap();
let base = request();
let mut requests = Vec::new();
let mut request = base.clone();
request.singleton_key = Some("fixture-singleton".into());
requests.push(request);
let mut request = base.clone();
request.intent = WorkerSpawnIntent::WorkspaceOrchestrator;
requests.push(request);
let mut request = base.clone();
request.resolved_subjektiv_attached = true;
requests.push(request);
let mut request = base.clone();
request
.workdir_attachment_requests
.push(WorkerSpawnWorkingDirectoryAttachmentRequest {
alias: workdir::WorkdirAttachmentAlias::new("repo").unwrap(),
working_directory: WorkerSpawnWorkingDirectoryRequest {
repository_key: "repo".into(),
selector: None,
},
});
requests.push(request);
for request in requests {
assert!(seed_fixed_worker(&store, seed(&request)).is_err());
}
assert!(
store
.get_worker_registry(WORKSPACE, &worker())
.unwrap()
.is_none()
);
assert!(
store
.worker_resource_key(WORKSPACE, &worker())
.unwrap()
.is_none()
);
assert_eq!(
seed_fixed_worker(&store, seed(&base))
.unwrap()
.worker_id
.to_string(),
WORKER
);
}
#[test]
fn late_catalog_failure_rolls_back_reservation_and_resource_allocation() {
let store = bootstrap();
let request = request();
store
.with_conn(|conn| {
conn.execute_batch(
"CREATE TEMP TRIGGER fail_fixture_catalog BEFORE INSERT ON worker_registry
BEGIN SELECT RAISE(ABORT, 'injected catalog failure'); END",
)?;
Ok(())
})
.unwrap();
assert!(seed_fixed_worker(&store, seed(&request)).is_err());
assert!(
!store
.has_active_worker_create_reservation(WORKSPACE, &worker())
.unwrap()
);
assert!(
store
.worker_resource_key(WORKSPACE, &worker())
.unwrap()
.is_none()
);
store
.with_conn(|conn| {
conn.execute_batch("DROP TRIGGER fail_fixture_catalog")?;
Ok(())
})
.unwrap();
seed_fixed_worker(&store, seed(&request)).unwrap();
assert_eq!(
store
.worker_resource_key(WORKSPACE, &worker())
.unwrap()
.as_deref(),
Some("W-1")
);
}
#[test]
fn ordinary_creation_still_allocates_and_replays_without_catalog_insertion() {
let store = bootstrap();
let memory = store.get_workspace_memory_settings(WORKSPACE).unwrap();
let reserve = |key| {
store
.reserve_worker_create(WORKSPACE, RUNTIME, key, "fingerprint", None, &memory)
.unwrap()
};
let first = reserve("ordinary-one");
assert_eq!(first, reserve("ordinary-one"));
let second = reserve("ordinary-two");
assert_ne!(first.worker_id, second.worker_id);
assert_ne!(first.worker_id.to_string(), WORKER);
assert_eq!(
first.worker_id.as_uuid().get_version(),
Some(uuid::Version::SortRand)
);
let worker = RuntimeWorkerRef::new(RUNTIME, first.worker_id.to_string());
assert!(
store
.has_active_worker_create_reservation(WORKSPACE, &worker)
.unwrap()
);
assert!(
store
.get_worker_registry(WORKSPACE, &worker)
.unwrap()
.is_none()
);
assert!(
store
.reserve_worker_create(WORKSPACE, RUNTIME, "ordinary-one", "changed", None, &memory)
.is_err()
);
}
}
+12
View File
@@ -9,6 +9,7 @@ autotests = false
[features]
default = []
e2e = []
fixture-support = ["yoi-workspace-server/test-support", "worker-runtime/test-support"]
[dependencies]
libc.workspace = true
@@ -17,11 +18,17 @@ serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
sha2.workspace = true
tempfile.workspace = true
protocol.workspace = true
tungstenite = { version = "0.29", default-features = false, features = ["handshake"] }
[dev-dependencies]
chrono = { version = "0.4", default-features = false, features = ["clock"] }
yoi-workspace-server.workspace = true
server-api.workspace = true
worker-runtime.workspace = true
session-store.workspace = true
config-source.workspace = true
manifest.workspace = true
webauthn-rs-proto = "0.5.5"
webauthn-authenticator-rs = { version = "0.5.5", default-features = false, features = ["softpasskey"] }
@@ -44,3 +51,8 @@ required-features = ["e2e"]
name = "worker_environment"
path = "tests/worker_environment.rs"
required-features = ["e2e"]
[[test]]
name = "worker_restore"
path = "tests/worker_restore.rs"
required-features = ["e2e", "fixture-support"]
+38
View File
@@ -7,3 +7,41 @@
- [`PanelHarness`]: PTY経由の入力、観測イベントによる同期、失敗時のartifact保存。
- [`FixtureWorkspace`]: TUI用WorkspaceとHOME/XDG領域の隔離。
- [`WorkerE2eEnvironment`]: Backend/Runtime用プロセスとfixtureの管理。
- [`FixtureSource`]: 提供資産の全ファイルSHA-256記録、独立copy、原本不変の検証。
- [`WorkerSessionSubscription`]: 公開Worker WebSocketの初期full Snapshot照合と停止確認。モデル呼出しは行わない。
## 固定Workerの段階起動
1. 提供fixtureを所有crateの型で検証し、`FixtureSource`で原本hashを記録する。各環境の一時領域へcopyする。fixtureが未提供・不適合なら失敗とし、代替Workerを生成しない。
2. `initialize_runtime_identity`で固定Runtime IDに新しい鍵を発行する。Backendの型付きstore APIで新しいAccount認証・Workspace署名identity・Runtime binding・固定Worker登録を準備する。
3. `start_seeded_backend`へ固定Workspace/Runtime IDとその環境の新しいbearerを渡す。Server起動後の公開APIからWorkspaceの公開identityを取得する。
4. `start_runtime_with_seed`はtrust設定後、Runtime processの起動前にseed closureを呼ぶ。ここで実Server URLを使いRuntime所有crateの型付きinstallを呼ぶ。Runtime store rootは`runtime_data_dir.join("runtime")`。
5. Worker一覧とdetailから停止状態・観測tokenを確認し、一つの固定request IDで`restore_worker`を一度だけ呼ぶ。結果不明・拒否は成功扱いせず、tokenを自動更新しない。
6. `WorkerSessionSubscription::connect`で期待`protocol::SessionSnapshot`全体(entry ID、timestamp、順序、本文を含む)を照合する。二環境を同時に保持し、一方のstop/cleanup後も他方の購読・一覧・内容を再確認する。
7. `stop_worker`、購読の`expect_stopped`、環境cleanup、原本`assert_unchanged`を確認する。
所有crateの`test-support`依存はテスト側の非default `fixture-support` featureだけで有効にする。製品binary providerは別Cargo invocationと専用`target/e2e-products`へ`yoi`・`yoi-server`・`yoi-runtime`を追加featureなしでビルドする。support test自身をこのtargetへビルドしてはならない。`YOI_E2E_BIN`等の明示binary overrideを使用する場合も、呼出し側が同じ分離を保証する。既存TUI用`e2e-test` featureは別の`target/e2e-tui-products`へビルドし、restore製品binaryへ混入しない。
固定資産の正常restore・並行隔離E2E:
```sh
cargo test -p yoi-e2e --features e2e,fixture-support --test worker_restore
# 親から提供された候補の検証(読取専用、原本の自動採用や生成はしない)
YOI_E2E_RESTORE_FIXTURE=target/fixture-candidate-v1 \
cargo test -p yoi-e2e --features e2e,fixture-support --test worker_restore
```
標準資産は`tests/e2e/fixtures/worker-codex-v1`の`manifest.json`・`worker-template.json`・`expected.json`・`workspace-config.dcdl`。manifestの各asset hash/size、schemaと固定ID、source revision、modelを検証する。Workspace configのimport参照を黙って最新へ置換せず、Gitでsource revisionに対する`resources`と`crates/builtin-source`の一致を確認する(Git履歴がない場合も失敗)。移植したSessionの正規公開projectionを供給された会話行と照合し、その全snapshotを実executionのWebSocketと比較する。欠落fixture・hash不一致・未実行は成功扱いしない。
通常のharness回帰試験(製品processは起動しない):
```sh
cargo test -p yoi-e2e --lib --no-default-features
```
既存の製品process E2Eは別結果として実行・報告する:
```sh
cargo test -p yoi-e2e --features e2e --test worker_environment
cargo test -p yoi-e2e --features e2e --test panel --test rewind
```
@@ -0,0 +1,52 @@
{
"source_revision": "1639be98411921308238b3a6a6e75c784c045156",
"model": "codex-oauth/gpt-6.1-sol",
"worker_id": "01a1657d-b762-7791-87ae-a49f28cadbce",
"session_id": "01a1657d-b818-77c1-bd73-175a27562f63",
"rows": [
{
"entry_id": "01a1657f-18bf-7a03-b8b8-953d27041fe0",
"timestamp": 1792704190655,
"role": "user",
"parts": [
{
"kind": "text",
"content": "これは保存と復元を確認するための人工的な会話です。合言葉は「青い灯台」です。覚えたことを一文で返してください。Toolは使わないでください。"
}
]
},
{
"entry_id": "01a1657f-1f55-71d3-b418-8199b863f59a",
"timestamp": 1792704192341,
"role": "assistant",
"parts": [
{
"kind": "text",
"text": "合言葉は「青い灯台」と覚えました。"
}
]
},
{
"entry_id": "01a1657f-3053-71f3-bf4f-91290d82488b",
"timestamp": 1792704196691,
"role": "user",
"parts": [
{
"kind": "text",
"content": "先ほどの合言葉は何でしたか?合言葉だけを答えてください。"
}
]
},
{
"entry_id": "01a1657f-36ef-78b1-b0c8-b141f13b0c7b",
"timestamp": 1792704198383,
"role": "assistant",
"parts": [
{
"kind": "text",
"text": "青い灯台"
}
]
}
]
}
@@ -0,0 +1,34 @@
{
"format_version": 1,
"source_revision": "1639be98411921308238b3a6a6e75c784c045156",
"runtime_schema_version": 12,
"session_schema_version": 6,
"workspace_id": "01a16553-b901-7191-8ceb-8e2306fe3da9",
"runtime_id": "fixture-951707ee-93ca-4205-9c12-45f08a104a74",
"worker_id": "01a1657d-b762-7791-87ae-a49f28cadbce",
"session_id": "01a1657d-b818-77c1-bd73-175a27562f63",
"segment_id": "01a1657d-b818-77c1-bd73-176172a4bd82",
"model": "codex-oauth/gpt-6.1-sol",
"state": "stopped",
"acquisition": "Real isolated Codex OAuth two-turn artificial conversation; typed offline StoppedWorkerTemplate extraction. Session bytes unchanged; environment credentials and execution receipts excluded.",
"source_file_sha256": {
"worker.json": "912e620013a17f05c1aaf900da48c9ee0ecb1f54121efc3721e03f8755eb5eca",
"metadata.json": "657928a89ec621bd092b86924201ed7b562e9683261789ce18bdb1d248bdc450",
"session/session.json": "22b01daab68a28b9777a1f7abb9ef9f7f6cbae113089446602f5d15421e7c7f3",
"session/segments/01a1657d-b818-77c1-bd73-176172a4bd82.jsonl": "7c5663bb06d9406d5bd0a5086159b31c0d7799458fcb1c6f81e18b63c49f4df5"
},
"files": {
"worker-template.json": {
"bytes": 48343,
"sha256": "8bf6ce3c71c4f05c82314de93c1e8e51b7502b00915b6adb8b33046944e463b2"
},
"expected.json": {
"bytes": 1474,
"sha256": "b9846db47ecb8f3f53b2abd9d5f0c766122f86015a7a7df7addf8beab3e9c55a"
},
"workspace-config.dcdl": {
"bytes": 1141,
"sha256": "9b07507b71733081f077e2e9968a7f1f676c916a243bb355d1813f067a55c415"
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,22 @@
{
profile = {
default_profile = "project:fixture-chat";
entries = [{
selector = "project:fixture-chat";
label = "Isolated Codex fixture conversation";
description = "Artificial conversation only; no Workdir, Tools, Subject or background work.";
profile = (import "$builtin/profiles/job.dcdl") // {
slug = "fixture-chat";
worker = { mode = "tools"; };
model = { ref = "codex-oauth/gpt-6.1-sol"; };
scope = "workspace_read";
delegation_scope = "workspace_read";
permissions = { default_action = "deny"; rule = []; };
engine = { instruction = "default"; reasoning = "low"; max_tokens = 2048; max_turns = 4; language = "Japanese"; };
session = { record_event_trace = false; };
feature = { workdir_catalog = { enabled = false; }; };
};
}];
};
prompts = { default_prompt = "これは隔離環境の人工的な会話テストです。日本語で簡潔に応答してください。Toolや外部操作は行わず、ユーザーが会話中に提示した情報だけを使ってください。"; };
} as WorkspaceConfigSchema
+173
View File
@@ -0,0 +1,173 @@
//! Byte-preserving fixture transport, independent of the owning store's schema validation.
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use sha2::{Digest, Sha256};
use crate::{HarnessError, Result};
#[derive(Debug, Clone)]
pub struct FixtureSource {
root: PathBuf,
hashes: BTreeMap<PathBuf, String>,
}
impl FixtureSource {
/// Record every supplied file, including the manifest. No fixture is generated
/// or repaired here; the typed store importer must additionally validate it.
pub fn capture(root: &Path) -> Result<Self> {
let hashes = inventory(root)?;
if hashes.is_empty() {
return Err(HarnessError::Protocol("provided fixture is empty".into()));
}
Ok(Self {
root: root.to_owned(),
hashes,
})
}
pub fn hashes(&self) -> &BTreeMap<PathBuf, String> {
&self.hashes
}
pub fn assert_unchanged(&self) -> Result<()> {
if inventory(&self.root)? != self.hashes {
return Err(HarnessError::Protocol(
"provided fixture changed during execution".into(),
));
}
Ok(())
}
/// The destination must not exist. A copy is never a link to the source.
pub fn copy_to(&self, destination: &Path) -> Result<()> {
self.assert_unchanged()?;
let parent = destination
.parent()
.filter(|path| !path.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
if parent
.canonicalize()?
.starts_with(self.root.canonicalize()?)
{
return Err(HarnessError::Protocol(
"fixture copy destination is inside the original".into(),
));
}
fs::create_dir(destination)?;
for (relative, expected) in &self.hashes {
let bytes = fs::read(self.root.join(relative))?;
if hash(&bytes) != *expected {
return Err(HarnessError::Protocol(
"fixture changed while copying".into(),
));
}
let target = destination.join(relative);
fs::create_dir_all(target.parent().unwrap())?;
use std::io::Write;
let mut file = fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(target)?;
file.write_all(&bytes)?;
}
if inventory(destination)? != self.hashes {
return Err(HarnessError::Protocol(
"fixture copy failed hash verification".into(),
));
}
self.assert_unchanged()
}
}
fn hash(bytes: &[u8]) -> String {
format!(
"sha256:{}",
Sha256::digest(bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
)
}
fn inventory(root: &Path) -> Result<BTreeMap<PathBuf, String>> {
fn visit(root: &Path, path: &Path, files: &mut BTreeMap<PathBuf, String>) -> Result<()> {
let metadata = fs::symlink_metadata(path)?;
if metadata.is_dir() {
for entry in fs::read_dir(path)? {
visit(root, &entry?.path(), files)?;
}
} else if metadata.is_file() {
files.insert(
path.strip_prefix(root).unwrap().to_owned(),
hash(&fs::read(path)?),
);
} else {
return Err(HarnessError::Protocol(
"fixture contains a symlink or special file".into(),
));
}
Ok(())
}
if !fs::symlink_metadata(root)?.is_dir() {
return Err(HarnessError::Protocol(
"fixture source must be a real directory".into(),
));
}
let mut files = BTreeMap::new();
visit(root, root, &mut files)?;
Ok(files)
}
#[cfg(test)]
mod tests {
use super::*;
// These are transport-only byte samples, never saved Worker substitutes.
#[test]
fn copies_are_independent_and_source_mutation_is_detected() -> Result<()> {
let temp = tempfile::tempdir()?;
let source = temp.path().join("source");
fs::create_dir(&source)?;
fs::write(source.join("bytes"), b"original")?;
let fixture = FixtureSource::capture(&source)?;
assert!(fixture.copy_to(&source.join("nested-copy")).is_err());
assert!(!source.join("nested-copy").exists());
fixture.assert_unchanged()?;
let first = temp.path().join("first");
let second = temp.path().join("second");
fixture.copy_to(&first)?;
fixture.copy_to(&second)?;
fs::write(first.join("bytes"), b"changed")?;
fs::remove_dir_all(first)?;
assert_eq!(fs::read(second.join("bytes"))?, b"original");
fixture.assert_unchanged()?;
fs::write(source.join("bytes"), b"changed")?;
assert!(fixture.assert_unchanged().is_err());
Ok(())
}
#[test]
fn rejects_missing_empty_existing_destination_and_extra_file() -> Result<()> {
let temp = tempfile::tempdir()?;
assert!(FixtureSource::capture(&temp.path().join("missing")).is_err());
assert!(FixtureSource::capture(temp.path()).is_err());
fs::write(temp.path().join("bytes"), b"original")?;
let source = FixtureSource::capture(temp.path())?;
assert!(source.copy_to(temp.path()).is_err());
fs::write(temp.path().join("extra"), b"new")?;
assert!(source.assert_unchanged().is_err());
Ok(())
}
#[cfg(unix)]
#[test]
fn rejects_symbolic_links() -> Result<()> {
let temp = tempfile::tempdir()?;
fs::write(temp.path().join("bytes"), b"original")?;
std::os::unix::fs::symlink("bytes", temp.path().join("link"))?;
assert!(FixtureSource::capture(temp.path()).is_err());
Ok(())
}
}
+29 -1
View File
@@ -1268,7 +1268,11 @@ impl Drop for PanelHarness {
}
}
mod fixture_source;
pub use fixture_source::FixtureSource;
mod worker_environment;
mod worker_session;
pub use worker_session::{WorkerSessionSubscription, validate_full_snapshot};
pub use worker_environment::{
WorkerBinaryProviderInfo, WorkerE2eEnvironment, WorkerEnvironmentCleanupReport,
@@ -1627,6 +1631,8 @@ fn resolve_yoi_binary() -> Result<BinaryProviderInfo> {
"e2e-test".to_owned(),
"--bin".to_owned(),
"yoi".to_owned(),
"--target-dir".to_owned(),
tui_product_target_dir()?.to_string_lossy().into_owned(),
];
if test_profile() == "release" {
args.push("--release".to_owned());
@@ -1648,7 +1654,9 @@ fn resolve_yoi_binary() -> Result<BinaryProviderInfo> {
});
}
let binary = current_target_profile_dir()?.join(binary_name());
let binary = tui_product_target_dir()?
.join(test_profile())
.join(binary_name());
let info = BinaryProviderInfo {
provider: "cargo-build".to_owned(),
binary,
@@ -1675,6 +1683,26 @@ fn workspace_root() -> Result<PathBuf> {
.ok_or_else(|| HarnessError::Protocol("could not resolve workspace root".to_owned()))
}
/// Product processes must never reuse the target used by test-support builds.
fn product_target_dir() -> Result<PathBuf> {
dedicated_product_target("e2e-products")
}
// Existing PTY tests need their observer feature, never on restore binaries.
fn tui_product_target_dir() -> Result<PathBuf> {
dedicated_product_target("e2e-tui-products")
}
fn dedicated_product_target(name: &str) -> Result<PathBuf> {
let target = workspace_root()?.join("target").join(name);
if current_target_profile_dir()?.parent() == Some(target.as_path()) {
return Err(HarnessError::Protocol(
"run support tests outside reserved product binary targets".to_owned(),
));
}
Ok(target)
}
fn current_target_profile_dir() -> Result<PathBuf> {
let mut path = std::env::current_exe()?;
while let Some(name) = path.file_name().and_then(|name| name.to_str()) {
+290 -36
View File
@@ -13,8 +13,8 @@ use serde_json::Value;
use sha2::{Digest, Sha256};
use super::{
FixtureAllocation, HarnessError, Result, allocate_fixture, command_display,
current_target_profile_dir, now_ms, test_profile, workspace_root, yoi_binary_info,
FixtureAllocation, HarnessError, Result, allocate_fixture, command_display, now_ms,
product_target_dir, test_profile, workspace_root,
};
const STARTUP_WAIT: Duration = Duration::from_secs(15);
@@ -61,22 +61,20 @@ pub fn worker_binary_provider_info() -> Result<WorkerBinaryProviderInfo> {
fn resolve_worker_binaries() -> Result<WorkerBinaryProviderInfo> {
let workspace_root = workspace_root()?;
let profile = test_profile();
let yoi = yoi_binary_info()?;
let mut records = vec![binary_record(
"yoi",
yoi.binary,
yoi.provider,
yoi.build_command,
)?];
let mut records = Vec::new();
let yoi_override = resolve_override("YOI_E2E_BIN", &workspace_root)?;
let server_override = resolve_override("YOI_E2E_SERVER_BIN", &workspace_root)?;
let runtime_override = resolve_override("YOI_E2E_RUNTIME_BIN", &workspace_root)?;
let mut build_command = None;
if server_override.is_none() || runtime_override.is_none() {
if yoi_override.is_none() || server_override.is_none() || runtime_override.is_none() {
let cargo = PathBuf::from(std::env::var_os("CARGO").unwrap_or_else(|| "cargo".into()));
let mut args = vec![
"build".to_string(),
"-p".to_string(),
"yoi".to_string(),
"--bin".to_string(),
"yoi".to_string(),
"-p".to_string(),
"yoi-workspace-server".to_string(),
"--bin".to_string(),
"yoi-server".to_string(),
@@ -84,6 +82,8 @@ fn resolve_worker_binaries() -> Result<WorkerBinaryProviderInfo> {
"worker-runtime".to_string(),
"--bin".to_string(),
"yoi-runtime".to_string(),
"--target-dir".to_string(),
product_target_dir()?.to_string_lossy().into_owned(),
];
if profile == "release" {
args.push("--release".to_string());
@@ -106,7 +106,16 @@ fn resolve_worker_binaries() -> Result<WorkerBinaryProviderInfo> {
build_command = Some(rendered);
}
let profile_dir = current_target_profile_dir()?;
let profile_dir = product_target_dir()?.join(&profile);
let (yoi_path, yoi_provider, yoi_build) = match yoi_override {
Some(path) => (path, "YOI_E2E_BIN".to_string(), None),
None => (
profile_dir.join(executable_name("yoi")),
"cargo-build".to_string(),
build_command.clone(),
),
};
records.push(binary_record("yoi", yoi_path, yoi_provider, yoi_build)?);
let (server_path, server_provider, server_build) = match server_override {
Some(path) => (path, "YOI_E2E_SERVER_BIN".to_string(), None),
None => (
@@ -231,6 +240,7 @@ pub struct WorkerE2eEnvironment {
pub runtime_url: Option<String>,
server: Option<ManagedProcess>,
runtime: Option<ManagedProcess>,
runtime_public_identity: Option<Value>,
access_token: Option<String>,
client: Client,
binaries: WorkerBinaryProviderInfo,
@@ -292,6 +302,7 @@ impl WorkerE2eEnvironment {
runtime_url: None,
server: None,
runtime: None,
runtime_public_identity: None,
access_token: None,
client,
binaries,
@@ -374,6 +385,47 @@ impl WorkerE2eEnvironment {
Ok(())
}
/// Start a store populated by the Backend's typed test-support API. The seed
/// supplies fixed catalog IDs, but the bearer must be freshly issued for this
/// environment. No database mutation or fixture credential import occurs here.
pub fn start_seeded_backend(
&mut self,
workspace_id: String,
runtime_id: String,
access_token: String,
) -> Result<()> {
if self.server.is_some() || self.runtime.is_some() || self.workspace_id.is_some() {
return Err(HarnessError::Protocol(
"fixture seed requires an unstarted environment".into(),
));
}
if workspace_id.is_empty() || runtime_id.is_empty() || access_token.is_empty() {
return Err(HarnessError::Protocol(
"fixture seed omitted an identity or fresh bearer".into(),
));
}
if !self.server_data_dir.join("server.db").is_file() {
return Err(HarnessError::Protocol(
"typed Backend seed has not created server.db".into(),
));
}
if self.runtime_public_identity.is_some() {
if self.runtime_id != runtime_id {
return Err(HarnessError::Protocol(
"seeded Runtime ID differs from initialized identity".into(),
));
}
} else {
assert_absent_or_empty(&self.runtime_data_dir, "Runtime data directory")?;
}
self.workspace_id = Some(workspace_id);
self.runtime_id = runtime_id;
self.access_token = Some(access_token);
self.start_server_process()?;
self.export_workspace_public_identity()?;
self.write_metadata("seeded-backend-running")
}
pub fn start_backend(&mut self) -> Result<()> {
self.start_empty_backend()?;
let server_binary = self.binaries.binary("yoi-server")?;
@@ -417,7 +469,15 @@ impl WorkerE2eEnvironment {
"create Workspace",
)?;
let workspace_id = json_string(&created, "/workspace/workspace_id")?;
self.workspace_id = Some(workspace_id.clone());
self.workspace_id = Some(workspace_id);
self.export_workspace_public_identity()?;
self.write_metadata("backend-running")?;
Ok(())
}
fn export_workspace_public_identity(&self) -> Result<()> {
let server_url = self.required_server_url()?;
let workspace_id = self.required_workspace_id()?;
let identity = self.request_json(
self.client.get(format!(
"{server_url}/api/w/{workspace_id}/settings/signing-identity"
@@ -429,17 +489,27 @@ impl WorkerE2eEnvironment {
})?;
let bundle_path = self.root.join("workspace-public-identity.json");
fs::write(&bundle_path, serde_json::to_vec_pretty(&public_bundle)?)?;
self.write_metadata("backend-running")?;
Ok(())
}
fn start_runtime(&mut self) -> Result<()> {
let bundle_path = self.root.join("workspace-public-identity.json");
let server_url = self.required_server_url()?.to_string();
let workspace_id = self.required_workspace_id()?.to_string();
let runtime_binary = self.binaries.binary("yoi-runtime")?;
self.start_runtime_with_seed(|_| Ok(()))?;
self.assert_connected_and_empty()
}
/// Generate fresh keys before offline Backend registration when fixed IDs are
/// required. Returns only the public bundle; no fixture keys are accepted.
pub fn initialize_runtime_identity(&mut self, runtime_id: String) -> Result<Value> {
if self.runtime_public_identity.is_some() || self.runtime.is_some() {
return Err(HarnessError::Protocol(
"Runtime identity is already initialized".into(),
));
}
assert_absent_or_empty(&self.runtime_data_dir, "Runtime data directory")?;
self.runtime_id = runtime_id;
let binary = self.binaries.binary("yoi-runtime")?;
self.run_command(
&runtime_binary,
&binary,
&[
"identity",
"init",
@@ -450,8 +520,8 @@ impl WorkerE2eEnvironment {
],
false,
)?;
let runtime_identity = self.run_command(
&runtime_binary,
let identity = self.run_command(
&binary,
&[
"identity",
"show",
@@ -461,7 +531,31 @@ impl WorkerE2eEnvironment {
],
false,
)?;
let runtime_identity: Value = serde_json::from_slice(&runtime_identity.stdout)?;
let identity: Value = serde_json::from_slice(&identity.stdout)?;
self.runtime_public_identity = Some(identity.clone());
Ok(identity)
}
/// Initialize fresh Runtime keys/trust, then invoke the typed Runtime seed
/// before starting its process. The actual Backend URL is now available.
pub fn start_runtime_with_seed(
&mut self,
seed: impl FnOnce(&Self) -> Result<()>,
) -> Result<()> {
if self.runtime.is_some() {
return Err(HarnessError::Protocol("Runtime is already running".into()));
}
if self.runtime_public_identity.is_none() {
self.initialize_runtime_identity(self.runtime_id.clone())?;
}
let runtime_identity = self
.runtime_public_identity
.clone()
.expect("initialized identity");
let bundle_path = self.root.join("workspace-public-identity.json");
let server_url = self.required_server_url()?.to_string();
let workspace_id = self.required_workspace_id()?.to_string();
let runtime_binary = self.binaries.binary("yoi-runtime")?;
self.run_command(
&runtime_binary,
&[
@@ -497,6 +591,8 @@ impl WorkerE2eEnvironment {
));
}
seed(self)?;
self.write_metadata("runtime-seeded")?;
let mut command = self.isolated_command(&runtime_binary);
command.args([
"--bind",
@@ -519,22 +615,38 @@ impl WorkerE2eEnvironment {
self.runtime = Some(runtime);
self.runtime_url = Some(runtime_url.clone());
let registered = self.request_json(
self.client
.get(format!("{server_url}/api/w/{workspace_id}/runtimes")),
"observe Runtime registration before endpoint update",
)?;
let expected_binding_id = registered
.get("items")
.and_then(Value::as_array)
.and_then(|items| {
items.iter().find(|item| {
item.get("runtime_id").and_then(Value::as_str) == Some(self.runtime_id.as_str())
})
})
.and_then(|item| item.pointer("/management/binding/binding_id"))
.cloned();
self.request_json(
self.client
.post(format!("{server_url}/api/w/{workspace_id}/runtimes"))
.json(&serde_json::json!({
"public_bundle": runtime_identity,
"display_name": format!("E2E {}", self.runtime_id),
"endpoint": runtime_url
"endpoint": runtime_url,
"expected_binding_id": expected_binding_id,
})),
"register Runtime",
)?;
self.assert_connected_and_empty()?;
self.assert_connected()?;
self.write_metadata("running")?;
Ok(())
}
pub fn assert_connected_and_empty(&self) -> Result<()> {
pub fn assert_connected(&self) -> Result<()> {
let server_url = self.required_server_url()?;
let workspace_id = self.required_workspace_id()?;
// connection testは実署名要求。保存済みの接続flagを認証成功の代わりにしない。
@@ -574,11 +686,22 @@ impl WorkerE2eEnvironment {
self.runtime_id
)));
}
let workers = self.request_json(
Ok(())
}
pub fn list_workers(&self) -> Result<Value> {
let server_url = self.required_server_url()?;
let workspace_id = self.required_workspace_id()?;
self.request_json(
self.client
.get(format!("{server_url}/api/w/{workspace_id}/workers")),
"list Workers",
)?;
)
}
pub fn assert_connected_and_empty(&self) -> Result<()> {
self.assert_connected()?;
let workers = self.list_workers()?;
if workers
.get("items")
.and_then(Value::as_array)
@@ -591,6 +714,104 @@ impl WorkerE2eEnvironment {
Ok(())
}
fn worker_url(&self, worker_id: &str, suffix: &str) -> Result<String> {
let mut url = reqwest::Url::parse(self.required_server_url()?)
.map_err(|error| HarnessError::Protocol(format!("invalid Server URL: {error}")))?;
url.path_segments_mut()
.map_err(|_| HarnessError::Protocol("Server URL has no path segments".into()))?
.extend([
"api",
"w",
self.required_workspace_id()?,
"runtimes",
&self.runtime_id,
"workers",
worker_id,
]);
if !suffix.is_empty() {
url.path_segments_mut().unwrap().extend(suffix.split('/'));
}
Ok(url.into())
}
pub fn worker_detail(&self, worker_id: &str) -> Result<Value> {
self.request_json(
self.client.get(self.worker_url(worker_id, "")?),
"read Worker detail",
)
}
/// Exactly one restore submission. No retries, replacement token or new intent.
pub fn restore_worker(
&self,
worker_id: &str,
request_id: &str,
expected_observation_token: &str,
) -> Result<Value> {
self.request_json(
self.client
.post(self.worker_url(worker_id, "restore")?)
.timeout(STARTUP_WAIT)
.json(&serde_json::json!({
"request_id": request_id,
"expected_observation_token": expected_observation_token,
})),
"restore Worker",
)
}
pub fn stop_worker(&self, worker_id: &str) -> Result<Value> {
self.request_json(
self.client
.post(self.worker_url(worker_id, "stop")?)
.json(&serde_json::json!({})),
"stop Worker",
)
}
/// Authorization stays in memory; callers must not persist or debug this request.
pub fn worker_protocol_request(&self, worker_id: &str) -> Result<reqwest::blocking::Request> {
let token = self.access_token.as_deref().ok_or_else(|| {
HarnessError::Protocol("Backend access token is not initialized".into())
})?;
self.client
.get(self.worker_url(worker_id, "protocol/ws")?)
.bearer_auth(token)
.build()
.map_err(|_| HarnessError::Protocol("build Worker protocol request".into()))
}
/// The other environment must reject this environment's fresh bearer even
/// when both contain identical Workspace/Runtime/Worker IDs. Never logs tokens.
pub fn assert_authentication_isolated_from(&self, other: &Self) -> Result<()> {
let token = self
.access_token
.as_deref()
.ok_or_else(|| HarnessError::Protocol("missing source bearer".into()))?;
if other.access_token.as_deref() == Some(token) {
return Err(HarnessError::Protocol(
"environments share authentication material".into(),
));
}
let (server, workspace) = other.backend_target()?;
let response = self
.client
.get(format!("{server}/api/w/{workspace}/workers"))
.bearer_auth(token)
.send()
.map_err(|_| {
HarnessError::Protocol("cross-environment authorization request failed".into())
})?;
let status = response.status().as_u16();
other.append_operation("reject other environment bearer", status)?;
if status != 401 {
return Err(HarnessError::Protocol(format!(
"cross-environment bearer was not rejected with 401: {status}"
)));
}
Ok(())
}
pub fn install_backend_token(&self, config_home: &Path) -> Result<PathBuf> {
let server_url = self.required_server_url()?;
let access_token = self.access_token.as_deref().ok_or_else(|| {
@@ -636,7 +857,7 @@ impl WorkerE2eEnvironment {
"assignee": null,
"readiness": null,
"risk_flags": [],
"workflow_state": "planning",
"workflow_state": "open",
"queued_by": null,
"queued_at": null,
"repository_key": null,
@@ -1026,7 +1247,7 @@ impl ManagedProcess {
fn shutdown(&mut self, timeout: Duration) -> Result<ExitStatus> {
if let Some(status) = self.child.try_wait()? {
self.join_readers();
return Ok(status);
return self.require_successful_exit(status);
}
let signal_result = unsafe { libc::kill(self.child.id() as i32, libc::SIGTERM) };
if signal_result != 0 {
@@ -1036,13 +1257,7 @@ impl ManagedProcess {
loop {
if let Some(status) = self.child.try_wait()? {
self.join_readers();
if status.success() {
return Ok(status);
}
return Err(HarnessError::Protocol(format!(
"{} did not complete graceful shutdown: {status}",
self.name
)));
return self.require_successful_exit(status);
}
if start.elapsed() >= timeout {
let _ = self.child.kill();
@@ -1057,6 +1272,17 @@ impl ManagedProcess {
}
}
fn require_successful_exit(&self, status: ExitStatus) -> Result<ExitStatus> {
if status.success() {
Ok(status)
} else {
Err(HarnessError::Protocol(format!(
"{} did not complete graceful shutdown: {status}",
self.name
)))
}
}
fn join_readers(&mut self) {
for reader in self.readers.drain(..) {
let _ = reader.join();
@@ -1177,6 +1403,34 @@ fn io_error(operation: &str, error: std::io::Error) -> HarnessError {
mod startup_address_tests {
use super::parse_address_after;
#[test]
fn shutdown_rejects_nonzero_exit_even_when_process_already_exited() -> super::Result<()> {
let root = tempfile::tempdir()?;
for code in [0, 7] {
let mut command = std::process::Command::new("/bin/sh");
command
.env_clear()
.current_dir(root.path())
.args(["-c", &format!("exit {code}")]);
let mut process = super::ManagedProcess::spawn(
"already-exited",
command,
&root.path().join(format!("{code}.stdout")),
&root.path().join(format!("{code}.stderr")),
)?;
// Reap first to deterministically exercise the initial try_wait branch.
assert_eq!(process.child.wait()?.code(), Some(code));
let result = process.shutdown(std::time::Duration::from_secs(1));
if code == 0 {
assert!(result?.success());
} else {
assert!(matches!(result, Err(super::HarnessError::Protocol(message))
if message.contains("did not complete graceful shutdown")));
}
}
Ok(())
}
#[test]
fn startup_address_waits_for_complete_valid_socket_record() {
let record = "yoi-server listening on http://127.0.0.1:49123\n";
+284
View File
@@ -0,0 +1,284 @@
//! Live public Worker protocol assertions. No Submit/Resume/model request is sent.
use std::net::TcpStream;
use std::time::{Duration, Instant};
use protocol::{Event, SessionSnapshot, WorkerState};
use tungstenite::{Message, WebSocket, client::IntoClientRequest};
use crate::{HarnessError, Result, WorkerE2eEnvironment};
const WS_WAIT: Duration = Duration::from_secs(10);
pub struct WorkerSessionSubscription {
socket: WebSocket<TcpStream>,
}
impl WorkerSessionSubscription {
pub fn connect(
environment: &WorkerE2eEnvironment,
worker_id: &str,
expected: &SessionSnapshot,
) -> Result<Self> {
let authorized = environment.worker_protocol_request(worker_id)?;
let mut url = authorized.url().clone();
url.set_scheme("ws")
.map_err(|_| error("invalid protocol URL"))?;
let mut request = url
.as_str()
.into_client_request()
.map_err(|_| error("build protocol handshake"))?;
request.headers_mut().extend(authorized.headers().clone());
// Only this environment's loopback endpoint is permitted, never a proxy.
let address = url
.socket_addrs(|| None)
.map_err(|_| error("resolve protocol address"))?
.into_iter()
.find(|address| address.ip().is_loopback())
.ok_or_else(|| error("protocol address is not loopback"))?;
let stream = TcpStream::connect_timeout(&address, WS_WAIT)?;
stream.set_read_timeout(Some(WS_WAIT))?;
stream.set_write_timeout(Some(WS_WAIT))?;
let (socket, _) =
tungstenite::client(request, stream).map_err(|_| error("protocol handshake failed"))?;
let mut subscription = Self { socket };
let deadline = Instant::now() + WS_WAIT;
loop {
match subscription.read(deadline)? {
Message::Text(text) => {
let raw: serde_json::Value = serde_json::from_str(&text)?;
validate_full_snapshot(&raw, expected)?;
// Only validated public content is retained, never request headers.
std::fs::write(
environment.artifacts_dir.join("restored-session.json"),
serde_json::to_vec_pretty(&raw)?,
)?;
return Ok(subscription);
}
Message::Ping(_) => subscription
.socket
.flush()
.map_err(|_| error("flush protocol Pong"))?,
Message::Pong(_) => {}
_ => return Err(error("protocol ended before its initial full Snapshot")),
}
}
}
/// Prove an already-open subscription remains usable after the other
/// environment is stopped and cleaned up. This does not invoke the model.
pub fn assert_alive(&mut self) -> Result<()> {
let payload = b"restore-e2e-isolation".to_vec();
self.socket
.send(Message::Ping(payload.clone().into()))
.map_err(|_| error("send protocol Ping"))?;
let deadline = Instant::now() + WS_WAIT;
loop {
match self.read(deadline)? {
Message::Pong(bytes) if bytes.as_ref() == payload.as_slice() => return Ok(()),
Message::Ping(_) => self
.socket
.flush()
.map_err(|_| error("flush protocol Pong"))?,
Message::Pong(_) => {}
_ => {
return Err(error(
"idle subscription changed or ended during isolation check",
));
}
}
}
}
/// A public stop must terminate the connected execution, not merely update
/// a retained catalog row. Require the shutdown event or websocket close.
pub fn expect_stopped(mut self) -> Result<()> {
let deadline = Instant::now() + WS_WAIT;
loop {
match self.read(deadline)? {
Message::Close(_) => return Ok(()),
Message::Text(text) => match serde_json::from_str::<Event>(&text)? {
Event::Shutdown => return Ok(()),
Event::WorkerState { .. } => {}
_ => return Err(error("unexpected protocol event after Worker stop")),
},
Message::Ping(_) => self
.socket
.flush()
.map_err(|_| error("flush protocol Pong"))?,
Message::Pong(_) => {}
_ => return Err(error("unexpected protocol frame after Worker stop")),
}
}
}
fn read(&mut self, deadline: Instant) -> Result<Message> {
let remaining = deadline
.checked_duration_since(Instant::now())
.filter(|duration| !duration.is_zero())
.ok_or_else(|| error("protocol deadline exceeded"))?;
self.socket.get_mut().set_read_timeout(Some(remaining))?;
self.socket
.read()
.map_err(|_| error("protocol read failed or timed out"))
}
}
fn error(message: &str) -> HarnessError {
HarnessError::Protocol(message.into())
}
pub fn validate_full_snapshot(raw: &serde_json::Value, expected: &SessionSnapshot) -> Result<()> {
let event: Event = serde_json::from_value(raw.clone())?;
// Product DTOs intentionally tolerate unknown fields for compatibility. This
// fixture boundary does not: anything discarded by typed decoding must never
// be persisted to an artifact, including fields outside the Session payload.
if serde_json::to_value(&event)? != *raw {
return Err(error(
"noncanonical or unknown fields in full protocol Snapshot envelope",
));
}
let Event::Snapshot {
session,
state,
in_flight,
internal_workers,
..
} = event
else {
return Err(error("first protocol event is not a full Snapshot"));
};
if state.state != WorkerState::Idle || !in_flight.is_empty() || !internal_workers.is_empty() {
return Err(error(
"restored Worker is not an idle, completed standalone Session",
));
}
// Typed equality covers entry order/ID/timestamp/provenance/content; raw
// equality also refuses unknown fields that could leak private data.
if session != *expected
|| raw.pointer("/data/session") != Some(&serde_json::to_value(expected)?)
{
return Err(error(
"full Session snapshot differs from supplied expected rows",
));
}
if !session.pending_submissions.submissions.is_empty() {
return Err(error("fixture has pending submissions"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use protocol::{
Greeting, InFlightSnapshot, Segment, SessionEntryProvenance, SessionSnapshotEntry,
SessionSnapshotEntryData, WorkerStateSnapshot, WorkerStatus,
};
// Protocol-validation samples only, not a substitute saved Worker fixture.
fn sample() -> (serde_json::Value, SessionSnapshot) {
let session = SessionSnapshot {
pending_submissions: Default::default(),
entries: vec![SessionSnapshotEntry {
entry_id: "E-unit-1".into(),
timestamp: 123,
provenance: SessionEntryProvenance::HumanInput,
derived_from: vec![],
use_policy: None,
data: SessionSnapshotEntryData::UserInput {
segments: vec![Segment::text("unit input")],
},
}],
};
let event = Event::Snapshot {
session: session.clone(),
state: WorkerStateSnapshot::from(WorkerStatus::Idle),
in_flight: InFlightSnapshot::default(),
internal_workers: vec![],
greeting: Greeting {
worker_name: "unit".into(),
cwd: ".".into(),
provider: "unit".into(),
model: "unit".into(),
reasoning: None,
scope_summary: "none".into(),
tools: vec![],
context_window: 0,
context_tokens: 0,
context_usage: None,
},
};
(serde_json::to_value(event).unwrap(), session)
}
#[test]
fn full_snapshot_requires_exact_ids_timestamps_contents_and_no_unknown_fields() {
let (raw, expected) = sample();
validate_full_snapshot(&raw, &expected).unwrap();
for (key, value) in [
("entry_id", serde_json::json!("different")),
("timestamp", serde_json::json!(124)),
(
"hidden_reasoning",
serde_json::json!("must never be exposed"),
),
("segments", serde_json::json!([])),
] {
let mut changed = raw.clone();
changed["data"]["session"]["entries"][0][key] = value;
assert!(
validate_full_snapshot(&changed, &expected).is_err(),
"{key}"
);
}
let mut missing = raw.clone();
missing["data"]["session"]["entries"] = serde_json::json!([]);
assert!(validate_full_snapshot(&missing, &expected).is_err());
let mut extra = raw;
let entry = extra["data"]["session"]["entries"][0].clone();
extra["data"]["session"]["entries"]
.as_array_mut()
.unwrap()
.push(entry);
assert!(validate_full_snapshot(&extra, &expected).is_err());
}
#[test]
fn full_snapshot_rejects_unknown_fields_outside_session_before_artifact_write() {
let (raw, expected) = sample();
for pointer in [
"",
"/data",
"/data/greeting",
"/data/state",
"/data/state/state",
] {
let mut changed = raw.clone();
changed
.pointer_mut(pointer)
.unwrap()
.as_object_mut()
.unwrap()
.insert(
"hidden_reasoning".into(),
serde_json::json!("must not reach artifact"),
);
assert!(
validate_full_snapshot(&changed, &expected).is_err(),
"{pointer}"
);
}
let mut changed = raw;
changed["data"]["greeting"]["private_key"] = serde_json::json!("must not reach artifact");
assert!(validate_full_snapshot(&changed, &expected).is_err());
}
#[test]
fn non_snapshot_event_cannot_stand_in_for_live_snapshot() {
let (_, expected) = sample();
assert!(
validate_full_snapshot(&serde_json::to_value(Event::Shutdown).unwrap(), &expected)
.is_err()
);
}
}
+5 -5
View File
@@ -41,13 +41,13 @@ fn backend_panel_loads_backend_owned_tickets_and_supports_reload_navigation_and_
&event.data,
&ready_ticket_id,
"Planning E2E Ticket A",
"planning",
"open",
)
&& has_backend_ticket(
&event.data,
&planning_ticket_id,
"Planning E2E Ticket B",
"planning",
"open",
)
},
)?;
@@ -82,13 +82,13 @@ fn backend_panel_loads_backend_owned_tickets_and_supports_reload_navigation_and_
&event.data,
&ready_ticket_id,
"Planning E2E Ticket A",
"planning",
"open",
)
&& has_backend_ticket(
&event.data,
&planning_ticket_id,
"Planning E2E Ticket B",
"planning",
"open",
)
},
)?;
@@ -137,7 +137,7 @@ fn backend_panel_shell_enter_path_uses_the_same_backend_authority() -> yoi_e2e::
&event.data,
&ready_ticket_id,
"Planning E2E Ticket A",
"planning",
"open",
)
},
)?;
+274
View File
@@ -0,0 +1,274 @@
//! Fixed, externally supplied Worker restore. Never generates a fixture or sends model input.
#[path = "worker_restore/fixture.rs"]
mod fixture;
#[path = "worker_restore/seed.rs"]
mod seed;
use std::fs;
use std::time::{Duration, Instant};
use fixture::{Fixture, failure};
use seed::Installed;
use server_api::{
RuntimeWorkerLifecycleResult, RuntimeWorkerShowResponse, WorkerListResponse,
WorkerOperationState, WorkerRestoreResponse, WorkerRestoreState, WorkerSummary,
};
use yoi_e2e::{FixtureSource, Result, WorkerE2eEnvironment, WorkerSessionSubscription};
#[test]
fn supplied_stopped_worker_restores_exact_live_session_and_cleans_up() -> Result<()> {
let fixture = Fixture::load()?;
let result = (|| {
let installed = seed::start(&fixture)?;
let subscription = restore(&installed, &fixture, "fixed-fixture-restore-single")?;
stop_and_cleanup(installed, vec![subscription], &fixture)
})();
fixture.source.assert_unchanged()?;
result
}
#[test]
fn concurrent_fixed_id_restores_keep_sessions_storage_and_cleanup_isolated() -> Result<()> {
let fixture = Fixture::load()?;
let result = (|| {
let (first, second) = std::thread::scope(|scope| {
let first = scope.spawn(|| seed::start(&fixture));
let second = seed::start(&fixture);
(
first
.join()
.map_err(|_| failure("first environment startup panicked"))
.and_then(|result| result),
second,
)
});
let first = first?;
let second = second?;
assert_eq!(
first.environment.workspace_id,
second.environment.workspace_id
);
assert_eq!(first.environment.runtime_id, second.environment.runtime_id);
assert_ne!(first.environment.root, second.environment.root);
assert_ne!(
first.environment.server_data_dir,
second.environment.server_data_dir
);
assert_ne!(
first.environment.runtime_data_dir,
second.environment.runtime_data_dir
);
assert_ne!(
first.environment.artifacts_dir,
second.environment.artifacts_dir
);
assert_ne!(
first.environment.account_handle,
second.environment.account_handle
);
assert_ne!(first.runtime_public_key, second.runtime_public_key);
assert_ne!(first.workspace_public_key, second.workspace_public_key);
assert_ne!(
first.environment.server_port()?,
second.environment.server_port()?
);
assert_ne!(
first.environment.runtime_port()?,
second.environment.runtime_port()?
);
assert_ne!(
first.environment.server_pid(),
second.environment.server_pid()
);
assert_ne!(
first.environment.runtime_pid(),
second.environment.runtime_pid()
);
first
.environment
.assert_authentication_isolated_from(&second.environment)?;
second
.environment
.assert_authentication_isolated_from(&first.environment)?;
let (first_subscription, second_subscription) = std::thread::scope(|scope| {
// Deliberately reuse request identity in isolated stores, just like fixed IDs.
let a = scope.spawn(|| restore(&first, &fixture, "fixed-fixture-restore-concurrent"));
let b = restore(&second, &fixture, "fixed-fixture-restore-concurrent");
(
a.join()
.map_err(|_| failure("first restore panicked"))
.and_then(|result| result),
b,
)
});
let first_subscription = first_subscription?;
let mut second_subscription = second_subscription?;
let second_session_root = second
.environment
.runtime_data_dir
.join("runtime/workers")
.join(&fixture.manifest.worker_id)
.join("session");
let second_session = FixtureSource::capture(&second_session_root)?;
let first_root = first.environment.root.clone();
let first_artifacts = first.environment.artifacts_dir.clone();
stop_and_cleanup(first, vec![first_subscription], &fixture)?;
assert!(!first_root.exists());
assert!(first_artifacts.join("cleanup.json").is_file());
assert!(second.environment.root.is_dir());
assert!(second.environment.server_data_dir.is_dir());
assert!(second.environment.runtime_data_dir.is_dir());
assert!(
second
.environment
.artifacts_dir
.join("restored-session.json")
.is_file()
);
second.environment.assert_connected()?;
assert_single_state(&second.environment, &fixture, "idle")?;
second_subscription.assert_alive()?;
// A second live connection independently verifies content after peer cleanup.
let mut reconnected = WorkerSessionSubscription::connect(
&second.environment,
&fixture.manifest.worker_id,
&second.snapshot,
)?;
reconnected.assert_alive()?;
second_session.assert_unchanged()?;
stop_and_cleanup(second, vec![second_subscription, reconnected], &fixture)?;
Ok(())
})();
fixture.source.assert_unchanged()?;
result
}
fn restore(
installed: &Installed,
fixture: &Fixture,
request_id: &str,
) -> Result<WorkerSessionSubscription> {
phase(&installed.environment, "observe-stopped")?;
let stopped = assert_single_state(&installed.environment, fixture, "stopped")?;
let token = stopped
.restore_observation_token
.as_deref()
.filter(|token| !token.is_empty())
.ok_or_else(|| failure("stopped Worker did not expose a restore observation token"))?;
phase(&installed.environment, "restore")?;
let result: WorkerRestoreResponse = serde_json::from_value(
installed
.environment
.restore_worker(&fixture.manifest.worker_id, request_id, token)?,
)?;
if result.workspace_id != fixture.manifest.workspace_id
|| result.runtime_id != fixture.manifest.runtime_id
|| result.worker_id != fixture.manifest.worker_id
|| result.result.state != WorkerRestoreState::Accepted
{
return Err(failure(format!(
"restore did not commit accepted: {:?}",
result.result.state
)));
}
assert_single_state(&installed.environment, fixture, "idle")?;
let detail: RuntimeWorkerShowResponse = serde_json::from_value(
installed
.environment
.worker_detail(&fixture.manifest.worker_id)?,
)?;
if detail.worker.worker_id != fixture.manifest.worker_id
|| detail.worker.runtime_id != fixture.manifest.runtime_id
|| detail.worker.state != "idle"
|| detail
.worker
.worker_state
.as_ref()
.is_none_or(|state| state.state != protocol::WorkerState::Idle)
{
return Err(failure(
"restored Worker detail is not the single live idle execution",
));
}
phase(&installed.environment, "subscribe-live-session")?;
let mut subscription = WorkerSessionSubscription::connect(
&installed.environment,
&fixture.manifest.worker_id,
&installed.snapshot,
)?;
subscription.assert_alive()?;
phase(&installed.environment, "live-session-verified")?;
Ok(subscription)
}
fn assert_single_state(
environment: &WorkerE2eEnvironment,
fixture: &Fixture,
state: &str,
) -> Result<WorkerSummary> {
let deadline = Instant::now() + Duration::from_secs(15);
loop {
let list: WorkerListResponse = serde_json::from_value(environment.list_workers()?)?;
if list.workspace_id != fixture.manifest.workspace_id || list.items.len() > 1 {
return Err(failure(
"Workspace Worker list has wrong identity or duplicate Workers",
));
}
if let Some(worker) = list.items.into_iter().next() {
if worker.worker_id != fixture.manifest.worker_id
|| worker.runtime_id != fixture.manifest.runtime_id
{
return Err(failure("Workspace listed an unexpected Worker identity"));
}
if worker.state == state {
return Ok(worker);
}
}
if Instant::now() >= deadline {
return Err(failure(format!(
"Worker list did not become exactly one {state} Worker"
)));
}
std::thread::sleep(Duration::from_millis(50));
}
}
fn stop_and_cleanup(
installed: Installed,
subscriptions: Vec<WorkerSessionSubscription>,
fixture: &Fixture,
) -> Result<()> {
phase(&installed.environment, "stop")?;
let result: RuntimeWorkerLifecycleResult = serde_json::from_value(
installed
.environment
.stop_worker(&fixture.manifest.worker_id)?,
)?;
if result.state != WorkerOperationState::Accepted {
return Err(failure("Worker stop was not accepted"));
}
installed.environment.assert_connected()?;
for subscription in subscriptions {
subscription.expect_stopped()?;
// A transport close caused by Server/Runtime shutdown is not stop evidence.
installed.environment.assert_connected()?;
}
assert_single_state(&installed.environment, fixture, "stopped")?;
phase(&installed.environment, "shutdown-cleanup")?;
let report = installed.environment.cleanup()?;
if !report.cleanup_success || report.fixture_root_exists_after {
return Err(failure("environment cleanup failed"));
}
if !report.report_path.is_file() {
return Err(failure("cleanup evidence is missing"));
}
Ok(())
}
fn phase(environment: &WorkerE2eEnvironment, phase: &str) -> Result<()> {
fs::write(
environment.artifacts_dir.join("restore-phase.json"),
serde_json::to_vec_pretty(&serde_json::json!({"phase":phase}))?,
)?;
Ok(())
}
+269
View File
@@ -0,0 +1,269 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use protocol::{SessionMessageRole, SessionSnapshot, SessionSnapshotEntryData};
use serde::Deserialize;
use serde_json::{Value, json};
use worker_runtime::test_support::StoppedWorkerTemplate;
use yoi_e2e::{FixtureSource, HarnessError, Result};
pub const PROFILE: &str = "project:fixture-chat";
const ASSETS: [&str; 3] = [
"expected.json",
"worker-template.json",
"workspace-config.dcdl",
];
const MAX_ASSET_BYTES: u64 = 8 * 1024 * 1024;
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Manifest {
pub format_version: u32,
pub source_revision: String,
pub runtime_schema_version: u32,
pub session_schema_version: u32,
pub workspace_id: String,
pub runtime_id: String,
pub worker_id: String,
pub session_id: String,
pub segment_id: String,
pub model: String,
pub state: String,
pub acquisition: String,
pub source_file_sha256: BTreeMap<String, String>,
pub files: BTreeMap<String, Asset>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Asset {
pub bytes: u64,
pub sha256: String,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Expected {
source_revision: String,
model: String,
worker_id: String,
session_id: String,
rows: Vec<Value>,
}
pub struct Fixture {
pub root: PathBuf,
pub source: FixtureSource,
pub manifest: Manifest,
expected: Expected,
}
pub fn failure(message: impl Into<String>) -> HarnessError {
HarnessError::Protocol(message.into())
}
pub fn support<T, E: std::fmt::Display>(result: std::result::Result<T, E>) -> Result<T> {
result.map_err(|error| failure(error.to_string()))
}
impl Fixture {
pub fn load() -> Result<Self> {
// Candidate override is explicit and read-only. Missing assets are failures,
// never skips, and no test generates or repairs a fixture.
let root = std::env::var_os("YOI_E2E_RESTORE_FIXTURE")
.map(PathBuf::from)
.map(|path| {
if path.is_absolute() {
path
} else {
Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap()
.join(path)
}
})
.unwrap_or_else(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("fixtures/worker-codex-v1")
});
let manifest: Manifest =
serde_json::from_slice(&bounded_read(&root.join("manifest.json"))?)?;
if manifest.format_version != 1
|| manifest.state != "stopped"
|| manifest.runtime_schema_version == 0
|| manifest.session_schema_version == 0
|| manifest.acquisition.is_empty()
|| manifest.source_file_sha256.is_empty()
|| manifest.source_revision.len() != 40
|| !manifest
.source_revision
.bytes()
.all(|c| c.is_ascii_hexdigit())
{
return Err(failure("unsupported/incomplete fixture manifest"));
}
let names: BTreeSet<_> = manifest.files.keys().map(String::as_str).collect();
if names != BTreeSet::from(ASSETS) {
return Err(failure("unexpected fixture asset set"));
}
for name in ASSETS {
bounded_read(&root.join(name))?;
}
let source = FixtureSource::capture(&root)?;
if source.hashes().len() != 4 {
return Err(failure("fixture has unmanifested files"));
}
for (name, asset) in &manifest.files {
if asset.bytes != fs::metadata(root.join(name))?.len()
|| source.hashes().get(Path::new(name)) != Some(&format!("sha256:{}", asset.sha256))
{
return Err(failure(format!("fixture asset hash/size mismatch: {name}")));
}
}
let expected: Expected =
serde_json::from_slice(&bounded_read(&root.join("expected.json"))?)?;
if expected.source_revision != manifest.source_revision
|| expected.model != manifest.model
|| expected.worker_id != manifest.worker_id
|| expected.session_id != manifest.session_id
|| expected.rows.is_empty()
{
return Err(failure(
"expected conversation identity differs from manifest",
));
}
verify_builtin_revision(&manifest.source_revision)?;
let fixture = Self {
root,
source,
manifest,
expected,
};
fixture.template(&fixture.root)?;
Ok(fixture)
}
pub fn template(&self, copy: &Path) -> Result<StoppedWorkerTemplate> {
let bytes = bounded_read(&copy.join("worker-template.json"))?;
let template: StoppedWorkerTemplate = serde_json::from_slice(&bytes)?;
let shape: Value = serde_json::from_slice(&bytes)?;
let session_manifest_bytes: Vec<u8> =
serde_json::from_value(shape["session_manifest"].clone())?;
let segment_bytes: Vec<u8> = serde_json::from_value(shape["segment_jsonl"].clone())?;
for (name, bytes) in [
(
"session/session.json".to_owned(),
session_manifest_bytes.as_slice(),
),
(
format!("session/segments/{}.jsonl", self.manifest.segment_id),
segment_bytes.as_slice(),
),
] {
use sha2::{Digest, Sha256};
let digest = Sha256::digest(bytes)
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>();
if self.manifest.source_file_sha256.get(&name) != Some(&digest) {
return Err(failure(
"exported Session bytes differ from acquisition hash evidence",
));
}
}
let session_manifest: Value = serde_json::from_slice(&session_manifest_bytes)?;
let m = &self.manifest;
if template.workspace_id() != m.workspace_id
|| template.runtime_id() != m.runtime_id
|| template.worker_id().to_string() != m.worker_id
|| template.session_id().to_string() != m.session_id
|| template.segment_id().to_string() != m.segment_id
|| shape["runtime_schema_version"] != m.runtime_schema_version
|| session_manifest["schema_version"] != m.session_schema_version
{
return Err(failure("template schema/identity differs from manifest"));
}
Ok(template)
}
pub fn verify_conversation(&self, snapshot: &SessionSnapshot) -> Result<()> {
let mut rows = Vec::new();
for entry in &snapshot.entries {
let (role, parts) = match &entry.data {
SessionSnapshotEntryData::UserInput { segments } => {
("user", serde_json::to_value(segments)?)
}
SessionSnapshotEntryData::Message {
role: SessionMessageRole::Assistant,
content,
} => ("assistant", serde_json::to_value(content)?),
// Display-only accounting is not a conversation row. Its exact
// IDs, timestamp and values are still checked in the full WS snapshot.
SessionSnapshotEntryData::RunStats { .. } => continue,
_ => {
return Err(failure(
"unexpected non-conversation entry in full Session snapshot",
));
}
};
rows.push(json!({"entry_id":entry.entry_id,"timestamp":entry.timestamp,"role":role,"parts":parts}));
}
if rows != self.expected.rows {
return Err(failure(
"full public conversation rows differ from expected IDs/time/roles/text/order",
));
}
Ok(())
}
}
fn bounded_read(path: &Path) -> Result<Vec<u8>> {
let metadata = fs::symlink_metadata(path).map_err(|error| {
failure(format!(
"required fixture asset {}: {error}",
path.display()
))
})?;
if !metadata.is_file() || metadata.len() > MAX_ASSET_BYTES {
return Err(failure("fixture asset is not a bounded regular file"));
}
Ok(fs::read(path)?)
}
fn verify_builtin_revision(revision: &str) -> Result<()> {
let workspace = Path::new(env!("CARGO_MANIFEST_DIR"))
.parent()
.unwrap()
.parent()
.unwrap();
// Resolve the immutable commit before diff; bad revisions must not be treated
// as an empty diff. All resources are checked, including transitive imports.
let commit = Command::new("git")
.args(["cat-file", "-t", revision])
.current_dir(workspace)
.output()?;
if !commit.status.success() || commit.stdout != b"commit\n" {
return Err(failure(
"fixture source revision is unavailable for builtin verification",
));
}
let status = Command::new("git")
.args([
"diff",
"--quiet",
revision,
"--",
"resources",
"crates/builtin-source",
])
.current_dir(workspace)
.status()?;
if !status.success() {
return Err(failure(
"builtin/config resources differ from fixture source revision; provide a new version or frozen references",
));
}
Ok(())
}
+482
View File
@@ -0,0 +1,482 @@
use std::fs;
use std::net::TcpListener;
use std::path::Path;
use std::sync::Arc;
use config_source::{ConfigTreeChange, VirtualPath};
use protocol::SessionSnapshot;
use serde_json::Value;
use worker_runtime::FsRuntimeStoreOptions;
use worker_runtime::catalog::{
ConfigBundleRef, CreateWorkerRequest, ProfileSelector, ProfileSourceArchiveSource,
WorkspaceApiRef,
};
use yoi_e2e::{Result, WorkerE2eEnvironment};
use yoi_workspace_server::auth::{mint_secret, new_id, now_rfc3339, token_hash};
use yoi_workspace_server::config_source::{ConfigCommitRequest, WorkspaceConfigSchemaRegistry};
use yoi_workspace_server::hosts::{
WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest,
};
use yoi_workspace_server::profile_settings::{
ProfileConfigSchemaProvider, build_virtual_profile_config_bundle,
project_profiles_from_workspace_config,
};
use yoi_workspace_server::store::{
AccountRecord, ApiTokenRecord, UserRecord, WorkspaceBootstrapRecord,
WorkspaceRuntimeAuthenticationMode, WorkspaceRuntimeBinding,
};
use yoi_workspace_server::test_support::{FixedWorkerSeed, seed_fixed_worker};
use yoi_workspace_server::workspace_signing_identity::{
FsWorkspaceSigningMaterialStore, WorkspaceSigningIdentityService,
workspace_signing_material_root,
};
use yoi_workspace_server::{ControlPlaneStore, SqliteWorkspaceStore, WorkspaceRecord};
use super::fixture::{Fixture, PROFILE, failure, support};
pub struct Installed {
pub environment: WorkerE2eEnvironment,
pub snapshot: SessionSnapshot,
pub runtime_public_key: String,
pub workspace_public_key: String,
}
pub fn start(fixture: &Fixture) -> Result<Installed> {
let mut environment = WorkerE2eEnvironment::new()?;
super::phase(&environment, "fixture-copy-verify")?;
let copy = environment.root.join("provided-fixture");
fixture.source.copy_to(&copy)?;
fs::write(
environment.artifacts_dir.join("fixture-manifest.json"),
fs::read(copy.join("manifest.json"))?,
)?;
let template = fixture.template(&copy)?;
let runtime_identity =
environment.initialize_runtime_identity(fixture.manifest.runtime_id.clone())?;
let runtime_public_key = runtime_identity["public_key"]
.as_str()
.ok_or_else(|| failure("Runtime public bundle omitted key"))?
.to_owned();
// Reserve an environment-local placeholder endpoint only while the Runtime is
// offline. Its real :0 endpoint replaces this binding through public API CAS.
let reserved_endpoint = TcpListener::bind("127.0.0.1:0")?;
super::phase(
&environment,
"offline-backend-bootstrap-profile-worker-seed",
)?;
let bootstrap = bootstrap(
&environment,
fixture,
&copy,
&runtime_identity,
&format!("http://{}", reserved_endpoint.local_addr()?),
&template.worker_name(),
)?;
// Close the placeholder before Backend probes it: an accepting TCP listener
// with no HTTP service would stall discovery until its transport timeout.
drop(reserved_endpoint);
super::phase(&environment, "start-seeded-backend")?;
environment.start_seeded_backend(
fixture.manifest.workspace_id.clone(),
fixture.manifest.runtime_id.clone(),
bootstrap.token,
)?;
let mut snapshot = None;
super::phase(&environment, "initialize-runtime-trust")?;
environment.start_runtime_with_seed(|environment| {
super::phase(environment, "install-runtime-fixture-and-validate-session")?;
let options = FsRuntimeStoreOptions::new(environment.runtime_data_dir.join("runtime"))
.with_runtime_id(&fixture.manifest.runtime_id);
let request = CreateWorkerRequest {
worker_id: template.worker_id(),
create_fingerprint: bootstrap.create_fingerprint.clone(),
profile: ProfileSelector::Named(PROFILE.into()),
display_name: Some("Restore E2E fixture".into()),
profile_source: ProfileSourceArchiveSource::WorkspaceConfig {
archive: bootstrap.archive.clone(),
},
config_bundle: Some(bootstrap.config_bundle.clone()),
workspace_api: Some(WorkspaceApiRef {
workspace_id: fixture.manifest.workspace_id.clone(),
base_url: environment.backend_target()?.0.into(),
}),
initial_input: None,
workdir_attachment_requests: vec![],
workdir_attachments: vec![],
worker_observation_enabled: false,
worker_observation_grants: vec![],
memory_settings: Some(bootstrap.memory_settings.clone()),
subjektiv_attached: false,
backend_job: None,
};
support(template.install(&options, request, &bootstrap.manifest))?;
let sessions = support(session_store::WorkerSessionStore::open_read_only(
options
.root
.join("workers")
.join(template.worker_id().to_string())
.join("session"),
))?;
let (logs, _) = support(sessions.read_all_read_only_bounded(
template.session_id(),
template.segment_id(),
8 * 1024 * 1024,
))?;
let projected = session_store::project_session_snapshot(template.session_id(), &logs);
fixture.verify_conversation(&projected)?;
snapshot = Some(full_restored_snapshot(&sessions, &logs));
Ok(())
})?;
fixture.source.assert_unchanged()?;
Ok(Installed {
environment,
snapshot: snapshot.ok_or_else(|| failure("Runtime seed was not invoked"))?,
runtime_public_key,
workspace_public_key: bootstrap.workspace_public_key,
})
}
fn full_restored_snapshot(
store: &dyn session_store::Store,
logs: &[session_store::LogEntry],
) -> SessionSnapshot {
session_store::public_snapshot::project_current_session_snapshot_with_accounting(store, logs)
}
#[cfg(test)]
mod tests {
use super::*;
use protocol::{
Event, Greeting, InFlightSnapshot, InvokeKind, SessionSnapshotEntryData,
WorkerStateSnapshot, WorkerStatus,
};
use session_store::{
LogEntry, LoggedContentPart, LoggedHistoryEntry, LoggedItem, LoggedRole,
LoggedSessionHistoryEntryId, LoggedSessionHistoryMetadata, LoggedSessionHistoryOrigin,
};
#[test]
fn completed_run_full_snapshot_retains_invoke_and_usage_accounting() -> Result<()> {
// Projection regression only: no saved Worker or substitute E2E fixture.
let temp = tempfile::tempdir()?;
let store = support(session_store::WorkerSessionStore::new(temp.path()))?;
let session_id = session_store::new_session_id();
let logs = vec![
LogEntry::AnnotatedSegmentStart {
ts: 1,
session_id,
system_prompt: None,
config: Default::default(),
history: vec![],
forked_from: None,
compacted_from: None,
},
LogEntry::Invoke {
ts: 10,
trigger: InvokeKind::UserSend,
},
LogEntry::AnnotatedAssistantItem {
ts: 20,
entry: LoggedHistoryEntry {
item: LoggedItem::Message {
role: LoggedRole::Assistant,
content: vec![LoggedContentPart::Text {
text: "unit response".into(),
}],
},
metadata: LoggedSessionHistoryMetadata {
entry_id: LoggedSessionHistoryEntryId("unit-response".into()),
origin: LoggedSessionHistoryOrigin::LegacyUnknown,
derivation: None,
input_metadata: None,
},
},
},
LogEntry::LlmUsage {
ts: 30,
history_len: 1,
input_total_tokens: 100,
cache_read_tokens: 20,
cache_write_tokens: 0,
output_tokens: 5,
},
serde_json::from_value(serde_json::json!({"kind":"run_completed", "ts":40,
"interrupted":false,"result":"finished"}))?,
];
let conversation = session_store::project_session_snapshot(session_id, &logs);
let expected = full_restored_snapshot(&store, &logs);
assert_eq!(expected.entries.len(), conversation.entries.len() + 2);
assert!(matches!(
expected.entries[expected.entries.len() - 2].data,
SessionSnapshotEntryData::Invoke {
trigger: InvokeKind::UserSend
}
));
assert_eq!(expected.entries[expected.entries.len() - 2].timestamp, 10);
assert!(matches!(
expected.entries.last().unwrap().data,
SessionSnapshotEntryData::Usage {
input_tokens: 100,
cache_read_input_tokens: 20,
output_tokens: 5
}
));
assert_eq!(expected.entries.last().unwrap().timestamp, 30);
let live = session_store::public_snapshot::project_current_session_snapshot_with_accounting(
&store, &logs,
);
let event = Event::Snapshot {
session: live,
state: WorkerStateSnapshot::from(WorkerStatus::Idle),
in_flight: InFlightSnapshot::default(),
internal_workers: vec![],
greeting: Greeting {
worker_name: "unit".into(),
cwd: ".".into(),
provider: "unit".into(),
model: "unit".into(),
reasoning: None,
scope_summary: "none".into(),
tools: vec![],
context_window: 0,
context_tokens: 0,
context_usage: None,
},
};
let raw = serde_json::to_value(event)?;
yoi_e2e::validate_full_snapshot(&raw, &expected)?;
assert!(yoi_e2e::validate_full_snapshot(&raw, &conversation).is_err());
Ok(())
}
}
struct Bootstrap {
token: String,
workspace_public_key: String,
create_fingerprint: String,
archive: worker_runtime::profile_archive::ProfileSourceArchiveRef,
config_bundle: ConfigBundleRef,
manifest: manifest::WorkerManifest,
memory_settings: manifest::WorkspaceMemorySettingsSnapshot,
}
fn bootstrap(
environment: &WorkerE2eEnvironment,
fixture: &Fixture,
copy: &Path,
runtime_identity: &Value,
endpoint: &str,
worker_name: &str,
) -> Result<Bootstrap> {
fs::create_dir_all(&environment.server_data_dir)?;
let database = environment.server_data_dir.join("server.db");
if database.exists() {
return Err(failure("Backend seed refuses an existing database"));
}
let store = support(SqliteWorkspaceStore::open(&database))?;
let now = now_rfc3339();
let account_id = new_id("acct-user");
let user_id = new_id("user");
support(store.upsert_account(&AccountRecord {
account_id: account_id.clone(),
kind: "user".into(),
handle: environment.account_handle.clone(),
display_name: "Restore E2E owner".into(),
created_at: now.clone(),
updated_at: now.clone(),
}))?;
support(store.upsert_user(&UserRecord {
user_id: user_id.clone(),
account_id: account_id.clone(),
handle: environment.account_handle.clone(),
display_name: "Restore E2E owner".into(),
created_at: now.clone(),
updated_at: now.clone(),
}))?;
let token = mint_secret("yoi_api");
support(store.create_api_token(&ApiTokenRecord {
token_id: new_id("api-token"),
token_hash: token_hash(&token),
user_id,
label: "restore-e2e".into(),
created_at: now.clone(),
expires_at: None,
revoked_at: None,
last_used_at: None,
}))?;
let identities = WorkspaceSigningIdentityService::new(
Arc::new(store.clone()),
Arc::new(FsWorkspaceSigningMaterialStore::new(
workspace_signing_material_root(&database),
)),
);
let (identity, operation) = support(identities.prepare_workspace_creation(
"restore-e2e-bootstrap",
"restore-e2e-bootstrap",
&fixture.manifest.workspace_id,
&account_id,
))?;
let workspace_public_key = identity.public_key.clone();
support(store.create_workspace_bootstrap(
&WorkspaceBootstrapRecord {
operation_key: "restore-e2e-bootstrap".into(),
request_fingerprint: "restore-e2e-bootstrap".into(),
workspace: WorkspaceRecord {
workspace_id: fixture.manifest.workspace_id.clone(),
owner_account_id: account_id,
display_name: "Restore E2E".into(),
state: "active".into(),
created_at: now.clone(),
updated_at: now.clone(),
},
repository: None,
},
&identity,
&operation,
))?;
support(
store.upsert_workspace_runtime_binding(
WorkspaceRuntimeBinding {
workspace_id: fixture.manifest.workspace_id.clone(),
runtime_id: fixture.manifest.runtime_id.clone(),
display_name: format!("E2E {}", fixture.manifest.runtime_id),
base_url: endpoint.into(),
public_key: runtime_identity["public_key"]
.as_str()
.ok_or_else(|| failure("missing Runtime public key"))?
.into(),
public_key_fingerprint: String::new(),
binding_id: String::new(),
authentication_mode: WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity,
created_at: now.clone(),
updated_at: now.clone(),
revoked_at: None,
},
false,
),
)?;
let schema = support(
WorkspaceConfigSchemaRegistry::default()
.with_provider(Arc::new(ProfileConfigSchemaProvider))
.with_provider(Arc::new(
yoi_workspace_server::prompt_settings::PromptConfigSchemaProvider,
))
.with_provider(Arc::new(
yoi_workspace_server::runtime_settings::RuntimeConfigSchemaProvider,
))
.with_provider(Arc::new(
yoi_workspace_server::repository_access::RepositoryAccessConfigSchemaProvider,
))
.with_provider(Arc::new(
yoi_workspace_server::skills::SkillConfigSchemaProvider,
))
.compose(),
)?;
let state = support(store.ensure_workspace_config_materialized_with_schema(
&fixture.manifest.workspace_id,
&now,
schema.clone(),
))?;
let main = support(VirtualPath::parse("main.dcdl"))?;
let current = state
.snapshot
.get(&main)
.ok_or_else(|| failure("Workspace config omitted main.dcdl"))?;
let candidate = support(store.evaluate_workspace_config_candidate_with_schema(
&fixture.manifest.workspace_id,
&ConfigCommitRequest {
base_digest: state.snapshot.digest.clone(),
changes: vec![ConfigTreeChange::Update {
path: main.clone(),
expected_digest: current.content_digest.clone(),
content: fs::read_to_string(copy.join("workspace-config.dcdl"))?,
}],
entrypoints: vec![main],
},
schema,
))?;
let state = support(
store.commit_evaluated_workspace_config(&fixture.manifest.workspace_id, &candidate),
)?;
let projection = support(project_profiles_from_workspace_config(
&fixture.manifest.workspace_id,
&state,
))?;
let bundle = support(build_virtual_profile_config_bundle(
&projection,
&state,
&fixture.manifest.workspace_id,
&now,
PROFILE,
))?
.ok_or_else(|| failure("fixture Profile is not registered"))?;
let archive = bundle
.profile_source_archive
.as_ref()
.ok_or_else(|| failure("fixture Profile archive is absent"))?;
let mut resolved = support(
support(archive.verify())?.resolve_profile(
PROFILE,
&environment
.runtime_data_dir
.join("runtime/workers")
.join(&fixture.manifest.worker_id),
worker_name,
),
)?;
if resolved.model.ref_.as_deref() != Some(fixture.manifest.model.as_str()) {
return Err(failure("resolved Profile changed the supplied model"));
}
let request = WorkerSpawnRequest {
intent: WorkerSpawnIntent::WorkspaceCoding,
requested_worker_name: Some("Restore E2E fixture".into()),
singleton_key: None,
acceptance: WorkerSpawnAcceptanceRequirement::SocketReady,
profile: ProfileSelector::Named(PROFILE.into()),
ticket_assignment: None,
initial_submit: vec![],
workdir_attachment_requests: vec![],
resolved_workdir_attachment_requests: vec![],
resolved_workdir_attachments: vec![],
resolved_config_bundle: None,
resolved_workspace_api: None,
resolved_memory_settings: None,
resolved_subjektiv_attached: false,
resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: vec![],
resolved_control_operation: None,
resolved_execution_grant: None,
};
let reservation = support(seed_fixed_worker(
&store,
FixedWorkerSeed {
workspace_id: &fixture.manifest.workspace_id,
runtime_id: &fixture.manifest.runtime_id,
worker_id: support(fixture.manifest.worker_id.parse())?,
allocation_key: "restore-e2e-worker",
request: &request,
},
))?;
if resolved.feature.memory.profile.enabled {
support(
resolved
.feature
.memory
.bind_workspace_settings(reservation.memory_settings.clone()),
)?;
}
Ok(Bootstrap {
manifest: resolved,
memory_settings: reservation.memory_settings,
token,
workspace_public_key,
create_fingerprint: reservation.create_fingerprint,
archive: archive.reference.clone(),
config_bundle: ConfigBundleRef {
id: bundle.metadata.id,
digest: bundle.metadata.digest,
},
})
}