From bd1dfcb0f6b235ca67e8a00a48c813f60887a0ad Mon Sep 17 00:00:00 2001 From: "ra3battlenet-release-bot[bot]" <5134029+ra3battlenet-release-bot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2026 13:42:36 +0000 Subject: [PATCH 1/3] =?UTF-8?q?release:=20RA3BattleNet=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=8C=87=E9=92=88=E6=8C=87=E5=90=91=202.0.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Metadata/apps/ra3battlenet/ra3battlenet.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Metadata/apps/ra3battlenet/ra3battlenet.xml b/Metadata/apps/ra3battlenet/ra3battlenet.xml index 00dc1f4..1a319ba 100644 --- a/Metadata/apps/ra3battlenet/ra3battlenet.xml +++ b/Metadata/apps/ra3battlenet/ra3battlenet.xml @@ -21,7 +21,7 @@ - + From 4e11e6a9d6b0e8a0dff775d5f8b50096a643568f Mon Sep 17 00:00:00 2001 From: mrbbbaixue Date: Mon, 9 Nov 2026 22:28:27 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=A6=20chore(ra3battlenet):=20?= =?UTF-8?q?=E6=A1=8C=E9=9D=A2=E7=AB=AF=E5=8F=AA=E4=BF=9D=E7=95=99=202.0.0.?= =?UTF-8?q?0=20=E5=AE=89=E8=A3=85=E5=8C=85=E4=B8=8E=E6=9B=B4=E6=96=B0?= =?UTF-8?q?=E7=BA=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 清掉 2.0.0.0 之前的 Updater,安装包从 1.9.9.22 换成 2.0.0.0 的 BT 源。 --- .../manifests/{1.9.9.22.xml => 2.0.0.0.xml} | 8 ++-- .../apps/ra3battlenet/manifests/manifests.xml | 2 +- Metadata/apps/ra3battlenet/ra3battlenet.xml | 38 ++----------------- 3 files changed, 9 insertions(+), 39 deletions(-) rename Metadata/apps/ra3battlenet/manifests/{1.9.9.22.xml => 2.0.0.0.xml} (58%) diff --git a/Metadata/apps/ra3battlenet/manifests/1.9.9.22.xml b/Metadata/apps/ra3battlenet/manifests/2.0.0.0.xml similarity index 58% rename from Metadata/apps/ra3battlenet/manifests/1.9.9.22.xml rename to Metadata/apps/ra3battlenet/manifests/2.0.0.0.xml index bd30e61..71a3ed4 100644 --- a/Metadata/apps/ra3battlenet/manifests/1.9.9.22.xml +++ b/Metadata/apps/ra3battlenet/manifests/2.0.0.0.xml @@ -3,13 +3,13 @@ ${TIMESTAMP} - - - Ra3BattleNet_Setup_1.9.9.22.exe + + + Ra3BattleNet_Setup_2.0.0.0.exe / APPLICATION - + diff --git a/Metadata/apps/ra3battlenet/manifests/manifests.xml b/Metadata/apps/ra3battlenet/manifests/manifests.xml index edabf5a..cd8ee44 100644 --- a/Metadata/apps/ra3battlenet/manifests/manifests.xml +++ b/Metadata/apps/ra3battlenet/manifests/manifests.xml @@ -7,6 +7,6 @@ - + \ No newline at end of file diff --git a/Metadata/apps/ra3battlenet/ra3battlenet.xml b/Metadata/apps/ra3battlenet/ra3battlenet.xml index 1a319ba..7114113 100644 --- a/Metadata/apps/ra3battlenet/ra3battlenet.xml +++ b/Metadata/apps/ra3battlenet/ra3battlenet.xml @@ -9,14 +9,14 @@ - 1.9.9.22 + 2.0.0.0 红色警戒3战网服务 Ra3BattleNet false - - 2026-11-03 - manifest-1.9.9.22 + + 2026-11-09 + manifest-2.0.0.0 @@ -24,36 +24,6 @@ - - - - - Date: Tue, 10 Nov 2026 00:07:22 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=A7=AA=20test(metadata):=20=E6=B8=85?= =?UTF-8?q?=E5=8D=95=E6=96=AD=E8=A8=80=E6=94=B9=E7=94=A8=E5=81=87=E6=B8=85?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 仓库样本里的版本号不再当断言,限定 ID、叶子发布和 Skudef 保序改由临时假清单验证。 --- .agents/docs/generated/notes-index.md | 3 +- .../2026-11-09-mock-manifest-fixtures.md | 26 +++ .../ManifestParseTests.cs | 4 +- .../PublishSurfaceTests.cs | 172 +++++++++++++----- Ra3.BattleNet.Metadata.Tests/QueryApiTests.cs | 5 +- Ra3.BattleNet.Metadata.Tests/StageATests.cs | 110 ++++++++--- 6 files changed, 252 insertions(+), 68 deletions(-) create mode 100644 .agents/notes/implemented/testing/2026-11-09-mock-manifest-fixtures.md diff --git a/.agents/docs/generated/notes-index.md b/.agents/docs/generated/notes-index.md index 5c3af13..ed33ea1 100644 --- a/.agents/docs/generated/notes-index.md +++ b/.agents/docs/generated/notes-index.md @@ -2,10 +2,11 @@ > 自动生成,勿手改。重新生成:`node scripts/verify-docs.mjs --write`。 -共 15 篇。按状态排序:提案 → 已实施 → 已否决。 +共 16 篇。按状态排序:提案 → 已实施 → 已否决。 | 日期 | 状态 | 分类 | 标题 | 问题 | |---|---|---|---|---| +| 2026-11-09 | implemented | testing | [清单行为用假清单测](../../notes/implemented/testing/2026-11-09-mock-manifest-fixtures.md) | 发布面和展平测试直接断言仓库里的真实清单路径与版本号。 | | 2026-11-06 | implemented | feature | [WebView2 zstd 包采用双层内容指纹](../../notes/implemented/feature/2026-11-06-webview2-raw-fingerprint.md) | 旧 WebView2 清单以 Compression="zip"、HashAlgorithm="None" 描述解包目录,不能验证下载物和待安装 ZIP 的内容。 | | 2026-11-04 | implemented | process | [Cloudflare 部署统一使用 Workers 静态资源](../../notes/implemented/process/2026-11-04-cloudflare-workers-deployment.md) | Cloudflare 后台项目是 Workers,但 npm 发布和本地服务仍调用 Pages 命令。 | | 2026-11-04 | implemented | process | [分支预览迁移到 Worker Previews](../../notes/implemented/process/2026-11-04-worker-previews.md) | 此前部署决定使用 wrangler versions upload 创建版本预览 URL,用户现在选择 Cloudflare 新的 Worker Previews 分支预览机制。 | diff --git a/.agents/notes/implemented/testing/2026-11-09-mock-manifest-fixtures.md b/.agents/notes/implemented/testing/2026-11-09-mock-manifest-fixtures.md new file mode 100644 index 0000000..042f465 --- /dev/null +++ b/.agents/notes/implemented/testing/2026-11-09-mock-manifest-fixtures.md @@ -0,0 +1,26 @@ +# Agent Note: 清单行为用假清单测 + +Status: implemented + +## Problem + +发布面和展平测试直接断言仓库里的真实清单路径与版本号。桌面端从 `1.9.9.22` 换成 `2.0.0.0` 之后,`PublishSurfaceTests` 和 `StageATests` 仍在找旧文件,CI 因此失败。清单版本会随发布变化,测试不该跟着改。 + +## Decision + +清单的限定 ID、叶子发布、变量替换和 Skudef 保序,全部用临时目录里的假清单验证。假清单走与正式数据相同的 `Include` 链,版本号固定为测试自己的 `9.9.9` / `1.0`,不读取 `Metadata/apps` 或 `Metadata/mods` 里的现行版本。 + +仓库样本测试只保留与具体清单无关的断言:发布物包含登记节点指向的资源,查询结果与登记 `Source` 一致。 + +## Alternatives considered + +- **把写死的版本号改成 2.0.0.0**:能让这次 CI 通过,但下一次发布还是会断。 +- **从仓库样本动态读当前清单再断言**:不写死版本,却仍依赖线上数据的目录和字段,样本一改测试就漂。 + +## Consequences + +发布换版本时不用再改单元测试。假清单必须自己满足 XSD 和构建校验,缺字段会在测试里失败,而不是在真实数据里被放过。 + +## Verification + +`dotnet test Metadata.sln --filter "FullyQualifiedName~StageATests|FullyQualifiedName~PublishSurfaceTests|FullyQualifiedName~ManifestParseTests|FullyQualifiedName~QueryApiTests"` diff --git a/Ra3.BattleNet.Metadata.Tests/ManifestParseTests.cs b/Ra3.BattleNet.Metadata.Tests/ManifestParseTests.cs index 375155e..0d7eaea 100644 --- a/Ra3.BattleNet.Metadata.Tests/ManifestParseTests.cs +++ b/Ra3.BattleNet.Metadata.Tests/ManifestParseTests.cs @@ -254,10 +254,10 @@ public void ManifestRegistration_ResolvesRegisteredId_AndNullWhenMissing() var root = MetadataBuilder.Load(Path.Combine(dst, "metadata.xml")); var registered = root.GetAllElements("Manifest") - .First(m => MetadataFlattener.LocalId(m.Get("ID")) == "manifest-3259"); + .First(m => !string.IsNullOrWhiteSpace(m.Get("ID")) && !string.IsNullOrWhiteSpace(m.Get("Source"))); var found = root.ManifestRegistration(registered.Get("ID")!); found.Should().BeSameAs(registered); - found!.Get("Source").Should().EndWith("manifests/3.259.xml"); + found!.Get("Source")!.Replace('\\', '/').Should().Contain("manifests/"); root.ManifestRegistration("no-such-manifest").Should().BeNull(); } diff --git a/Ra3.BattleNet.Metadata.Tests/PublishSurfaceTests.cs b/Ra3.BattleNet.Metadata.Tests/PublishSurfaceTests.cs index eb62a88..e26b8cf 100644 --- a/Ra3.BattleNet.Metadata.Tests/PublishSurfaceTests.cs +++ b/Ra3.BattleNet.Metadata.Tests/PublishSurfaceTests.cs @@ -44,8 +44,12 @@ public void Build_RepoSample_OutputHasOnlyFlattenedAndReferencedResources() File.ReadAllBytes(Path.Combine(dst, relative)) .Should().Equal(File.ReadAllBytes(Path.Combine(src, relative))); } - File.Exists(Path.Combine(dst, "mods", "corona", "manifests", "3.259.xml")).Should().BeTrue(); - File.Exists(Path.Combine(dst, "apps", "ra3battlenet", "manifests", "1.9.9.22.xml")).Should().BeTrue(); + foreach (var manifest in loaded.GetAllElements("Manifest").Where(m => m.Get("Source") is not null)) + { + var relative = manifest.Get("Source")!.Replace('/', Path.DirectorySeparatorChar); + File.Exists(Path.Combine(dst, relative)).Should().BeTrue($"登记清单必须进发布物: {relative}"); + File.Exists(Path.Combine(src, relative)).Should().BeTrue(); + } // _redirects 自动带出 File.Exists(Path.Combine(dst, "_redirects")).Should().BeTrue(); @@ -104,16 +108,123 @@ public void Build_UrlOnlyImage_CopiesNothingForIt() } [TestMethod] - public void Build_RepoSample_LeafManifestIsFlattenedAndLoadable() + public void Build_MockLeafManifest_IsFlattenedAndLoadable() { - var dst = Path.Combine(Path.GetTempPath(), $"publish-leaf-{Guid.NewGuid():N}"); - + var temp = Path.Combine(Path.GetTempPath(), $"publish-mock-leaf-{Guid.NewGuid():N}"); + var src = Path.Combine(temp, "src"); + var dst = Path.Combine(temp, "out"); + SeedSchemas(src); try { - MetadataBuilder.Build(RepoMetadataDir, dst, schemaVersion: "1.0", contentRevision: "publish-test"); + Directory.CreateDirectory(Path.Combine(src, "mods", "mockmod", "manifests")); + Directory.CreateDirectory(Path.Combine(src, "apps", "mockruntime", "manifests")); + File.WriteAllText(Path.Combine(src, "metadata.xml"), """ + + + + + + + + """); + File.WriteAllText(Path.Combine(src, "mods", "mockmod", "mockmod.xml"), """ + + + + + + + 1.0 + 假模组 + MockMod + + + manifest-mock + + + + + + """); + File.WriteAllText(Path.Combine(src, "mods", "mockmod", "manifests", "1.0.xml"), """ + + + + ${TIMESTAMP} + + + + + + + + + + + core.big + / + MISC; + + + + + + text.big + / + MISC; + + + + + + base.lyi + / + MOD; + + + + + + + """); + File.WriteAllText(Path.Combine(src, "apps", "mockruntime", "mockruntime.xml"), """ + + + + + + + 1.0 + 假运行时 + MockRuntime + + + manifest-runtime + + + + + + """); + File.WriteAllText(Path.Combine(src, "apps", "mockruntime", "manifests", "1.0.xml"), """ + + + + + runtime.zip + / + RUNTIME + + + + + + + """); + + MetadataBuilder.Build(src, dst, schemaVersion: "1.0", contentRevision: "publish-mock"); - // 叶子清单不能照抄源文件:客户端解析器拒绝 Includes,也不该收到未替换的变量 - var leafPath = Path.Combine(dst, "mods", "corona", "manifests", "3.259.xml"); + var leafPath = Path.Combine(dst, "mods", "mockmod", "manifests", "1.0.xml"); var text = File.ReadAllText(leafPath); text.Should().NotContain(" f.Sources.Count > 0); entry.Files.Should().Contain(f => f.DownloadName != null); - var runtime = MetadataBuilder.Load(Path.Combine(dst, "apps", "webview2", "manifests", "156.0.4314.58.xml")) + var runtime = MetadataBuilder.Load(Path.Combine(dst, "apps", "mockruntime", "manifests", "1.0.xml")) .Find("Manifest")!.ToManifestEntry(); runtime.HashAlgorithm.Should().Be("SHA256"); var file = runtime.Files.Should().ContainSingle().Subject; - file.FileName.Should().Be("WebView2Runtime.zip"); - file.DownloadName.Should().Be("WebView2Runtime.zip.zst"); + file.FileName.Should().Be("runtime.zip"); + file.DownloadName.Should().Be("runtime.zip.zst"); file.Compression.Should().Be("zstd"); - file.Size.Should().Be(233263294); + file.Size.Should().Be(64); file.Hash.Should().Be("31de4e57a19017eee55e6f66ddb9f6c217f944716dee2cd14a37f72dfbbe831b"); file.RawHash.Should().Be("b601d79b38720d174c17b6affed3506f01c5033d20a42d1f90fe5745f1b8d021"); file.RawHashAlgorithm.Should().Be("SHA256"); file.Sources.Should().ContainSingle().Which.Url.Should() - .Be("https://file.cor-games.com/srv/redist/WebView2Runtime.zip.zst.torrent"); + .Be("https://example.invalid/runtime.zip.zst.torrent"); var exported = MetadataXml.Write(file.Raw); exported.Should().Contain($"RawHash=\"{file.RawHash}\"").And.Contain("RawHashAlgorithm=\"SHA256\""); - } - finally - { - if (Directory.Exists(dst)) - Directory.Delete(dst, recursive: true); - } - } - [TestMethod] - public void Build_RepoSample_SkudefSurvivesFlattenInDeclaredOrder() - { - var dst = Path.Combine(Path.GetTempPath(), $"publish-skudef-{Guid.NewGuid():N}"); - - try - { - MetadataBuilder.Build(RepoMetadataDir, dst, schemaVersion: "1.0", contentRevision: "publish-test"); - - var entry = MetadataBuilder.Load(Path.Combine(dst, "mods", "corona", "manifests", "3.259.xml")) - .Find("Manifest")!.ToManifestEntry(); var skudef = entry.Skudef; - skudef.Should().NotBeNull("展平后叶子清单要保住 Skudef"); skudef!.GameVersion.Should().Be("1.12"); skudef.Commands.Select(c => c.Target).Should().ContainInOrder( - "CustomConfig.txt", "Disabler.big", "HighResShadow.big", "Cor_ENG_3.250.big", - "coronaBGM_3.228.lyi", "corona_3.259.lyi", "StaticVersion.big"); + "CustomConfig.txt", "core.big", "text.big", "base.lyi"); skudef.Commands.Should().ContainSingle(c => c.Target == "CustomConfig.txt" && c.Kind == SkudefCommandKind.Config && c.Optional); - skudef.Commands.Should().Contain(c => c.Kind == SkudefCommandKind.Big && c.Target == "Disabler.big" && c.Package == "disable-sky-and-patches"); - skudef.Commands.Should().Contain(c => c.Kind == SkudefCommandKind.Big && c.Target == "HighResShadow.big" && c.Package == "hd-shadow"); - skudef.Commands.Should().Contain(c => c.Kind == SkudefCommandKind.Big && c.Target == "Cor_ENG_3.250.big" && c.Language == "en"); - skudef.Commands.Should().Contain(c => c.Kind == SkudefCommandKind.Big && c.Target == "StaticVersion.big" && c.Package == "world-builder"); - - // 加载条件单源:File 上不能再留 Mount/Language/Package + skudef.Commands.Should().Contain(c => c.Kind == SkudefCommandKind.Big && c.Target == "core.big" && c.Package == "optional-pack"); + skudef.Commands.Should().Contain(c => c.Kind == SkudefCommandKind.Big && c.Target == "text.big" && c.Language == "en"); entry.Files.Should().OnlyContain(f => f.Mount == "base" && f.Language == null && f.Package == null); } finally { - if (Directory.Exists(dst)) - Directory.Delete(dst, recursive: true); + if (Directory.Exists(temp)) + Directory.Delete(temp, recursive: true); } } + } diff --git a/Ra3.BattleNet.Metadata.Tests/QueryApiTests.cs b/Ra3.BattleNet.Metadata.Tests/QueryApiTests.cs index 5e65f97..61095f5 100644 --- a/Ra3.BattleNet.Metadata.Tests/QueryApiTests.cs +++ b/Ra3.BattleNet.Metadata.Tests/QueryApiTests.cs @@ -64,7 +64,10 @@ public void Mod_ManifestSource_ResolvesRegisteredLeafManifest() var corona = root.Catalog().Mod("Corona")!; corona.Package().Should().NotBeNull("版本缺省取实体当前版本"); - corona.ManifestSource().Replace('\\', '/').Should().EndWith("manifests/3.259.xml"); + var declared = corona.Package()!.ManifestId; + declared.Should().NotBeNullOrWhiteSpace(); + corona.ManifestSource().Replace('\\', '/').Should().Be( + root.ManifestRegistration(declared!)!.Get("Source")!.Replace('\\', '/')); var act = () => corona.ManifestSource("9.9.9"); act.Should().Throw().WithMessage("*没有版本*"); diff --git a/Ra3.BattleNet.Metadata.Tests/StageATests.cs b/Ra3.BattleNet.Metadata.Tests/StageATests.cs index bcf4ce2..b703364 100644 --- a/Ra3.BattleNet.Metadata.Tests/StageATests.cs +++ b/Ra3.BattleNet.Metadata.Tests/StageATests.cs @@ -1,4 +1,4 @@ -using System.Xml.Linq; +using System.Xml.Linq; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -45,27 +45,7 @@ public void Build_RepoSample_FlattensWithoutInclude_AndResolvesIds() loaded.Applications().Should().Contain(a => a.Id == "RA3BattleNet"); loaded.Mods().Should().Contain(m => m.Id == "Corona"); - // 发布 ID = {路径前缀}:{localId} - var registries = loaded.GetAllElements("Manifest") - .Where(m => !string.IsNullOrEmpty(m.Get("ID"))).ToList(); - var manifest = registries - .First(m => MetadataFlattener.LocalId(m.Get("ID")) == "manifest-1.9.9.22"); - manifest.Get("ID")!.Should().Contain(":"); - manifest.Get("ID")!.Should().EndWith(":manifest-1.9.9.22"); - var source = manifest.Get("Source"); - source.Should().NotBeNullOrWhiteSpace(); - source!.Replace('\\', '/').Should().EndWith("manifests/1.9.9.22.xml"); - source.Should().NotContain("apps.xml"); - var manifestPath = Path.Combine(dst, source.Replace('/', Path.DirectorySeparatorChar)); - File.Exists(manifestPath).Should().BeTrue(); - File.ReadAllText(manifestPath).Should().Contain(" MetadataFlattener.LocalId(m.Get("ID")!) == "manifest-3259"); - coronaManifest.Get("Source")!.Replace('\\', '/').Should().EndWith("manifests/3.259.xml"); - // 引用已改写为限定 ID(示例数据里只剩 Mod 带新闻) - var app = loaded.Applications().Single(a => a.Id == "RA3BattleNet"); var modDoc = XDocument.Load(Path.Combine(src, "mods", "corona", "corona.xml")); var modXml = Path.Combine(src, "mods", "corona", "corona.xml"); var localContentId = modDoc.Descendants("Mod") @@ -113,7 +93,6 @@ public void Build_RepoSample_FlattensWithoutInclude_AndResolvesIds() File.ReadAllText(Path.Combine(dst, markdown.Source!.Replace('/', Path.DirectorySeparatorChar))) .Should().Be(sourceBody); - app.Packages.Single(p => p.Version == "1.9.9.22").ManifestId.Should().Be(manifest.Get("ID")); var corona = loaded.Mods().Single(m => m.Id == "Corona"); var icon = loaded.Images().Single(image => image.Id == corona.Icon); var sourceDoc = XDocument.Load(Path.Combine(src, "mods", "corona", "corona.xml")); @@ -127,6 +106,93 @@ public void Build_RepoSample_FlattensWithoutInclude_AndResolvesIds() } } + [TestMethod] + public void Build_MockManifest_QualifiesIdAndPublishesLeaf() + { + var temp = Path.Combine(Path.GetTempPath(), $"stage-a-mock-{Guid.NewGuid():N}"); + var src = Path.Combine(temp, "src"); + var dst = Path.Combine(temp, "out"); + SeedSchemas(src); + try + { + Directory.CreateDirectory(Path.Combine(src, "apps", "mockapp", "manifests")); + File.WriteAllText(Path.Combine(src, "metadata.xml"), """ + + + + + + + """); + File.WriteAllText(Path.Combine(src, "apps", "mockapp", "mockapp.xml"), """ + + + + + + + 9.9.9 + 假应用 + MockApp + + + manifest-mock + + + + + + """); + File.WriteAllText(Path.Combine(src, "apps", "mockapp", "manifests", "manifests.xml"), """ + + + + + + + """); + File.WriteAllText(Path.Combine(src, "apps", "mockapp", "manifests", "9.9.9.xml"), """ + + + + ${TIMESTAMP} + + + + MockApp_Setup.exe + / + APPLICATION + + + + + + + """); + + MetadataBuilder.Build(src, dst, schemaVersion: "1.0", contentRevision: "mock-rev"); + + var loaded = MetadataBuilder.Load(Path.Combine(dst, "metadata.xml")); + var manifest = loaded.GetAllElements("Manifest") + .Single(m => MetadataFlattener.LocalId(m.Get("ID")) == "manifest-mock"); + manifest.Get("ID").Should().Be("apps/mockapp/manifests/9.9.9:manifest-mock"); + var source = manifest.Get("Source")!.Replace('\\', '/'); + source.Should().Be("apps/mockapp/manifests/9.9.9.xml"); + source.Should().NotContain("apps.xml"); + + var manifestPath = Path.Combine(dst, source.Replace('/', Path.DirectorySeparatorChar)); + var leaf = File.ReadAllText(manifestPath); + leaf.Should().Contain("