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:
2026-10-07 02:25:16 +09:00
parent 3558e4aae1
commit 61f0469986
5 changed files with 258 additions and 5 deletions
+83 -4
View File
@@ -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"),