blob: 003fd910f74657c83459aa98a532c7300033df48 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use super::*;
use maka_capabilities::call::{Identity, ReadResource, ResourceEncoding};
use maka_event_log::root::{RootNamespaces, RootOwner};
use maka_plugin_core::{
composition::Environment,
contract::{ResourceType, text_resource_type},
runtime::Runtime,
};
use serde_json::json;
use std::{ops::Range, time::Duration};
struct Fixture {
manager: Manager,
store: Arc<EventLog>,
source: DeploymentRef,
input: Input,
_directory: tempfile::TempDir,
}
impl Fixture {
async fn new() -> Self {
let directory = tempfile::tempdir().unwrap();
let owner = Arc::new(
RootOwner::create(
&directory.path().join("root"),
&RootNamespaces {
ownership: directory.path().join("owners"),
control: directory.path().join("control"),
},
)
.unwrap(),
);
let store = Arc::new(EventLog::for_root(owner).await.unwrap());
let input = Input {
environment: Environment {
location: ExecutionLocation::Host,
presentation: None,
},
instances: BTreeMap::new(),
recovery: None,
launch: None,
};
let source = store
.commit_application(Uuid::new_v4(), 0, input.clone(), vec![], vec![])
.await
.unwrap();
let manager = ManagementTarget::from_source(store.clone(), source.clone())
.await
.unwrap()
.0;
Self {
manager,
store,
source,
input,
_directory: directory,
}
}
}
fn package(bytes: Vec<u8>) -> Package {
Package::new(BTreeMap::from([
(
"maka.extension.json".into(),
serde_json::to_vec(&json!({
"schemaVersion":1,"id":"example.payload",
"application":{"entries":{"host":{
"implementation":{"kind":"native","implementation":"example.payload"},
"configuration":{"type":"object"},"imports":{},"exports":{}
}}}
}))
.unwrap(),
),
("图像.bin".into(), bytes),
]))
.unwrap()
}
fn input(package: &Package) -> Input {
serde_json::from_value(json!({
"environment":{"location":"host"},
"instances":{"payload":{
"key":Uuid::from_u128(1),
"package":{"source":"pinned","package":package.application_ref()},"usePackage":"entry"
}}
}))
.unwrap()
}
struct Snapshot {
bytes: Vec<u8>,
encoding: ResourceEncoding,
}
impl ReadResource for Snapshot {
fn encoding(&self) -> ResourceEncoding {
self.encoding
}
fn resource_type(&self) -> ResourceType {
match self.encoding {
ResourceEncoding::Utf8 => text_resource_type(),
ResourceEncoding::Bytes => binary_resource_type(),
}
}
fn bytes(&self) -> usize {
self.bytes.len()
}
fn metadata(&self) -> Value {
Value::Null
}
fn read(&self, range: Range<usize>) -> Result<Vec<u8>> {
if self.encoding == ResourceEncoding::Utf8 {
let text = std::str::from_utf8(&self.bytes).map_err(|error| error.to_string())?;
if !text.is_char_boundary(range.start) || !text.is_char_boundary(range.end) {
return Err("invalid UTF-8 range".into());
}
}
self.bytes
.get(range)
.map(|bytes| bytes.to_vec())
.ok_or("invalid range".into())
}
}
fn attachments(document: &DeploymentDocument, packages: Option<Vec<u8>>) -> Attachments {
let mut resources: Attachments = BTreeMap::from([(
"document".into(),
Arc::new(Snapshot {
bytes: serde_json::to_vec(document).unwrap(),
encoding: ResourceEncoding::Utf8,
}) as Arc<dyn ReadResource>,
)]);
if let Some(bytes) = packages {
resources.insert(
"packages".into(),
Arc::new(Snapshot {
bytes,
encoding: ResourceEncoding::Bytes,
}),
);
}
resources
}
fn bundles(packages: &[&Package]) -> Vec<u8> {
let mut bytes = vec![b'['];
for (index, package) in packages.iter().enumerate() {
if index != 0 {
bytes.push(b',');
}
bytes.extend(package.to_bundle());
}
bytes.push(b']');
bytes
}
fn context(attachments: Attachments) -> Context {
// A real kernel activation supplies the request identities without starting
// an unrelated Application, backend or native service for this unit fixture.
let package = package(vec![]);
let mut packages = Packages::default();
packages
.insert_checked(
package.application_ref(),
package.manifest().application.clone().unwrap(),
)
.unwrap();
let plan = Plan::prepare(&input(&package), &packages, &RemoteExports::new()).unwrap();
let mut runtime = Runtime::<()>::new(plan);
let activation = runtime
.begin(&InstanceId(vec![Uuid::from_u128(1)]))
.unwrap();
Context {
stream: None,
caller: None,
scope: Issuer::default()
.issue(
Identity::Remote {
request_id: Uuid::new_v4(),
},
CancellationToken::new(),
)
.unwrap(),
consumer: activation.clone(),
requester: activation,
operation: None,
attachments,
imports: Arc::new(BTreeMap::new()),
dispatch: Weak::new(),
}
}
fn export(manager: Manager, source: DeploymentRef, package: PackageRef) -> (Subscription, Scope) {
let mut context = context(Attachments::new());
let scope = context.scope.clone();
let (sender, receiver) = oneshot::channel();
let contract = deployment_manager_contract();
let Interface::Service { methods } = contract.interface else {
unreachable!()
};
let validator = Arc::new(jsonschema::validator_for(&methods["exportPackage"].output).unwrap());
let (stream, emitter) = subscriptions::channel(
Accepted {
id: Uuid::new_v4(),
result: receiver,
},
validator.clone(),
scope.cancellation.clone(),
);
context.stream = Some(emitter.clone());
let finishing = scope.clone();
tokio::spawn(async move {
let result = manager
.export_package(json!({"source":source,"package":package}), context)
.await
.and_then(|value| {
validator
.validate(&value)
.map_err(|error| error.to_string())?;
Ok(value)
});
emitter.close();
let cleanup = finishing.finish().await.map_err(|error| error.to_string());
let _ = sender.send(QueryCompletion { result, cleanup });
});
(stream, scope)
}
#[tokio::test]
async fn text_authoring_uses_original_source_revision_and_package_validation() {
let fixture = Fixture::new().await;
let Fixture {
manager,
store,
source,
input,
..
} = &fixture;
let files = json!({
"maka.extension.json":json!({"schemaVersion":1,"id":"example.footer","application":{"entries":{"host":{"implementation":{"kind":"javascript","module":"host.mjs","realm":"shared"},"configuration":{"type":"object"},"imports":{},"exports":{}}}}}).to_string(),
"host.mjs":"export function activate() { return {}; } // 界面"
});
let installed = manager
.install_source(json!({"expectedRevision":1,"files":files}))
.await
.unwrap();
let installed: ManagementResult<PackageInstalled> = serde_json::from_value(installed).unwrap();
let ManagementResult::Committed { value } = installed else {
panic!("valid package rejected")
};
assert_eq!(value.source.revision, 2);
let read = manager
.read_package(
json!({"source":value.source,"package":value.package,"path":"host.mjs","offset":0}),
)
.await
.unwrap();
assert!(read["text"].as_str().unwrap().ends_with("界面"));
let stale = manager
.install_source(json!({"expectedRevision":1,"files":files}))
.await
.unwrap();
assert_eq!(stale["status"], "conflict");
let invalid = manager
.install_source(json!({"expectedRevision":2,"files":{"../outside":"invalid"}}))
.await
.unwrap();
assert_eq!(invalid["status"], "rejected");
let mut foreign = value.source.clone();
foreign.deployment = uuid::Uuid::new_v4();
assert!(
manager
.read_package(json!({"source":foreign,"package":value.package,"path":null,"offset":0}))
.await
.is_err()
);
let replaced=manager.replace_source(json!({"expectedRevision":2,"document":{"input":input,"remoteExports":[],"packages":[value.package]}})).await.unwrap();
assert_eq!(replaced["status"], "committed");
assert_eq!(
store
.read_application_metadata(source.deployment)
.await
.unwrap()
.unwrap()
.source
.reference
.revision,
3
);
}
async fn replace(
manager: &Manager,
expected: u64,
document: &DeploymentDocument,
packages: Option<Vec<u8>>,
) -> Value {
let context = context(attachments(document, packages));
let scope = context.scope.clone();
let result = manager
.call(
"replace".into(),
json!({"expectedRevision":expected}),
context,
)
.await
.unwrap();
scope.finish().await.unwrap();
assert!(scope.resources.is_idle());
result
}
#[tokio::test]
async fn replacement_commits_document_and_binary_packages_in_one_revision() {
let fixture = Fixture::new().await;
let first = package(vec![0, 255, 128, 13, 10]);
let second = package(vec![0, 254, 129]);
let document = DeploymentDocument {
input: input(&first),
remote_exports: vec![],
packages: vec![first.application_ref(), second.application_ref()],
};
let result = replace(
&fixture.manager,
1,
&document,
Some(bundles(&[&first, &second])),
)
.await;
assert_eq!(result["status"], "committed");
assert_eq!(result["value"]["revision"], 2);
let loaded = fixture
.store
.load_application(fixture.source.deployment)
.await
.unwrap()
.unwrap();
assert_eq!(loaded.source().reference.revision, 2);
assert_eq!(loaded.source().input, document.input);
assert_eq!(loaded.packages().len(), 2);
assert_eq!(
loaded
.packages()
.iter()
.find(|package| package.application_ref() == first.application_ref())
.unwrap()
.file("图像.bin"),
first.file("图像.bin")
);
assert!(
fixture
.store
.load_application_at(&fixture.source)
.await
.unwrap()
.unwrap()
.packages()
.is_empty()
);
// Already installed bytes need no attachment, and omitted references are
// actually removed rather than silently retained in the new source.
let document = DeploymentDocument {
input: input(&second),
remote_exports: vec![],
packages: vec![second.application_ref()],
};
let result = replace(&fixture.manager, 2, &document, None).await;
assert_eq!(result["value"]["revision"], 3);
let loaded = fixture
.store
.load_application(fixture.source.deployment)
.await
.unwrap()
.unwrap();
assert_eq!(loaded.packages().len(), 1);
assert_eq!(
loaded.packages()[0].application_ref(),
second.application_ref()
);
}
#[tokio::test]
async fn invalid_bundles_documents_and_stale_replacements_leave_source_unchanged() {
let fixture = Fixture::new().await;
let selected = package(vec![0, 255]);
let unrelated = package(vec![1, 254]);
let document = DeploymentDocument {
input: input(&selected),
remote_exports: vec![],
packages: vec![selected.application_ref()],
};
let mut duplicate_document = document.clone();
duplicate_document.packages.push(selected.application_ref());
let mut invalid_source = document.clone();
// A required package differs from the document set, so full validation fails.
invalid_source.input = input(&unrelated);
for (document, supplied, reason) in [
(&document, Some(b"[{}]".to_vec()), "invalid_bundle"),
(
&document,
Some(bundles(&[&selected, &selected])),
"invalid_bundle",
),
(
&document,
Some(bundles(&[&selected, &unrelated])),
"invalid_bundle",
),
(&document, None, "package_unavailable"),
(
&duplicate_document,
Some(bundles(&[&selected])),
"invalid_document",
),
(
&invalid_source,
Some(bundles(&[&selected])),
"invalid_source",
),
] {
let result = replace(&fixture.manager, 1, document, supplied).await;
assert_eq!(result["status"], "rejected");
assert_eq!(result["reason"], reason);
let loaded = fixture
.store
.load_application(fixture.source.deployment)
.await
.unwrap()
.unwrap();
assert_eq!(loaded.source().reference, fixture.source);
assert_eq!(loaded.source().input, fixture.input);
assert!(loaded.packages().is_empty());
}
let result = replace(&fixture.manager, 1, &document, Some(bundles(&[&selected]))).await;
assert_eq!(result["value"]["revision"], 2);
let stale = replace(&fixture.manager, 1, &document, Some(b"not JSON".to_vec())).await;
assert_eq!(stale, json!({"status":"conflict","currentRevision":2}));
assert_eq!(
fixture
.manager
.metadata()
.await
.unwrap()
.source
.reference
.revision,
2
);
}
#[tokio::test]
async fn historical_binary_bundle_export_round_trips_and_cancels_with_cleanup() {
tokio::time::timeout(Duration::from_secs(10), async {
let fixture = Fixture::new().await;
let package = package((0..400_000).map(|index| (index % 256) as u8).collect());
let historical = fixture
.store
.commit_application(
fixture.source.deployment,
1,
input(&package),
vec![],
vec![package.clone()],
)
.await
.unwrap();
fixture
.store
.commit_application(
fixture.source.deployment,
2,
fixture.input.clone(),
vec![],
vec![],
)
.await
.unwrap();
let (mut stream, scope) = export(
fixture.manager.clone(),
historical.clone(),
package.application_ref(),
);
let mut bytes = Vec::new();
let mut chunks = 0;
let mut total = None;
while let Some(value) = stream.recv().await {
let PackageBundleExport::Chunk {
offset,
total_bytes,
text,
} = serde_json::from_value(value).unwrap()
else {
panic!("completion belongs to the final result");
};
assert_eq!(offset, bytes.len());
assert!(text.len() <= 16 * 1024);
assert!(total.is_none_or(|total| total == total_bytes));
total = Some(total_bytes);
bytes.extend_from_slice(text.as_bytes());
chunks += 1;
}
assert!(chunks > 1);
assert_eq!(total, Some(bytes.len()));
let completion = stream.completion().await;
assert_eq!(completion.result.unwrap(), json!({"kind":"complete"}));
completion.cleanup.unwrap();
assert!(scope.resources.is_idle());
let restored = Package::from_bundle(&bytes).unwrap();
assert_eq!(restored.application_ref(), package.application_ref());
assert_eq!(restored.files(), package.files());
let (mut stream, scope) = export(
fixture.manager.clone(),
historical.clone(),
package.application_ref(),
);
assert!(stream.recv().await.is_some());
stream.cancel();
let completion = stream.completion().await;
assert!(completion.result.is_err());
completion.cleanup.unwrap();
assert!(scope.cancellation.is_cancelled());
assert!(scope.resources.is_idle());
let mut foreign = historical;
foreign.deployment = Uuid::new_v4();
let (mut stream, _) = export(fixture.manager.clone(), foreign, package.application_ref());
assert!(stream.recv().await.is_none());
let completion = stream.completion().await;
assert!(completion.result.unwrap_err().contains("foreign"));
completion.cleanup.unwrap();
let current = fixture.manager.metadata().await.unwrap().source.reference;
let (mut stream, _) = export(fixture.manager.clone(), current, package.application_ref());
assert!(stream.recv().await.is_none());
let completion = stream.completion().await;
assert!(completion.result.unwrap_err().contains("not installed"));
completion.cleanup.unwrap();
})
.await
.unwrap();
}
#[tokio::test]
async fn immutable_document_resource_over_two_mib_roundtrips_through_validated_pages() {
async fn read_all(manager: &Manager, source: &DeploymentRef) -> String {
let contract = deployment_manager_contract();
let Interface::Service { methods } = contract.interface else {
unreachable!()
};
let method = &methods["readDocument"];
let input_validator = jsonschema::validator_for(&method.input).unwrap();
let output_validator = jsonschema::validator_for(&method.output).unwrap();
let mut offset = 0;
let mut text = String::new();
let mut read_beyond_two_mib = false;
loop {
let input = json!({"source":source,"offset":offset});
input_validator.validate(&input).unwrap();
let context = context(Attachments::new());
let scope = context.scope.clone();
let value = manager
.call("readDocument".into(), input, context)
.await
.unwrap();
scope.finish().await.unwrap();
assert!(scope.resources.is_idle());
output_validator.validate(&value).unwrap();
let chunk: DeploymentDocumentChunk = serde_json::from_value(value).unwrap();
assert_eq!(&chunk.source, source);
assert_eq!(chunk.offset, offset);
assert!(chunk.text.len() <= 65536);
read_beyond_two_mib |= chunk.offset > 2 * 1024 * 1024;
text.push_str(&chunk.text);
if let Some(next) = chunk.next {
assert_eq!(next, text.len());
assert!(next > offset);
offset = next;
} else {
assert_eq!(text.len(), chunk.total_bytes);
break;
}
}
assert!(
read_beyond_two_mib,
"public continuation offsets must cross the old document limit"
);
text
}
let fixture = Fixture::new().await;
let selected = package(vec![0, 255, 128]);
let mut large_input = input(&selected);
let padding = format!("界{}\n🙂尾", "x".repeat(2 * 1024 * 1024 + 128 * 1024));
large_input
.instances
.get_mut("payload")
.unwrap()
.configuration
.values
.insert("padding".into(), json!(padding));
let document = DeploymentDocument {
input: large_input,
remote_exports: vec![],
packages: vec![selected.application_ref()],
};
assert!(serde_json::to_vec(&document).unwrap().len() > 2 * 1024 * 1024);
let replaced = replace(&fixture.manager, 1, &document, Some(bundles(&[&selected]))).await;
if replaced["status"] != "committed" {
fixture.store.shutdown().await.unwrap();
}
assert_eq!(replaced["status"], "committed", "{replaced}");
let source: DeploymentRef = serde_json::from_value(replaced["value"].clone()).unwrap();
assert_eq!(source.revision, 2);
let first = read_all(&fixture.manager, &source).await;
assert!(
serde_json::from_str::<Value>(&first).unwrap() == serde_json::to_value(&document).unwrap(),
"paged document differs from the complete original source"
);
// Advancing the head cannot reinterpret any old-source continuation.
let smaller = DeploymentDocument {
input: fixture.input.clone(),
remote_exports: vec![],
packages: vec![],
};
let newer = replace(&fixture.manager, 2, &smaller, None).await;
assert_eq!(newer["status"], "committed", "{newer}");
assert_eq!(newer["value"]["revision"], 3);
let repeated = read_all(&fixture.manager, &source).await;
assert!(
repeated == first,
"later head changed the original source pages"
);
assert!(
serde_json::from_str::<Value>(&repeated).unwrap()
== serde_json::to_value(&document).unwrap(),
"original document bytes did not survive the later head"
);
fixture.store.shutdown().await.unwrap();
}
async fn k64_manager_call(manager: &Manager, method: &str, input: Value) -> Value {
let contract = deployment_manager_contract();
let Interface::Service { methods } = contract.interface else {
unreachable!()
};
let method_schema = &methods[method];
jsonschema::validator_for(&method_schema.input)
.unwrap()
.validate(&input)
.unwrap();
let context = context(Attachments::new());
let scope = context.scope.clone();
let result = manager.call(method.into(), input, context).await.unwrap();
scope.finish().await.unwrap();
assert!(scope.resources.is_idle());
jsonschema::validator_for(&method_schema.output)
.unwrap()
.validate(&result)
.unwrap();
result
}
#[tokio::test]
async fn k64_complete_package_files_bytes_and_long_paths_roundtrip_through_management() {
let fixture = Fixture::new().await;
let mut files = package(vec![]).files().clone();
files.remove("图像.bin");
for index in 0..253 {
files.insert(format!("assets/{index:03}.bin"), vec![(index % 251) as u8]);
}
files.insert("large-a.bin".into(), vec![0x81; 8 * 1024 * 1024 + 1]);
files.insert("large-b.bin".into(), vec![0xfe; 8 * 1024 * 1024 + 1]);
let long_path = format!("{}last.bin", "original-segment/".repeat(33));
assert!(long_path.len() > 512);
files.insert(long_path.clone(), vec![0, 255, 128, 13, 10]);
assert_eq!(files.len(), 257);
let selected = Package::new(files).unwrap();
let document = DeploymentDocument {
input: input(&selected),
remote_exports: vec![],
packages: vec![selected.application_ref()],
};
let result = replace(&fixture.manager, 1, &document, Some(bundles(&[&selected]))).await;
assert_eq!(result["status"], "committed", "{result}");
let source: DeploymentRef = serde_json::from_value(result["value"].clone()).unwrap();
let loaded = fixture
.store
.load_application_at(&source)
.await
.unwrap()
.unwrap();
assert_eq!(
loaded.packages()[0].application_ref(),
selected.application_ref()
);
assert!(
loaded.packages()[0].files() == selected.files(),
"persisted original package bytes differ"
);
assert_eq!(
loaded.packages()[0].file(&long_path),
Some(&[0, 255, 128, 13, 10][..])
);
let (mut stream, scope) = export(
fixture.manager.clone(),
source.clone(),
selected.application_ref(),
);
let mut bytes = Vec::new();
while let Some(value) = stream.recv().await {
let PackageBundleExport::Chunk {
offset,
total_bytes,
text,
} = serde_json::from_value(value).unwrap()
else {
panic!("completion belongs to the final result")
};
assert_eq!(offset, bytes.len());
assert!(text.len() <= 16 * 1024);
bytes.extend_from_slice(text.as_bytes());
assert!(bytes.len() <= total_bytes);
}
let completion = stream.completion().await;
assert_eq!(completion.result.unwrap(), json!({"kind":"complete"}));
completion.cleanup.unwrap();
scope.finish().await.unwrap();
let restored = Package::from_bundle(&bytes).unwrap();
assert_eq!(restored.application_ref(), selected.application_ref());
assert!(
restored.files() == selected.files(),
"export/reimport changed original package bytes"
);
let target = fixture._directory.path().join("complete.maka-package.json");
selected.export_to(&target).unwrap();
assert!(Package::read_from(&target).unwrap().files() == selected.files());
assert!(
selected.export_to(&target).is_err(),
"export must not overwrite an existing publication"
);
let current = replace(
&fixture.manager,
2,
&DeploymentDocument {
input: fixture.input.clone(),
remote_exports: vec![],
packages: vec![],
},
None,
)
.await;
assert_eq!(current["status"], "committed");
let historical = fixture
.store
.load_application_at(&source)
.await
.unwrap()
.unwrap();
assert!(
historical.packages()[0].files() == selected.files(),
"new head changed the original package"
);
fixture.store.shutdown().await.unwrap();
}
#[tokio::test]
async fn k64_settings_keep_large_singleton_all_options_deep_paths_and_atomic_edits() {
use maka_plugin_core::deployment::management::{InstanceSettingsPage, SettingValue};
let fixture = Fixture::new().await;
let options: Vec<String> = (0..17)
.map(|index| format!("{index:02}{}", "界".repeat(10_000)))
.collect();
let mut properties = serde_json::Map::new();
properties.insert("aChoice".into(), json!({"type":"string","enum":options,"title":"完整标题".repeat(80),"description":"完整说明".repeat(150)}));
for index in 0..33 {
properties.insert(format!("field{index:02}"), json!({"type":"string"}));
}
let mut nested_schema = json!({"type":"object","properties":{"leaf":{"type":"string"}},"additionalProperties":false});
let mut nested_value = json!({"leaf":"original leaf"});
for _ in 0..16 {
nested_schema = json!({"type":"object","properties":{"child":nested_schema},"additionalProperties":false});
nested_value = json!({"child":nested_value});
}
properties.insert("nested".into(), nested_schema);
let mut files = package(vec![]).files().clone();
let mut manifest: Value = serde_json::from_slice(&files["maka.extension.json"]).unwrap();
manifest["application"]["entries"]["host"]["configuration"] =
json!({"type":"object","properties":properties,"additionalProperties":false});
files.insert(
"maka.extension.json".into(),
serde_json::to_vec(&manifest).unwrap(),
);
let selected = Package::new(files).unwrap();
let mut original = input(&selected);
let values = &mut original
.instances
.get_mut("payload")
.unwrap()
.configuration
.values;
values.insert("aChoice".into(), json!(options[0]));
values.insert("nested".into(), nested_value);
for index in 0..33 {
values.insert(
format!("field{index:02}"),
json!(format!("original {index}")),
);
}
let source = fixture
.store
.commit_application(
fixture.source.deployment,
1,
original.clone(),
vec![],
vec![selected],
)
.await
.unwrap();
let instance = InstanceId(vec![Uuid::from_u128(1)]);
let mut after = None::<String>;
let mut fields = Vec::new();
loop {
let value = k64_manager_call(
&fixture.manager,
"readSettings",
json!({"source":source,"instance":instance,"path":[],"after":after}),
)
.await;
let page: InstanceSettingsPage = serde_json::from_value(value).unwrap();
assert!(!page.fields.is_empty());
if fields.is_empty() {
assert_eq!(
page.fields.len(),
1,
"oversized first field must make singleton progress"
);
assert_eq!(page.fields[0].key, "aChoice");
assert_eq!(
page.fields[0].options,
options
.iter()
.cloned()
.map(|text| SettingValue::Text { text })
.collect::<Vec<_>>()
);
assert_eq!(page.fields[0].label, "完整标题".repeat(80));
assert_eq!(page.fields[0].description, "完整说明".repeat(150));
assert_eq!(
page.fields[0].value,
Some(SettingValue::Text {
text: options[0].clone()
})
);
assert!(serde_json::to_vec(&page.fields[0]).unwrap().len() > 448 * 1024);
}
let next = page.next.clone();
if let Some(next) = &next {
assert_eq!(Some(next), page.fields.last().map(|field| &field.key));
assert!(after.as_ref().is_none_or(|after| next > after));
}
fields.extend(page.fields);
match next {
Some(next) => after = Some(next),
None => break,
}
}
assert_eq!(fields.len(), 35);
let mut path = vec!["nested"; 1];
path.extend(std::iter::repeat_n("child", 16));
let deep = k64_manager_call(
&fixture.manager,
"readSettings",
json!({"source":source,"instance":instance,"path":path,"after":null}),
)
.await;
assert_eq!(deep["fields"][0]["key"], "leaf");
assert_eq!(deep["fields"][0]["editable"], true);
let long_text = "新值🙂".repeat(1100);
let changes: Vec<Value> = (0..33).map(|index| json!({"path":[format!("field{index:02}")],"value":{"kind":"text","text":long_text}})).collect();
let changed = k64_manager_call(
&fixture.manager,
"configure",
json!({"expectedRevision":2,"instance":instance,"changes":changes}),
)
.await;
assert_eq!(changed["status"], "committed");
assert_eq!(changed["value"]["revision"], 3);
let loaded = fixture
.store
.load_application(fixture.source.deployment)
.await
.unwrap()
.unwrap();
let actual = &loaded.source().input.instances["payload"];
for index in 0..33 {
assert_eq!(
actual.configuration.values[&format!("field{index:02}")],
long_text
);
}
assert_eq!(
actual.configuration.values["nested"],
original.instances["payload"].configuration.values["nested"]
);
assert_eq!(actual.package, original.instances["payload"].package);
let stale = k64_manager_call(
&fixture.manager,
"configure",
json!({"expectedRevision":2,"instance":instance,"changes":changes}),
)
.await;
assert_eq!(stale, json!({"status":"conflict","currentRevision":3}));
let frozen = fixture
.store
.load_application_at(&source)
.await
.unwrap()
.unwrap();
assert_eq!(frozen.source().input, original);
fixture.store.shutdown().await.unwrap();
}