fix: preserve uncertain config effects and exercise owner adoption fixtures
Select identifier-free startup summaries from T-840 1d8c205b, without adopting that whole branch or its review status. Keep physical identity guards, migration rollback assertions and old-writer fencing intact.
This commit is contained in:
@@ -2347,8 +2347,8 @@ impl RuntimeHttpRestError {
|
|||||||
RuntimeError::WorkerExecutionRejected { result, .. } => startup_transfer(result),
|
RuntimeError::WorkerExecutionRejected { result, .. } => startup_transfer(result),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
let message = if startup.is_some() {
|
let message = if let Some(startup) = &startup {
|
||||||
"Worker startup failed".to_owned()
|
worker::startup_failure::internal_startup_failure_summary(&startup.primary).to_owned()
|
||||||
} else {
|
} else {
|
||||||
error.to_string()
|
error.to_string()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -120,6 +120,20 @@ pub fn reproject_internal_startup_failure(
|
|||||||
StartupFailureAttachment::from_projected(&projected)
|
StartupFailureAttachment::from_projected(&projected)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fixed, identifier-free summary for an already authenticated internal caller.
|
||||||
|
/// Keep the required operation actionable without exposing the source's Display.
|
||||||
|
/// This neither projects a Browser cause nor chooses an HTTP status.
|
||||||
|
pub fn internal_startup_failure_summary(failure: &StartupFailureAttachment) -> &'static str {
|
||||||
|
if failure
|
||||||
|
.project_for::<InternalTransfer, protocol::session_failure::SessionRegistrationCause>(&())
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
"Worker startup failed while registering Session (session.record)"
|
||||||
|
} else {
|
||||||
|
"Worker startup failed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Dispatch by the domain attested in process, not by parsing attachment fields.
|
/// Dispatch by the domain attested in process, not by parsing attachment fields.
|
||||||
pub(crate) fn persist_binding_startup_failure<S: session_store::WorkerMetadataStore>(
|
pub(crate) fn persist_binding_startup_failure<S: session_store::WorkerMetadataStore>(
|
||||||
store: &S,
|
store: &S,
|
||||||
@@ -249,6 +263,36 @@ mod tests {
|
|||||||
WorkerStoreError,
|
WorkerStoreError,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn internal_summary_retains_only_attested_required_operation() {
|
||||||
|
use protocol::session_failure::{SessionPrincipal, SessionRegistrationCause};
|
||||||
|
let session = protocol::session_failure::session_registration_attachment(
|
||||||
|
SessionRegistrationCause::denied(SessionPrincipal::Subject),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
internal_startup_failure_summary(&session),
|
||||||
|
"Worker startup failed while registering Session (session.record)"
|
||||||
|
);
|
||||||
|
let runtime = runtime_startup_attachment(RuntimeStartupCause {
|
||||||
|
stage: StartupStage::Feature,
|
||||||
|
reason: RuntimeStartupReason::FeatureFailed,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
internal_startup_failure_summary(&runtime),
|
||||||
|
"Worker startup failed"
|
||||||
|
);
|
||||||
|
let corrupt = serde_json::from_value(serde_json::json!(
|
||||||
|
"session.record denied TEST_ONLY_CREDENTIAL /private/path raw_http_body"
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
internal_startup_failure_summary(&corrupt),
|
||||||
|
"Worker startup failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
struct LostSaveResponse(FsWorkerStore);
|
struct LostSaveResponse(FsWorkerStore);
|
||||||
impl WorkerMetadataStore for LostSaveResponse {
|
impl WorkerMetadataStore for LostSaveResponse {
|
||||||
fn write(&self, metadata: &WorkerMetadata) -> Result<(), WorkerStoreError> {
|
fn write(&self, metadata: &WorkerMetadata) -> Result<(), WorkerStoreError> {
|
||||||
|
|||||||
@@ -6772,8 +6772,11 @@ fn embedded_runtime_diagnostic(error: &EmbeddedRuntimeError) -> RuntimeDiagnosti
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
if let EmbeddedRuntimeError::WorkerExecutionRejected { result, .. } = error {
|
if let EmbeddedRuntimeError::WorkerExecutionRejected { result, .. } = error {
|
||||||
if result.startup_failure.is_some() {
|
if let Some(primary) = &result.startup_failure {
|
||||||
projection.message = "Embedded Runtime Worker startup failed".into();
|
projection.message = format!(
|
||||||
|
"Embedded Runtime: {}",
|
||||||
|
worker::startup_failure::internal_startup_failure_summary(primary)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
projection.startup = result.startup_failure.as_ref().and_then(|primary| {
|
projection.startup = result.startup_failure.as_ref().and_then(|primary| {
|
||||||
internal_startup_transfer(runtime_api::WorkerStartupFailure {
|
internal_startup_transfer(runtime_api::WorkerStartupFailure {
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ fn plugin_catalog_migration_matches_fresh_schema_from_oldest_retained_and_105()
|
|||||||
let fresh = SqliteWorkspaceStore::open_for_test(dir.path().join("fresh.db")).unwrap();
|
let fresh = SqliteWorkspaceStore::open_for_test(dir.path().join("fresh.db")).unwrap();
|
||||||
let canonical = fresh.with_conn(|c| Ok(catalog_schema(c))).unwrap();
|
let canonical = fresh.with_conn(|c| Ok(catalog_schema(c))).unwrap();
|
||||||
for version in [50, 105] {
|
for version in [50, 105] {
|
||||||
let path = dir.path().join(format!("v{version}.db"));
|
let root = dir.path().join(format!("v{version}"));
|
||||||
|
std::fs::create_dir(&root).unwrap();
|
||||||
|
let path = root.join("workspace.db");
|
||||||
if version == 50 {
|
if version == 50 {
|
||||||
tests::prepare_schema_v50(&path, Some("workspace-a"));
|
tests::prepare_schema_v50(&path, Some("workspace-a"));
|
||||||
} else {
|
} else {
|
||||||
@@ -30,7 +32,7 @@ fn plugin_catalog_migration_matches_fresh_schema_from_oldest_retained_and_105()
|
|||||||
// Plugin catalog remains migration 106 even when startup applies later steps.
|
// Plugin catalog remains migration 106 even when startup applies later steps.
|
||||||
assert_eq!(after.iter().filter(|(version, _)| *version == 106).cloned().collect::<Vec<_>>(),
|
assert_eq!(after.iter().filter(|(version, _)| *version == 106).cloned().collect::<Vec<_>>(),
|
||||||
vec![(106, crate::plugin_catalog::MIGRATION_NAME.into())]);
|
vec![(106, crate::plugin_catalog::MIGRATION_NAME.into())]);
|
||||||
assert_eq!(after.last().unwrap(), &(113, "Ticket work-decomposition parent links".into()));
|
assert_eq!(after.last().unwrap(), &(122, crate::config_adoption::MIGRATION_NAME.into()));
|
||||||
assert_eq!(current_schema_version(c)?, LATEST_SCHEMA_VERSION);
|
assert_eq!(current_schema_version(c)?, LATEST_SCHEMA_VERSION);
|
||||||
assert_eq!(c.query_row("SELECT display_name FROM workspaces WHERE workspace_id='plugin-workspace'",[],|r|r.get::<_,String>(0))?,"Plugins");
|
assert_eq!(c.query_row("SELECT display_name FROM workspaces WHERE workspace_id='plugin-workspace'",[],|r|r.get::<_,String>(0))?,"Plugins");
|
||||||
assert_eq!(c.query_row("SELECT count(*) FROM pragma_foreign_key_check",[],|r|r.get::<_,i64>(0))?,0);
|
assert_eq!(c.query_row("SELECT count(*) FROM pragma_foreign_key_check",[],|r|r.get::<_,i64>(0))?,0);
|
||||||
@@ -83,6 +85,7 @@ fn plugin_catalog_migration_marker_failure_rolls_back_then_retries_without_chang
|
|||||||
));
|
));
|
||||||
expected.push((112, "Ticket Service outbox and fenced consumers".into()));
|
expected.push((112, "Ticket Service outbox and fenced consumers".into()));
|
||||||
expected.push((113, "Ticket work-decomposition parent links".into()));
|
expected.push((113, "Ticket work-decomposition parent links".into()));
|
||||||
|
expected.push((122, crate::config_adoption::MIGRATION_NAME.into()));
|
||||||
assert_eq!(workspace_schema_migration_history(c)?, expected);
|
assert_eq!(workspace_schema_migration_history(c)?, expected);
|
||||||
crate::plugin_catalog::verify_with_data(c)?;
|
crate::plugin_catalog::verify_with_data(c)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -101,7 +104,13 @@ fn plugin_catalog_startup_rejects_drifted_reference_fence() {
|
|||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
drop(store);
|
drop(store);
|
||||||
assert!(
|
match SqliteWorkspaceStore::open_for_test(path) {
|
||||||
matches!(SqliteWorkspaceStore::open_for_test(path),Err(Error::Store(message)) if message.contains("Plugin catalog storage schema has drifted"))
|
Err(Error::Store(message)) => assert!(
|
||||||
);
|
message.contains("Server storage format: physical Base mismatch")
|
||||||
|
&& message.contains("plugin_reference_live"),
|
||||||
|
"{message}"
|
||||||
|
),
|
||||||
|
Err(error) => panic!("unexpected rejection: {error}"),
|
||||||
|
Ok(_) => panic!("startup accepted a missing reference fence"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ fn plugin_plan_migration_marker_failure_preserves_catalog_then_retries_and_match
|
|||||||
));
|
));
|
||||||
expected.push((112, "Ticket Service outbox and fenced consumers".into()));
|
expected.push((112, "Ticket Service outbox and fenced consumers".into()));
|
||||||
expected.push((113, "Ticket work-decomposition parent links".into()));
|
expected.push((113, "Ticket work-decomposition parent links".into()));
|
||||||
|
expected.push((122, crate::config_adoption::MIGRATION_NAME.into()));
|
||||||
assert_eq!(workspace_schema_migration_history(conn)?, expected);
|
assert_eq!(workspace_schema_migration_history(conn)?, expected);
|
||||||
subject_assignment_migration::verify(conn)?;
|
subject_assignment_migration::verify(conn)?;
|
||||||
crate::plugin_plan::verify_plan_schema(conn)?;
|
crate::plugin_plan::verify_plan_schema(conn)?;
|
||||||
|
|||||||
@@ -47,33 +47,63 @@ fn source(value: &str) -> String {
|
|||||||
async fn value_profiles_builtin_http_config_tree_is_read_only_and_never_falls_back() {
|
async fn value_profiles_builtin_http_config_tree_is_read_only_and_never_falls_back() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let api = test_api(dir.path()).await;
|
let api = test_api(dir.path()).await;
|
||||||
let token = seed_test_api_token(api.store.as_ref(), TEST_WORKSPACE_ID);
|
let token = seed_test_workspace_owner_token(api.store.as_ref(), "builtin-profile-owner").await;
|
||||||
let app = build_router(api.clone());
|
let catalog = WorkspaceServerApi::new(api.config.clone(), api.store.clone());
|
||||||
|
catalog
|
||||||
|
.apis
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.insert(TEST_WORKSPACE_ID.into(), api.clone());
|
||||||
|
catalog
|
||||||
|
.routers
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.insert(TEST_WORKSPACE_ID.into(), build_inner_router(api.clone()));
|
||||||
|
let app = workspace_server_router(catalog);
|
||||||
let tree = format!("/api/w/{TEST_WORKSPACE_ID}/config/source-tree");
|
let tree = format!("/api/w/{TEST_WORKSPACE_ID}/config/source-tree");
|
||||||
let commit = format!("{tree}/commit");
|
let adoption_path = format!("/api/w/{TEST_WORKSPACE_ID}/config/adoption");
|
||||||
let initial = get_json_authenticated(app.clone(), &tree, &token).await;
|
let commit = format!("{adoption_path}/commit");
|
||||||
let saved = request_json_authenticated(
|
let current = get_json_authenticated(app.clone(), &adoption_path, &token).await;
|
||||||
|
let initial = current["state"].clone();
|
||||||
|
let adoption = |current: &Value, id: &str, source: Value| {
|
||||||
|
json!({
|
||||||
|
"operation_id": id, "expected_head": current["history_head"],
|
||||||
|
"expected_catalog_digest": current["catalog_digest"], "expected_legacy_export": current["expected_legacy_export"],
|
||||||
|
"source": source,
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let source_request = json!({
|
||||||
|
"base_digest": current["state"]["snapshot"]["digest"],
|
||||||
|
"entrypoints": current["state"]["contract"]["entrypoints"],
|
||||||
|
"changes": [
|
||||||
|
{ "kind": "update", "path": "main.dcdl",
|
||||||
|
"expected_digest": current["state"]["snapshot"]["entries"]["main.dcdl"]["content_digest"],
|
||||||
|
"content": source(r#"import "$builtin/profiles/companion.dcdl""#) },
|
||||||
|
// A Workspace file at the matching suffix must not rescue an unknown builtin.
|
||||||
|
{ "kind": "create", "path": "profiles/private.dcdl", "content_type": "decodal", "content": PROFILE }
|
||||||
|
],
|
||||||
|
});
|
||||||
|
let save = adoption(¤t, "builtin-profile-adopt", source_request);
|
||||||
|
let preview = request_json_authenticated(
|
||||||
|
app.clone(),
|
||||||
|
"POST",
|
||||||
|
&format!("{adoption_path}/preview"),
|
||||||
|
Some(save.clone()),
|
||||||
|
&token,
|
||||||
|
StatusCode::OK,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let receipt = request_json_authenticated(
|
||||||
app.clone(),
|
app.clone(),
|
||||||
"POST",
|
"POST",
|
||||||
&commit,
|
&commit,
|
||||||
Some(
|
Some(save),
|
||||||
serde_json::to_value(request(
|
|
||||||
&api,
|
|
||||||
&source(r#"import "$builtin/profiles/companion.dcdl""#),
|
|
||||||
vec![config_source::ConfigTreeChange::Create {
|
|
||||||
// An ordinary Workspace file at the matching suffix must not rescue
|
|
||||||
// an unknown builtin import.
|
|
||||||
path: config_source::VirtualPath::parse("profiles/private.dcdl").unwrap(),
|
|
||||||
content_type: config_source::ConfigContentType::Decodal,
|
|
||||||
content: PROFILE.into(),
|
|
||||||
}],
|
|
||||||
))
|
|
||||||
.unwrap(),
|
|
||||||
),
|
|
||||||
&token,
|
&token,
|
||||||
StatusCode::CREATED,
|
StatusCode::OK,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
assert_eq!(receipt["state"], preview["state"]);
|
||||||
|
let saved = receipt["state"].clone();
|
||||||
assert_ne!(saved["snapshot"]["digest"], initial["snapshot"]["digest"]);
|
assert_ne!(saved["snapshot"]["digest"], initial["snapshot"]["digest"]);
|
||||||
let listed = get_json_authenticated(app.clone(), &tree, &token).await;
|
let listed = get_json_authenticated(app.clone(), &tree, &token).await;
|
||||||
assert_eq!(listed, saved);
|
assert_eq!(listed, saved);
|
||||||
@@ -107,22 +137,23 @@ async fn value_profiles_builtin_http_config_tree_is_read_only_and_never_falls_ba
|
|||||||
"base_digest": listed["snapshot"]["digest"],
|
"base_digest": listed["snapshot"]["digest"],
|
||||||
"entrypoints": listed["contract"]["entrypoints"],
|
"entrypoints": listed["contract"]["entrypoints"],
|
||||||
});
|
});
|
||||||
|
let current = get_json_authenticated(app.clone(), &adoption_path, &token).await;
|
||||||
let reserved = "$builtin/profiles/companion.dcdl";
|
let reserved = "$builtin/profiles/companion.dcdl";
|
||||||
for change in [
|
for (index, change) in [
|
||||||
json!({ "kind": "create", "path": reserved, "content_type": "decodal", "content": PROFILE }),
|
json!({ "kind": "create", "path": reserved, "content_type": "decodal", "content": PROFILE }),
|
||||||
json!({ "kind": "create", "path": "$builtin", "content_type": "text", "content": "shadow" }),
|
json!({ "kind": "create", "path": "$builtin", "content_type": "text", "content": "shadow" }),
|
||||||
json!({ "kind": "update", "path": reserved, "expected_digest": "not-a-workspace-entry", "content": PROFILE }),
|
json!({ "kind": "update", "path": reserved, "expected_digest": "not-a-workspace-entry", "content": PROFILE }),
|
||||||
json!({ "kind": "delete", "path": reserved, "expected_digest": "not-a-workspace-entry" }),
|
json!({ "kind": "delete", "path": reserved, "expected_digest": "not-a-workspace-entry" }),
|
||||||
json!({ "kind": "rename", "from": "profiles/private.dcdl", "to": reserved, "expected_digest": shadow["content_digest"] }),
|
json!({ "kind": "rename", "from": "profiles/private.dcdl", "to": reserved, "expected_digest": shadow["content_digest"] }),
|
||||||
json!({ "kind": "rename", "from": reserved, "to": "profiles/copy.dcdl", "expected_digest": "not-a-workspace-entry" }),
|
json!({ "kind": "rename", "from": reserved, "to": "profiles/copy.dcdl", "expected_digest": "not-a-workspace-entry" }),
|
||||||
] {
|
].into_iter().enumerate() {
|
||||||
let mut body = base.clone();
|
let mut body = base.clone();
|
||||||
body["changes"] = json!([change]);
|
body["changes"] = json!([change]);
|
||||||
let error = request_json_authenticated(
|
let error = request_json_authenticated(
|
||||||
app.clone(),
|
app.clone(),
|
||||||
"POST",
|
"POST",
|
||||||
&commit,
|
&commit,
|
||||||
Some(body),
|
Some(adoption(¤t, &format!("reserved-builtin-{index}"), body)),
|
||||||
&token,
|
&token,
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
)
|
)
|
||||||
@@ -148,7 +179,7 @@ async fn value_profiles_builtin_http_config_tree_is_read_only_and_never_falls_ba
|
|||||||
app.clone(),
|
app.clone(),
|
||||||
"POST",
|
"POST",
|
||||||
&commit,
|
&commit,
|
||||||
Some(body),
|
Some(adoption(¤t, "unknown-builtin-import", body)),
|
||||||
&token,
|
&token,
|
||||||
StatusCode::BAD_REQUEST,
|
StatusCode::BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -633,9 +633,12 @@ pub(super) async fn commit(
|
|||||||
&persistence_api.config.workspace_id,
|
&persistence_api.config.workspace_id,
|
||||||
&candidate,
|
&candidate,
|
||||||
).map_err(|_| unknown())?;
|
).map_err(|_| unknown())?;
|
||||||
|
// Repository projection may already have auxiliary secret
|
||||||
|
// effects. A subsequent save error cannot promise that the
|
||||||
|
// whole operation had no durable effect, even on SQL rollback.
|
||||||
persistence_api.config_store.commit_evaluated_workspace_config(
|
persistence_api.config_store.commit_evaluated_workspace_config(
|
||||||
&persistence_api.config.workspace_id, &candidate,
|
&persistence_api.config.workspace_id, &candidate,
|
||||||
).map_err(pre_error)
|
).map_err(|_| unknown())
|
||||||
})())
|
})())
|
||||||
}).map_err(|error| match error {
|
}).map_err(|error| match error {
|
||||||
subjektiv::SubjektivError::PermissionAuthoritySealed => fail(409, "owner_adoption_required"),
|
subjektiv::SubjektivError::PermissionAuthoritySealed => fail(409, "owner_adoption_required"),
|
||||||
|
|||||||
@@ -390,8 +390,10 @@ mod workspace_config_integration {
|
|||||||
"the granted WIP path remains available"
|
"the granted WIP path remains available"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Real API-token authentication still permits the normal UI editor.
|
// Real owner API-token authentication still permits the normal editor,
|
||||||
let token = seed_test_api_token(api.store.as_ref(), TEST_WORKSPACE_ID);
|
// but only through the head/request-bound adoption boundary.
|
||||||
|
let token =
|
||||||
|
seed_test_workspace_owner_token(api.store.as_ref(), "logical-editor-owner").await;
|
||||||
for (method, path) in &routes {
|
for (method, path) in &routes {
|
||||||
if *method == "GET" {
|
if *method == "GET" {
|
||||||
request_json_authenticated(
|
request_json_authenticated(
|
||||||
@@ -409,11 +411,52 @@ mod workspace_config_integration {
|
|||||||
build_router(api.clone()),
|
build_router(api.clone()),
|
||||||
"POST",
|
"POST",
|
||||||
&format!("{tree}/commit"),
|
&format!("{tree}/commit"),
|
||||||
Some(serde_json::to_value(change).unwrap()),
|
Some(serde_json::to_value(&change).unwrap()),
|
||||||
&token,
|
&token,
|
||||||
StatusCode::CREATED,
|
StatusCode::CONFLICT,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
let catalog = WorkspaceServerApi::new(api.config.clone(), api.store.clone());
|
||||||
|
catalog
|
||||||
|
.apis
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.insert(TEST_WORKSPACE_ID.into(), api.clone());
|
||||||
|
catalog
|
||||||
|
.routers
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.insert(TEST_WORKSPACE_ID.into(), build_inner_router(api.clone()));
|
||||||
|
let editor = workspace_server_router(catalog);
|
||||||
|
let adoption_path = format!("/api/w/{TEST_WORKSPACE_ID}/config/adoption");
|
||||||
|
let current = get_json_authenticated(editor.clone(), &adoption_path, &token).await;
|
||||||
|
let adoption = json!({
|
||||||
|
"operation_id": "logical-editor-owner-adoption",
|
||||||
|
"expected_head": current["history_head"],
|
||||||
|
"expected_catalog_digest": current["catalog_digest"],
|
||||||
|
"expected_legacy_export": current["expected_legacy_export"],
|
||||||
|
"source": {"base_digest": current["state"]["snapshot"]["digest"],
|
||||||
|
"entrypoints": current["state"]["contract"]["entrypoints"], "changes": change.changes}
|
||||||
|
});
|
||||||
|
let preview = request_json_authenticated(
|
||||||
|
editor.clone(),
|
||||||
|
"POST",
|
||||||
|
&format!("{adoption_path}/preview"),
|
||||||
|
Some(adoption.clone()),
|
||||||
|
&token,
|
||||||
|
StatusCode::OK,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let receipt = request_json_authenticated(
|
||||||
|
editor,
|
||||||
|
"POST",
|
||||||
|
&format!("{adoption_path}/commit"),
|
||||||
|
Some(adoption),
|
||||||
|
&token,
|
||||||
|
StatusCode::OK,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(receipt["state"], preview["state"]);
|
||||||
let saved = api
|
let saved = api
|
||||||
.config_store
|
.config_store
|
||||||
.load_workspace_config(TEST_WORKSPACE_ID)
|
.load_workspace_config(TEST_WORKSPACE_ID)
|
||||||
|
|||||||
Reference in New Issue
Block a user