新增版本绑定改写的窄放行通道与定向用例
- 新增第二条、也是更窄的版本放行通道 mutate_manifest_allowing_version_binding_rewrites:只放行"显式列出的版本改写自己的 resourceBindings",供「直接替换」写入使用 - 新增独立校验函数 validate_version_records_allow_binding_rewrites:不增不删(候选与磁盘版本数量必须相等)、不重排(版本 ID 序列逐项相同)、只有放行 ID 允许 resourceBindings 不同、其余字段(versionId / parentVersionId / projectRevision / createdReason / createdAt / editPrompt)逐字段相等、未放行版本整条相等、放行集合取自写入前 manifest - 两组放行集合必须互斥:同一版本 ID 既被放行删除又被放行改写绑定 → 报「版本绑定改写放行与版本删除放行必须互斥」失败关闭 - 既有 validate_version_records_are_append_only 与既有 mutate_manifest_at_allowing_version_removals 的语义一行未改:本通道是另一条独立的窄校验路径,默认写入路径仍然只有「只追加 + 显式放行删除」 - 把写盘公共体抽成 write_manifest_locked_with_version_guard(校验器由调用方注入),write_manifest_locked 变成一行委托:装盘 / 回读 / 原子替换只有一份实现,存储行为与改造前逐行等价;唯一被参数化的就是版本校验那一步 - 新增 7 条定向用例:放行版本只改自己的绑定成功且其余版本整条相等、未放行版本改绑定被拒且字节不变、增删版本被拒、重排被拒(直接打校验器 + 真实通道两条)、放行版本改 createdAt / editPrompt / parentVersionId / versionId 四种其它字段都被拒、两组放行集合重叠被拒、默认路径与删除放行通道仍然拒绝绑定改写(证明只追加保证没被打穿) - 回归确认:既有 manifest_versions_are_append_only_at_the_storage_boundary 与 asset_delete 7 条继续全绿
This commit is contained in:
@@ -1372,6 +1372,91 @@ fn no_version_removals(_manifest: &GameCreationAppManifest) -> Vec<String> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
/// 同 [`mutate_manifest_at`],但允许显式放行的既有版本**只改写自己的 `resourceBindings`**。
|
||||
///
|
||||
/// 这是"直接替换资源"的写入通道(PRD §3.2 的直接替换口径):把某个版本的绑定从素材 A 指向
|
||||
/// 素材 B,既不新增版本、也不删除版本。放行面被刻意压到最小:
|
||||
///
|
||||
/// - **不增不删**:候选与磁盘的版本数量必须相等;
|
||||
/// - **不重排**:版本 ID 序列必须逐项相同;
|
||||
/// - **只有放行 ID 允许 `resourceBindings` 不同**,其余字段(`versionId` / `parentVersionId` /
|
||||
/// `projectRevision` / `createdReason` / `createdAt` / `editPrompt`)逐字段相等;
|
||||
/// - 未放行的版本必须整条相等;
|
||||
/// - `allowed_binding_rewrites` 按**写入前的 manifest**求值(与删除放行口同口径);
|
||||
/// - 与删除放行集合**互斥**:同一版本 ID 不得既被放行删除、又被放行改写绑定。
|
||||
///
|
||||
/// 既有 [`mutate_manifest_at_allowing_version_removals`] 与
|
||||
/// [`validate_version_records_are_append_only`] 的语义不受影响:本通道是**另一条**独立、更窄的
|
||||
/// 校验路径,默认路径仍然只有"只追加 + 显式放行删除"。
|
||||
pub(crate) fn mutate_manifest_allowing_version_binding_rewrites<T>(
|
||||
root: &Path,
|
||||
allowed_binding_rewrites: &dyn Fn(&GameCreationAppManifest) -> Vec<String>,
|
||||
mutate: impl FnOnce(&mut GameCreationAppManifest) -> Result<T, String>,
|
||||
) -> Result<T, String> {
|
||||
if root.as_os_str().is_empty() {
|
||||
return Err("项目目录不能为空".to_string());
|
||||
}
|
||||
if !root.is_absolute() {
|
||||
return Err("项目目录必须是绝对路径".to_string());
|
||||
}
|
||||
let manifest_path = root.join(".agent/manifest.json");
|
||||
if let Some(parent) = manifest_path.parent() {
|
||||
ensure_game_creator_private_directory_tree(parent, "manifest 目录")?;
|
||||
prepare_game_creator_private_path_for_read(parent, true, "manifest 目录")?;
|
||||
}
|
||||
let _write_lock = acquire_manifest_write_lock(&manifest_path)?;
|
||||
let (_, mut manifest) = read_or_create_manifest(root)?;
|
||||
let allowed_binding_rewrites = allowed_binding_rewrites(&manifest);
|
||||
let result = mutate(&mut manifest)?;
|
||||
write_manifest_locked_with_version_guard(&manifest_path, &manifest, &|existing, candidate| {
|
||||
validate_version_records_allow_binding_rewrites(
|
||||
&existing.versions,
|
||||
&candidate.versions,
|
||||
&allowed_binding_rewrites,
|
||||
&[],
|
||||
)
|
||||
})?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// 项目版本数组的**第二条**放行通道:只放行"显式列出的版本改写自己的 `resourceBindings`"。
|
||||
///
|
||||
/// 与 [`validate_version_records_are_append_only`] 并列存在、互不替代:本函数不做任何"允许追加/
|
||||
/// 允许删除/允许重排"的放宽,只在一个字段上开一个显式白名单窗口。
|
||||
fn validate_version_records_allow_binding_rewrites(
|
||||
existing: &[GameIterationVersion],
|
||||
candidate: &[GameIterationVersion],
|
||||
allowed_binding_rewrites: &[String],
|
||||
allowed_version_removals: &[String],
|
||||
) -> Result<(), String> {
|
||||
let violation = || "项目版本记录写入后不可修改、删除或重排".to_string();
|
||||
// 两条放行通道必须互斥:既被放行删除、又被放行改写绑定,等于同时表达"这条消失"与
|
||||
// "这条保留但改绑定",语义自相矛盾,直接失败关闭。
|
||||
if allowed_binding_rewrites
|
||||
.iter()
|
||||
.any(|version_id| allowed_version_removals.contains(version_id))
|
||||
{
|
||||
return Err("版本绑定改写放行与版本删除放行必须互斥".to_string());
|
||||
}
|
||||
// 直接替换不产生新版本,也不允许任何版本消失。
|
||||
if existing.len() != candidate.len() {
|
||||
return Err(violation());
|
||||
}
|
||||
for (existing_version, candidate_version) in existing.iter().zip(candidate.iter()) {
|
||||
if existing_version.version_id != candidate_version.version_id {
|
||||
return Err(violation());
|
||||
}
|
||||
let mut normalized = candidate_version.clone();
|
||||
if allowed_binding_rewrites.contains(&existing_version.version_id) {
|
||||
normalized.resource_bindings = existing_version.resource_bindings.clone();
|
||||
}
|
||||
if normalized != *existing_version {
|
||||
return Err(violation());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 项目版本数组的写入边界:既有版本只允许按显式放行的 ID 删除,其余必须原样保留且只能追加。
|
||||
fn validate_version_records_are_append_only(
|
||||
existing: &[GameIterationVersion],
|
||||
@@ -1720,6 +1805,28 @@ fn write_manifest_locked(
|
||||
path: &Path,
|
||||
manifest: &GameCreationAppManifest,
|
||||
allowed_version_removals: &dyn Fn(&GameCreationAppManifest) -> Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
write_manifest_locked_with_version_guard(path, manifest, &|existing, candidate| {
|
||||
validate_version_records_are_append_only(
|
||||
&existing.versions,
|
||||
&candidate.versions,
|
||||
&allowed_version_removals(existing),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// 版本数组写入的公共体:把"装盘 / 回读 / 原子替换"这套存储细节与版本校验分开。
|
||||
///
|
||||
/// 校验器由调用方注入,所以 [`write_manifest_locked`](只追加 + 显式放行删除)与
|
||||
/// [`mutate_manifest_allowing_version_binding_rewrites`](只放行显式版本的绑定改写)共用同一份
|
||||
/// 装盘实现,不会出现两份略有差异的原子替换路径。存储行为与改造前逐行等价。
|
||||
fn write_manifest_locked_with_version_guard(
|
||||
path: &Path,
|
||||
manifest: &GameCreationAppManifest,
|
||||
version_guard: &dyn Fn(
|
||||
&GameCreationAppManifest,
|
||||
&GameCreationAppManifest,
|
||||
) -> Result<(), String>,
|
||||
) -> Result<(), String> {
|
||||
validate_game_iteration_versions(&manifest.versions)
|
||||
.map_err(|error| format!("校验 manifest 项目版本失败:{error}"))?;
|
||||
@@ -1727,11 +1834,7 @@ fn write_manifest_locked(
|
||||
.map_err(|error| format!("序列化 manifest 失败:{error}"))?;
|
||||
if manifest_storage_exists(path)? {
|
||||
let existing = read_manifest(path)?;
|
||||
validate_version_records_are_append_only(
|
||||
&existing.versions,
|
||||
&manifest.versions,
|
||||
&allowed_version_removals(&existing),
|
||||
)?;
|
||||
version_guard(&existing, manifest)?;
|
||||
}
|
||||
match fs::symlink_metadata(path) {
|
||||
Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
|
||||
@@ -1814,6 +1917,8 @@ mod classification_tests;
|
||||
mod import_tests;
|
||||
#[cfg(test)]
|
||||
mod recovery_tests;
|
||||
#[cfg(test)]
|
||||
mod version_binding_rewrite_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod npm_scaffold_tests {
|
||||
|
||||
+343
@@ -0,0 +1,343 @@
|
||||
use super::*;
|
||||
use shared_contracts::game_creation_app::{
|
||||
GameIterationVersion, GameIterationVersionCreatedReason, GameIterationVersionResourceBinding,
|
||||
};
|
||||
|
||||
/// 「直接替换」的写入通道用例:只放行显式版本的 `resourceBindings` 改写,其余一切照旧失败关闭。
|
||||
///
|
||||
/// 这些用例同时是"只追加保证没有被这条新通道打穿"的证据:不增、不删、不重排、不改别的字段。
|
||||
|
||||
fn unique_binding_rewrite_root(test_name: &str) -> PathBuf {
|
||||
std::env::temp_dir().join(format!(
|
||||
"genarrative-binding-rewrite-{test_name}-{}-{}",
|
||||
std::process::id(),
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_nanos()
|
||||
))
|
||||
}
|
||||
|
||||
fn binding(resource_id: &str) -> GameIterationVersionResourceBinding {
|
||||
GameIterationVersionResourceBinding {
|
||||
slot_id: format!("asset:{resource_id}"),
|
||||
resource_id: resource_id.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn binding_rewrite_version(
|
||||
version_id: &str,
|
||||
parent_version_id: Option<&str>,
|
||||
project_revision: u64,
|
||||
created_reason: GameIterationVersionCreatedReason,
|
||||
resource_ids: &[&str],
|
||||
) -> GameIterationVersion {
|
||||
GameIterationVersion {
|
||||
version_id: version_id.to_string(),
|
||||
parent_version_id: parent_version_id.map(str::to_string),
|
||||
project_revision,
|
||||
resource_bindings: resource_ids.iter().copied().map(binding).collect(),
|
||||
created_reason,
|
||||
created_at: project_revision,
|
||||
edit_prompt: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 三版本项目:root → child → grandchild,绑定集合各不同,供"只放行一条"的对照使用。
|
||||
fn binding_rewrite_fixture(test_name: &str) -> (PathBuf, PathBuf, GameCreationAppManifest) {
|
||||
let root = unique_binding_rewrite_root(test_name);
|
||||
let manifest_path = root.join(".agent/manifest.json");
|
||||
let mut manifest = new_game_creation_app_manifest("project-binding-rewrite", "绑定改写项目");
|
||||
manifest.versions = vec![
|
||||
binding_rewrite_version(
|
||||
"version-root",
|
||||
None,
|
||||
1,
|
||||
GameIterationVersionCreatedReason::Initial,
|
||||
&["asset-a"],
|
||||
),
|
||||
binding_rewrite_version(
|
||||
"version-child",
|
||||
Some("version-root"),
|
||||
2,
|
||||
GameIterationVersionCreatedReason::AgentRevision,
|
||||
&["asset-a", "asset-b"],
|
||||
),
|
||||
binding_rewrite_version(
|
||||
"version-grandchild",
|
||||
Some("version-child"),
|
||||
3,
|
||||
GameIterationVersionCreatedReason::AgentRevision,
|
||||
&["asset-a", "asset-b", "asset-c"],
|
||||
),
|
||||
];
|
||||
write_manifest(&manifest_path, &manifest).expect("write versioned manifest fixture");
|
||||
(root, manifest_path, manifest)
|
||||
}
|
||||
|
||||
/// 放行的版本:只改自己的 `resourceBindings` 成功,其余版本与其余字段逐字不动。
|
||||
#[test]
|
||||
fn binding_rewrite_allows_only_the_whitelisted_version_bindings() {
|
||||
let (root, manifest_path, installed) = binding_rewrite_fixture("allowed");
|
||||
let version_id = "version-child".to_string();
|
||||
|
||||
mutate_manifest_allowing_version_binding_rewrites(
|
||||
&root,
|
||||
&|_manifest| vec![version_id.clone()],
|
||||
|manifest| {
|
||||
let version = manifest
|
||||
.versions
|
||||
.iter_mut()
|
||||
.find(|version| version.version_id == version_id)
|
||||
.expect("child version exists");
|
||||
version.resource_bindings = vec![binding("asset-a"), binding("asset-b2")];
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect("放行版本改写自己的绑定必须成功");
|
||||
|
||||
let written = read_manifest(&manifest_path).expect("read rewritten manifest");
|
||||
assert_eq!(written.versions.len(), installed.versions.len(), "不得增删版本");
|
||||
assert_eq!(
|
||||
written.versions[0], installed.versions[0],
|
||||
"未放行版本必须整条相等"
|
||||
);
|
||||
assert_eq!(
|
||||
written.versions[2], installed.versions[2],
|
||||
"未放行版本必须整条相等"
|
||||
);
|
||||
// 放行版本:除绑定外逐字段相等。
|
||||
let rewritten = &written.versions[1];
|
||||
let original = &installed.versions[1];
|
||||
assert_eq!(rewritten.version_id, original.version_id);
|
||||
assert_eq!(rewritten.parent_version_id, original.parent_version_id);
|
||||
assert_eq!(rewritten.project_revision, original.project_revision);
|
||||
assert_eq!(rewritten.created_reason, original.created_reason);
|
||||
assert_eq!(rewritten.created_at, original.created_at);
|
||||
assert_eq!(rewritten.edit_prompt, original.edit_prompt);
|
||||
assert_eq!(
|
||||
rewritten.resource_bindings,
|
||||
vec![binding("asset-a"), binding("asset-b2")]
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// 未放行的版本改绑定 → 拒绝,manifest 字节不变。
|
||||
#[test]
|
||||
fn binding_rewrite_rejects_versions_outside_the_whitelist() {
|
||||
let (root, manifest_path, _installed) = binding_rewrite_fixture("outside-whitelist");
|
||||
let before = fs::read(&manifest_path).expect("read manifest bytes before rejection");
|
||||
|
||||
let error = mutate_manifest_allowing_version_binding_rewrites(
|
||||
&root,
|
||||
&|_manifest| vec!["version-child".to_string()],
|
||||
|manifest| {
|
||||
// 放行的是 child,改的却是 root。
|
||||
manifest.versions[0].resource_bindings = vec![binding("asset-z")];
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect_err("未放行版本改绑定必须被拒绝");
|
||||
assert!(error.contains("不可修改、删除或重排"), "{error}");
|
||||
assert_eq!(
|
||||
fs::read(&manifest_path).expect("read manifest bytes after rejection"),
|
||||
before,
|
||||
"被拒绝的写入不得改动 manifest"
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// 不增不删:追加新版本、删除版本都被这条通道拒绝(它不放行任何增删)。
|
||||
#[test]
|
||||
fn binding_rewrite_rejects_version_additions_and_removals() {
|
||||
let (root, manifest_path, _installed) = binding_rewrite_fixture("no-add-no-remove");
|
||||
let before = fs::read(&manifest_path).expect("read manifest bytes before rejection");
|
||||
let version_id = "version-child".to_string();
|
||||
|
||||
let append_error = mutate_manifest_allowing_version_binding_rewrites(
|
||||
&root,
|
||||
&|_manifest| vec![version_id.clone()],
|
||||
|manifest| {
|
||||
manifest.versions.push(binding_rewrite_version(
|
||||
"version-appended",
|
||||
Some("version-grandchild"),
|
||||
4,
|
||||
GameIterationVersionCreatedReason::AgentRevision,
|
||||
&["asset-d"],
|
||||
));
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect_err("绑定改写通道不得追加版本");
|
||||
assert!(append_error.contains("不可修改、删除或重排"), "{append_error}");
|
||||
|
||||
let remove_error = mutate_manifest_allowing_version_binding_rewrites(
|
||||
&root,
|
||||
&|_manifest| vec![version_id.clone()],
|
||||
|manifest| {
|
||||
manifest.versions.remove(2);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect_err("绑定改写通道不得删除版本");
|
||||
assert!(remove_error.contains("不可修改、删除或重排"), "{remove_error}");
|
||||
|
||||
assert_eq!(
|
||||
fs::read(&manifest_path).expect("read manifest bytes after rejections"),
|
||||
before
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// 不重排:版本 ID 序列一变就必须拒绝。
|
||||
///
|
||||
/// 集成路径上"换序"会先被版本图校验拦下(父版本必须先于子版本存在),所以这里两条都测:
|
||||
/// ① 直接打校验器,精确命中"不重排"分支;② 走真实通道,确认任何换序都失败关闭且字节不变。
|
||||
#[test]
|
||||
fn binding_rewrite_rejects_version_reordering() {
|
||||
let (root, manifest_path, installed) = binding_rewrite_fixture("no-reorder");
|
||||
let before = fs::read(&manifest_path).expect("read manifest bytes before rejection");
|
||||
|
||||
let mut reordered = installed.versions.clone();
|
||||
reordered.swap(1, 2);
|
||||
let direct_error = validate_version_records_allow_binding_rewrites(
|
||||
&installed.versions,
|
||||
&reordered,
|
||||
&[],
|
||||
&[],
|
||||
)
|
||||
.expect_err("版本 ID 序列变化必须被拒绝");
|
||||
assert!(direct_error.contains("不可修改、删除或重排"), "{direct_error}");
|
||||
|
||||
let channel_error = mutate_manifest_allowing_version_binding_rewrites(
|
||||
&root,
|
||||
&|_manifest| vec!["version-child".to_string()],
|
||||
|manifest| {
|
||||
manifest.versions.swap(1, 2);
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect_err("绑定改写通道不得重排版本");
|
||||
assert!(
|
||||
channel_error.contains("项目版本"),
|
||||
"拒绝原因必须仍然来自版本校验:{channel_error}"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(&manifest_path).expect("read manifest bytes after rejection"),
|
||||
before
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// 放行版本改**其它字段**同样拒绝(每一条都先通过版本图校验,才能精确命中改写守卫)。
|
||||
#[test]
|
||||
fn binding_rewrite_rejects_other_field_changes_on_the_whitelisted_version() {
|
||||
let (root, manifest_path, _installed) = binding_rewrite_fixture("other-fields");
|
||||
let before = fs::read(&manifest_path).expect("read manifest bytes before rejection");
|
||||
let version_id = "version-child".to_string();
|
||||
|
||||
let allowed = |_: &GameCreationAppManifest| vec![version_id.clone()];
|
||||
let cases: [(&str, fn(&mut GameCreationAppManifest)); 4] = [
|
||||
("createdAt", |manifest| {
|
||||
manifest.versions[1].created_at = 1;
|
||||
}),
|
||||
("editPrompt", |manifest| {
|
||||
manifest.versions[1].edit_prompt = Some("重新出图".to_string());
|
||||
}),
|
||||
("parentVersionId", |manifest| {
|
||||
manifest.versions[2].parent_version_id = Some("version-root".to_string());
|
||||
}),
|
||||
("versionId", |manifest| {
|
||||
manifest.versions[2].version_id = "version-renamed".to_string();
|
||||
}),
|
||||
];
|
||||
|
||||
for (label, mutate) in cases {
|
||||
let error = mutate_manifest_allowing_version_binding_rewrites(&root, &allowed, |manifest| {
|
||||
mutate(manifest);
|
||||
Ok(())
|
||||
})
|
||||
.err()
|
||||
.unwrap_or_else(|| panic!("{label} 必须被拒绝"));
|
||||
assert!(
|
||||
error.contains("不可修改、删除或重排"),
|
||||
"{label} 的拒绝原因必须来自版本改写守卫:{error}"
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(&manifest_path).expect("read manifest bytes after rejection"),
|
||||
before,
|
||||
"{label} 被拒绝后字节不得变化"
|
||||
);
|
||||
}
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// 两组放行集合必须互斥:同一版本 ID 既被放行删除、又被放行改写绑定 → 失败关闭。
|
||||
#[test]
|
||||
fn binding_rewrite_whitelist_must_be_disjoint_from_version_removals() {
|
||||
let (root, manifest_path, installed) = binding_rewrite_fixture("disjoint-whitelists");
|
||||
let before = fs::read(&manifest_path).expect("read manifest bytes before rejection");
|
||||
|
||||
let error = validate_version_records_allow_binding_rewrites(
|
||||
&installed.versions,
|
||||
&installed.versions,
|
||||
&["version-child".to_string()],
|
||||
&["version-child".to_string()],
|
||||
)
|
||||
.expect_err("两组放行集合重叠必须被拒绝");
|
||||
assert!(error.contains("必须互斥"), "{error}");
|
||||
|
||||
// 真实写入路径也拿不到重叠放行:该通道固定传空删除集合。
|
||||
assert_eq!(
|
||||
fs::read(&manifest_path).expect("read manifest bytes after rejection"),
|
||||
before
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
/// 只追加保证没有被打穿:默认路径与删除放行通道仍然拒绝绑定改写。
|
||||
#[test]
|
||||
fn binding_rewrite_channel_does_not_loosen_the_default_or_removal_paths() {
|
||||
let (root, manifest_path, installed) = binding_rewrite_fixture("default-path-unchanged");
|
||||
let before = fs::read(&manifest_path).expect("read manifest bytes before rejection");
|
||||
|
||||
let default_error = mutate_manifest_at(&root, |manifest| {
|
||||
manifest.versions[1].resource_bindings = vec![binding("asset-b2")];
|
||||
Ok(())
|
||||
})
|
||||
.expect_err("默认写入路径必须继续拒绝绑定改写");
|
||||
assert!(
|
||||
default_error.contains("不可修改、删除或重排"),
|
||||
"{default_error}"
|
||||
);
|
||||
|
||||
let removal_channel_error = mutate_manifest_at_allowing_version_removals(
|
||||
&root,
|
||||
&|_manifest| vec!["version-child".to_string()],
|
||||
|manifest| {
|
||||
// 即使用删除放行口把这条列进去,它也不能"保留下来顺便改绑定"。
|
||||
manifest.versions[1].resource_bindings = vec![binding("asset-b2")];
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect_err("删除放行通道不得用于绑定改写");
|
||||
assert!(
|
||||
removal_channel_error.contains("不可修改、删除或重排"),
|
||||
"{removal_channel_error}"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
fs::read(&manifest_path).expect("read manifest bytes after rejections"),
|
||||
before
|
||||
);
|
||||
let unchanged = read_manifest(&manifest_path).expect("read unchanged manifest");
|
||||
assert_eq!(unchanged.versions.len(), installed.versions.len());
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
Reference in New Issue
Block a user