Fix nullable stored-history constructor switching
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
This commit is contained in:
@@ -449,6 +449,9 @@ impl Draft {
|
||||
None
|
||||
};
|
||||
let tag = |value: &Value| {
|
||||
if self.kind == RecordKind::Trace && history_fields::is_path(path) {
|
||||
return history_fields::constructor_tag(path, value);
|
||||
}
|
||||
value
|
||||
.as_str()
|
||||
.or_else(|| value.as_object()?.keys().next().map(String::as_str))
|
||||
|
||||
@@ -382,6 +382,45 @@ fn branch_has_fields(schema: &'static Value) -> bool {
|
||||
.is_none_or(|values| values.len() > 1)
|
||||
}
|
||||
|
||||
/// Constructor identity is independent of a primitive's edited content. In
|
||||
/// particular, absent, an empty UUID/text, a number and a list are different
|
||||
/// branches. String enum tags and externally tagged objects retain their variant.
|
||||
pub(super) fn constructor_tag(path: &str, value: &Value) -> String {
|
||||
fn enum_value(schema: &'static Value, value: &Value, depth: usize) -> bool {
|
||||
if depth > MAX_DEPTH {
|
||||
return false;
|
||||
}
|
||||
let Ok(schema) = resolve(schema) else {
|
||||
return false;
|
||||
};
|
||||
if let Some(options) = alternatives(schema) {
|
||||
return options
|
||||
.iter()
|
||||
.any(|option| enum_value(option, value, depth + 1));
|
||||
}
|
||||
schema
|
||||
.get("enum")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|values| values.contains(value))
|
||||
}
|
||||
match value {
|
||||
Value::Null => "null".into(),
|
||||
Value::Bool(_) => "boolean".into(),
|
||||
Value::Number(_) => "number".into(),
|
||||
Value::Array(_) => "array".into(),
|
||||
Value::String(word)
|
||||
if schema_at(path).is_some_and(|schema| enum_value(schema, value, 0)) =>
|
||||
{
|
||||
format!("enum:{word}")
|
||||
}
|
||||
Value::String(_) => "string".into(),
|
||||
Value::Object(fields) => format!(
|
||||
"object:{}",
|
||||
fields.keys().cloned().collect::<Vec<_>>().join("/")
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn paging<'a>(draft: &'a Draft, path: &str, count: usize) -> (usize, View<'a, Message>) {
|
||||
let last = count.saturating_sub(1) / PAGE_SIZE;
|
||||
let page = draft
|
||||
|
||||
@@ -731,3 +731,181 @@ fn history_clear_discards_inactive_invalid_constructors_before_reusing_the_same_
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
}
|
||||
|
||||
/// Exercise the same descriptor choice and FieldShape event emitted by the
|
||||
/// nested picker. Injecting an already complete Observed value skips this path.
|
||||
fn choose_history_constructor(draft: &mut Draft, path: &str, label: &str) {
|
||||
let schema = resolve(schema_at(path).expect("typed history field")).unwrap();
|
||||
let branch = alternatives(schema)
|
||||
.expect("constructor choices")
|
||||
.iter()
|
||||
.find(|branch| branch_label(branch) == label)
|
||||
.expect("visible constructor choice");
|
||||
let event = variant_message(
|
||||
path,
|
||||
branch_seed(branch).unwrap(),
|
||||
draft.value.pointer(path).is_some(),
|
||||
);
|
||||
let Message::FieldShape(path, value) = event else {
|
||||
panic!("constructor picker must emit FieldShape");
|
||||
};
|
||||
draft.shape(&path, value).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_nested_optional_number_events_switch_absent_present_and_recover_invalid_raw() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
draft
|
||||
.append(
|
||||
FACETS,
|
||||
choice(FacetSelector::Data(DataFacet::IntegerMinimum)),
|
||||
)
|
||||
.unwrap();
|
||||
let side = format!("{FACETS}/0/Data/IntegerMinimum/oa");
|
||||
let path = format!("{side}/Value");
|
||||
choose_history_constructor(&mut draft, &side, "Observed value");
|
||||
assert_eq!(draft.value.pointer(&path), Some(&Value::Null));
|
||||
choose_history_constructor(&mut draft, &path, "Set number");
|
||||
assert_eq!(
|
||||
draft.value.pointer(&path),
|
||||
Some(&json!(0)),
|
||||
"null and numeric constructor identities must differ"
|
||||
);
|
||||
draft.input(&path, "-".into());
|
||||
choose_history_constructor(&mut draft, &path, "Absent (none)");
|
||||
assert_eq!(draft.value.pointer(&path), Some(&Value::Null));
|
||||
let mut draft = Draft::from_recovery(&project, draft.recovery_snapshot()).unwrap();
|
||||
choose_history_constructor(&mut draft, &path, "Set number");
|
||||
assert_eq!(draft.inputs[&path].raw, "-");
|
||||
assert!(draft.command().is_err());
|
||||
draft.input(&path, "7".into());
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Data(DataReview::IntegerMinimum(Reviewed {
|
||||
oa: Observed::Value(Some(7)),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
choose_history_constructor(&mut draft, &path, "Absent (none)");
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![FacetReview::Data(DataReview::IntegerMinimum(Reviewed {
|
||||
oa: Observed::Value(None),
|
||||
sa: Observed::NotApplicable,
|
||||
}))],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
choose_history_constructor(&mut draft, &path, "Set number");
|
||||
assert_eq!(
|
||||
draft.inputs[&path].raw, "7",
|
||||
"returning to Value restores its edited contents"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_nested_uuid_and_list_events_preserve_present_values_and_tagged_item_branches() {
|
||||
let (project, id) = fixture(Some(baseline(vec![])));
|
||||
let mut draft = Draft::from_project(&project, id).unwrap();
|
||||
draft
|
||||
.append(FACETS, choice(FacetSelector::Header(HeaderFacet::Parent)))
|
||||
.unwrap();
|
||||
draft
|
||||
.append(FACETS, choice(FacetSelector::Field(FieldFacet::Default)))
|
||||
.unwrap();
|
||||
let side = format!("{FACETS}/0/Header/Parent/oa");
|
||||
let uuid = format!("{side}/Value");
|
||||
choose_history_constructor(&mut draft, &side, "Observed value");
|
||||
choose_history_constructor(&mut draft, &uuid, "Set historical UUID");
|
||||
assert_eq!(
|
||||
draft.value.pointer(&uuid),
|
||||
Some(&json!("")),
|
||||
"empty UUID input is distinct from absent"
|
||||
);
|
||||
assert!(draft.command().is_err());
|
||||
draft.input(&uuid, "incomplete-uuid".into());
|
||||
choose_history_constructor(&mut draft, &uuid, "Absent (none)");
|
||||
choose_history_constructor(&mut draft, &uuid, "Set historical UUID");
|
||||
assert_eq!(draft.inputs[&uuid].raw, "incomplete-uuid");
|
||||
let historical = Id::from_u128(8765);
|
||||
draft.input(&uuid, historical.to_string());
|
||||
choose_history_constructor(&mut draft, &uuid, "Absent (none)");
|
||||
choose_history_constructor(&mut draft, &uuid, "Set historical UUID");
|
||||
assert_eq!(draft.inputs[&uuid].raw, historical.to_string());
|
||||
|
||||
let side = format!("{FACETS}/1/Field/Default/oa");
|
||||
let list = format!("{side}/Value");
|
||||
choose_history_constructor(&mut draft, &side, "Observed value");
|
||||
choose_history_constructor(&mut draft, &list, "Set list");
|
||||
assert_eq!(
|
||||
draft.value.pointer(&list),
|
||||
Some(&json!([])),
|
||||
"empty list is distinct from absent"
|
||||
);
|
||||
let array_schema = alternatives(resolve(schema_at(&list).unwrap()).unwrap())
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|branch| resolve(branch).unwrap())
|
||||
.find(|branch| branch["type"] == "array")
|
||||
.unwrap();
|
||||
let event = Message::FieldAppend(list.clone(), seed(&array_schema["items"], 0).unwrap());
|
||||
let Message::FieldAppend(path, value) = event else {
|
||||
unreachable!()
|
||||
};
|
||||
draft.append(&path, value).unwrap();
|
||||
let item = format!("{list}/0");
|
||||
choose_history_constructor(&mut draft, &item, "Integer");
|
||||
draft.input(&format!("{item}/Integer"), "-".into());
|
||||
choose_history_constructor(&mut draft, &item, "String");
|
||||
draft.input(&format!("{item}/String"), "retained textual branch".into());
|
||||
choose_history_constructor(&mut draft, &item, "Integer");
|
||||
assert_eq!(draft.inputs[&format!("{item}/Integer")].raw, "-");
|
||||
draft.input(&format!("{item}/Integer"), "21".into());
|
||||
choose_history_constructor(&mut draft, &list, "Absent (none)");
|
||||
assert_eq!(draft.value.pointer(&list), Some(&Value::Null));
|
||||
let mut draft = Draft::from_recovery(&project, draft.recovery_snapshot()).unwrap();
|
||||
choose_history_constructor(&mut draft, &list, "Set list");
|
||||
assert_eq!(draft.inputs[&format!("{item}/Integer")].raw, "21");
|
||||
choose_history_constructor(&mut draft, &item, "String");
|
||||
assert_eq!(
|
||||
draft.inputs[&format!("{item}/String")].raw,
|
||||
"retained textual branch"
|
||||
);
|
||||
let mut expected = project.clone();
|
||||
expected
|
||||
.traces
|
||||
.get_mut(&id)
|
||||
.unwrap()
|
||||
.baseline
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.semantic = Some(SemanticBaseline {
|
||||
facets: vec![
|
||||
FacetReview::Header(HeaderReview::Parent(Reviewed {
|
||||
oa: Observed::Value(Some(historical)),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
FacetReview::Field(FieldReview::Default(Reviewed {
|
||||
oa: Observed::Value(Some(vec![DataValue::String(
|
||||
"retained textual branch".into(),
|
||||
)])),
|
||||
sa: Observed::NotApplicable,
|
||||
})),
|
||||
],
|
||||
});
|
||||
assert_history_transaction(&mut document(&project), &draft, &expected);
|
||||
}
|
||||
|
||||
+17
-4
@@ -44,7 +44,7 @@ Implemented locally, awaiting compilation:
|
||||
are written. A bounded exact caller/use return path and scoped mounted-scroll
|
||||
restoration are implemented with regressions, also untested.
|
||||
- Advanced typed stored-history editor uses existing OpenAPI type descriptors;
|
||||
ten focused tests cover typed selectors, historical IDs, invalid raw input,
|
||||
twelve focused tests cover typed selectors, historical IDs, invalid raw input,
|
||||
recovery and source/history. Core validation remains authoritative.
|
||||
- Normative catalogs now include ExchangeCategory, ContextualLink and both
|
||||
Exchange trace pairs. Manual trace help and scope source version corrected;
|
||||
@@ -62,12 +62,25 @@ successfully; stable **1.98.1** GNU/LLVM, rustfmt and Clippy installed.
|
||||
job. Locked dependency fetch is local exec session **13108**. No build/test pass
|
||||
is established; the target cache is still cold.
|
||||
|
||||
The coherent candidate passed `cargo fmt --all` and `git diff --check`. It is
|
||||
published as an **UNTESTED integration snapshot** so the other host can use its
|
||||
warm build cache; publication is not acceptance. Their broader architecture
|
||||
The coherent candidate `01bb52a` passed `cargo fmt --all` and `git diff --check`.
|
||||
Direct push failed because this fresh host has no Git credentials. Authenticated
|
||||
Gitea MCP transferred its exact verified Git bundle through
|
||||
`codex/behavior-transfer-20260910` (`harness/transfers/behavior-01bb52a.bundle.b64`),
|
||||
prerequisite `1797516`, decoded SHA256
|
||||
`85A38B00207036DCB8DB24ED4A7E80B2BE670275EEB7B00413D36153322ADA5D`.
|
||||
This **UNTESTED integration snapshot** lets the other host use its warm cache;
|
||||
transfer is not acceptance. Their broader architecture
|
||||
palette/Escape repair should be reconciled with our overlapping compile/stale
|
||||
repairs while retaining the additional invariant tests. Their measured minimum
|
||||
scenario canvas is too small; they own the subsequent layout implementation.
|
||||
Their isolated palette commit is `7d1df4f`, fetched locally for reconciliation.
|
||||
|
||||
Independent F1 review found a real optional-value cache collision in `01bb52a`.
|
||||
The follow-up distinguishes null/scalar/list/tagged constructor identities and
|
||||
adds actual inner selector-event tests. Independent source recheck found no
|
||||
remaining collision; execution is still pending. The original full-platform
|
||||
fetch was stopped after network timeouts; current Windows desktop test build
|
||||
is exec session **51036**, compiling dependencies. Root owns this Cargo lease.
|
||||
|
||||
Next: focused `architecture_authoring_tests`,
|
||||
`behavior::topology`, `direct_clicked`, caller-navigation and `history_fields::tests`,
|
||||
|
||||
@@ -86,3 +86,5 @@ Root authorized the bounded implementation in `crates/oa-desktop/src/forms.rs`,
|
||||
Ten targeted tests are present. They cover absent/empty/existing history; descriptor-derived complete witnesses for every offered scalar/keyed fact against actual FacetReview deserialization and historical validation; rejected NA/NA and duplicates with unchanged Document snapshots; deleted UUID keys/observations; numeric limits and invalid raw repair; compound values and constructor switching; TypeRef's nested primitive picker; exact Unicode/quoted strings and literal keys; cache relocation/recovery; clear/recreate isolation; source/JSON round-trip and exact one-step Undo/Redo including retained source comments.
|
||||
|
||||
**Verification status:** `git diff --check` passes for the candidate (an unrelated generated HTML line-ending warning is present). No Cargo, rustfmt, native GUI or integration suite was run by this worker; root owns the serialized build/toolchain lease. F1 is therefore a ready source candidate, not an accepted fix yet. Root should run targeted history tests, the relevant desktop suite and strict lint, then exercise expanded history controls at normal/minimum width, typed Add/repair, paging, apply/reopen and Undo in the actual desktop. F2 and shared decision/wiki synthesis remain root-owned. F3 remains open.
|
||||
|
||||
**Independent-review follow-up:** behavior_audit found a real defect in snapshot `01bb52a`: the reused constructor-cache key mapped null, numbers, booleans and arrays to the same empty tag, also colliding with empty UUID input. The nested optional picker consequently restored the old branch immediately instead of switching; earlier tests that injected complete Observed values skipped that event. The bounded follow-up adds history-only typed constructor identities (including distinct enum and tagged-object variants) while retaining other property-editor behavior. Two additional tests derive the visible nested picker choices and dispatch their actual FieldShape events: absent/present integer with invalid raw recovery and exact Value(None); empty UUID/absence with deleted identity; optional list/absence with nested Integer/String constructor retention and recovery. The resulting accepted models still receive exact Document/source/Undo checks. Twelve history tests are now present. Whitespace check passes; compilation, execution and native acceptance remain pending with root.
|
||||
|
||||
Reference in New Issue
Block a user