fix: preserve string and table Profile scope authoring (T-710)
Resolve MR sequence 5 profile_authoring_rejects_valid_string_scope_intents. Decodal 0.4 has no union constraint: retain an opaque scope range with non-materialized table completion hints so string intents remain valid. Keep shared Backend semantic validation authoritative for scope forms/types/unknown keys without changing persisted values or weakening worker/feature editor diagnostics.
Prove actual-provider native/WASM scalar/table, inline/import/patch, nested intent/deny_write/symlink_policy completion and evaluation invariance. Add real save/projection/Runtime scope equivalence and invalid-scope save non-loss coverage. Regenerated WASM artifact is byte-identical; the changed shape is transported via the fingerprint-bound Backend schema metadata.
Validation: root cargo check; config-source39; value_profiles4; unfiltered server643+11; worker-runtime340+17+17; server-api --features typescript77; wasm32 release/wasm-bindgen0.2.117/Binaryen132 -Oz; Deno39; deno task check0/0; fmt/diff checks. Logs target/t710-scope-{check,server,server-api,runtime}.log. No target/ref integration changes.
This commit is contained in:
@@ -2555,9 +2555,8 @@ mod tests {
|
||||
assert_eq!(environment.evaluate_contract(&contract).unwrap(), evaluated);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_profile_schema_diagnoses_nested_values_without_materializing_omissions() {
|
||||
let schema = WorkspaceConfigSchemaBundle::compose([ConfigSchemaContribution::new(
|
||||
fn actual_profile_schema() -> WorkspaceConfigSchemaBundle {
|
||||
WorkspaceConfigSchemaBundle::compose([ConfigSchemaContribution::new(
|
||||
"builtin:profile",
|
||||
"profile",
|
||||
"2",
|
||||
@@ -2567,7 +2566,12 @@ mod tests {
|
||||
.with_authoring_source(include_str!(
|
||||
"../../../resources/config-schema/profile-authoring.dcdl"
|
||||
))])
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_profile_schema_diagnoses_nested_values_without_materializing_omissions() {
|
||||
let schema = actual_profile_schema();
|
||||
let source = "{ profile = { entries = [{ selector = \"project:alpha\"; profile = {}; }]; }; } as WorkspaceConfigSchema";
|
||||
let snapshot = ConfigTreeSnapshot::from_entries(
|
||||
8,
|
||||
@@ -2659,6 +2663,81 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn actual_profile_schema_preserves_scalar_and_table_scope_forms_and_completion() {
|
||||
for (recipe, expected) in [
|
||||
(
|
||||
r#"{ scope = "workspace_read"; delegation_scope = "workspace_write"; }"#,
|
||||
serde_json::json!({"scope": "workspace_read", "delegation_scope": "workspace_write"}),
|
||||
),
|
||||
(
|
||||
r#"{ scope = "workspace_write"; delegation_scope = "workspace_read"; }"#,
|
||||
serde_json::json!({"scope": "workspace_write", "delegation_scope": "workspace_read"}),
|
||||
),
|
||||
(
|
||||
r#"{ scope = { intent = "workspace_read"; }; delegation_scope = { intent = "workspace_write"; deny_write = ["private"]; symlink_policy = "resolved"; }; }"#,
|
||||
serde_json::json!({"scope": {"intent": "workspace_read"}, "delegation_scope": {"intent": "workspace_write", "deny_write": ["private"], "symlink_policy": "resolved"}}),
|
||||
),
|
||||
] {
|
||||
for form in [
|
||||
recipe.to_owned(),
|
||||
r#"import "./scoped.dcdl""#.into(),
|
||||
format!(r#"(import "./scoped.dcdl") // {{ description = "patched"; }}"#),
|
||||
] {
|
||||
let source = format!(
|
||||
r#"{{ profile.entries = [{{ selector = "project:scoped"; profile = {form}; }}]; }} as WorkspaceConfigSchema"#
|
||||
);
|
||||
let snapshot = ConfigTreeSnapshot::from_entries(
|
||||
9,
|
||||
[entry("main.dcdl", &source), entry("scoped.dcdl", recipe)],
|
||||
)
|
||||
.unwrap();
|
||||
let schema = actual_profile_schema();
|
||||
let environment =
|
||||
SnapshotEnvironment::new(snapshot).with_schema_bundle(schema.clone());
|
||||
let contract =
|
||||
ToolchainContract::with_schema_bundle(1, vec![path("main.dcdl")], 1, schema);
|
||||
let before = environment.evaluate_contract(&contract).unwrap();
|
||||
assert!(
|
||||
environment.analyze(&path("main.dcdl"), None).is_empty(),
|
||||
"{source}"
|
||||
);
|
||||
let mut expected = expected.clone();
|
||||
if form.contains("patched") {
|
||||
expected["description"] = serde_json::json!("patched");
|
||||
}
|
||||
assert_eq!(
|
||||
before.projections[0]
|
||||
.data_json
|
||||
.pointer("/profile/entries/0/profile"),
|
||||
Some(&expected)
|
||||
);
|
||||
for field in ["scope", "delegation_scope"] {
|
||||
for (prefix, label) in [
|
||||
("int", "intent"),
|
||||
("deny", "deny_write"),
|
||||
("sym", "symlink_policy"),
|
||||
] {
|
||||
let marked = format!(
|
||||
"{{ profile.entries = [{{ profile.{field} = {{ {prefix}| }}; }}]; }} as WorkspaceConfigSchema"
|
||||
);
|
||||
let cursor = marked.find('|').unwrap();
|
||||
let source = marked.replace('|', "");
|
||||
let result = environment
|
||||
.complete_config(&path("main.dcdl"), &source, cursor, true)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
result.items.iter().any(|item| item.label == label),
|
||||
"{source}: {result:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert_eq!(environment.evaluate_contract(&contract).unwrap(), before);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completion_authoring_sources_compose_with_validation_source_fallbacks() {
|
||||
let schema = WorkspaceConfigSchemaBundle::compose([
|
||||
|
||||
@@ -228,6 +228,81 @@ async fn value_profiles_save_project_and_runtime_consume_the_same_revision() {
|
||||
assert!(restored.feature.task.enabled);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn value_profiles_scalar_and_table_scopes_analyze_save_and_resolve_equally() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let api = test_api(dir.path()).await;
|
||||
let scalar = format!(
|
||||
r#"{PROFILE} // {{ scope = "workspace_read"; delegation_scope = "workspace_write"; }}"#
|
||||
);
|
||||
let table = format!(
|
||||
r#"{PROFILE} // {{ scope = {{ intent = "workspace_read"; }}; delegation_scope = {{ intent = "workspace_write"; }}; }}"#
|
||||
);
|
||||
let mut scopes = Vec::new();
|
||||
for (index, form) in [scalar.clone(), table, r#"import "./scoped.dcdl""#.into()]
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let extra = if index == 0 {
|
||||
vec![config_source::ConfigTreeChange::Create {
|
||||
path: config_source::VirtualPath::parse("scoped.dcdl").unwrap(),
|
||||
content_type: config_source::ConfigContentType::Decodal,
|
||||
content: scalar.clone(),
|
||||
}]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let authored = format!("{} as WorkspaceConfigSchema", source(form));
|
||||
let state =
|
||||
commit_workspace_config_tree(&api, TEST_WORKSPACE_ID, &request(&api, &authored, extra))
|
||||
.unwrap();
|
||||
let environment = config_source::SnapshotEnvironment::new(state.snapshot.clone())
|
||||
.with_schema_bundle(state.contract.schema_bundle.clone());
|
||||
assert!(
|
||||
environment
|
||||
.analyze(
|
||||
&config_source::VirtualPath::parse("main.dcdl").unwrap(),
|
||||
None
|
||||
)
|
||||
.is_empty(),
|
||||
"{authored}"
|
||||
);
|
||||
let projection = project_profiles_from_workspace_config(TEST_WORKSPACE_ID, &state).unwrap();
|
||||
let bundle = build_virtual_profile_config_bundle(
|
||||
&projection,
|
||||
&state,
|
||||
TEST_WORKSPACE_ID,
|
||||
"created",
|
||||
"project:alpha",
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let resolved = bundle
|
||||
.profile_source_archive
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.verify()
|
||||
.unwrap()
|
||||
.resolve_profile(
|
||||
"project:alpha",
|
||||
Path::new("/runtime-worker"),
|
||||
"scoped-worker",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resolved.scope.allow.len(), 1);
|
||||
assert_eq!(resolved.delegation_scope.allow.len(), 1);
|
||||
let scope = serde_json::to_value(&resolved.scope).unwrap();
|
||||
let delegation = serde_json::to_value(&resolved.delegation_scope).unwrap();
|
||||
assert_eq!(scope["allow"][0]["target"], "/runtime-worker");
|
||||
assert_eq!(scope["allow"][0]["permission"], "read");
|
||||
assert_eq!(delegation["allow"][0]["target"], "/runtime-worker");
|
||||
assert_eq!(delegation["allow"][0]["permission"], "write");
|
||||
scopes.push((scope, delegation));
|
||||
}
|
||||
assert_eq!(scopes[0], scopes[1]);
|
||||
assert_eq!(scopes[1], scopes[2]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn value_profiles_invalid_saves_preserve_the_active_config() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -242,6 +317,12 @@ async fn value_profiles_invalid_saves_preserve_the_active_config() {
|
||||
(source("42"), ""),
|
||||
(source(r#"{ worker = { mode = "not-a-mode"; }; }"#), "profile_value_invalid"),
|
||||
(source(r#"{ worker = { name = "runtime-only"; }; }"#), "profile_value_invalid"),
|
||||
(source(r#"{ scope = 42; }"#), "profile_value_invalid"),
|
||||
(source(r#"{ scope = "not-an-intent"; }"#), "profile_value_invalid"),
|
||||
(source(r#"{ delegation_scope = { intent = 42; }; }"#), "profile_value_invalid"),
|
||||
(source(r#"{ scope = { intent = "workspace_read"; unknown = true; }; }"#), "profile_value_invalid"),
|
||||
(source(r#"{ scope = { intent = "workspace_read"; deny_write = 42; }; }"#), "profile_value_invalid"),
|
||||
(source(r#"{ delegation_scope = { intent = "workspace_write"; symlink_policy = 42; }; }"#), "profile_value_invalid"),
|
||||
(source(r#"import "./missing.dcdl""#), ""),
|
||||
(source(r#"import "../outside.dcdl""#), ""),
|
||||
(r#"{ profile = { entries = [{ selector = "project:alpha"; profile = {}; }, { selector = "project:alpha"; profile = {}; }]; }; }"#.into(), "profile_selector_duplicate"),
|
||||
|
||||
Reference in New Issue
Block a user