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
@@ -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"),