diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2899ddd..db008a0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,7 @@ name: CI on: push: - branches: [main, dev] + branches: [main] pull_request: workflow_dispatch: diff --git a/AGENTS.md b/AGENTS.md index 7a96886..82a95d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -74,3 +74,29 @@ Telnet / 串口见 [`zh/host/Telnet与串口可行性调研.md`](https://github. ### 留在本仓库的文档 `README.md`、`LICENSE`,以及各插件目录下的 `README.md`(该插件自己的实现说明与偏离记录)。 + +### 测试:不要把 `await` 写进带集合实参的断言里 + +本仓库跑在 `net11.0` + `LangVersion preview`(C# 的 first-class span)。在这套语义下, +`byte[]` / 集合表达式实参会**隐式转成 `ReadOnlySpan`** 再传进 +`Assert.AreSequenceEqual`、`MemoryExtensions.SequenceEqual`、`StartsWith`、`IndexOf` 之类的重载。 + +于是这一行是错的: + +```csharp +Assert.AreSequenceEqual(content, await File.ReadAllBytesAsync(local)); // ❌ +``` + +实参从左往右求值:`content` 先转成 `ReadOnlySpan`,然后在第二个实参的 `await` 处挂起。 +span 是 ref struct,跨不了挂起点 —— 恢复之后拿到的是**空 span**,断言无条件失败。 +编译器**不报错也不告警**,只有真正走异步(await 没同步完成)时才现形, +表现为「单跑绿、一起跑红」「本地绿、CI 红」的假不稳定。 + +正确写法是先把 `await` 落到局部变量: + +```csharp +byte[] downloaded = await File.ReadAllBytesAsync(local); // ✅ +Assert.AreSequenceEqual(content, downloaded); +``` + +同一条规则适用于任何 span 接收者或 span 实参的调用:**调用的实参列表里不许出现 `await`**。 diff --git a/Directory.Packages.props b/Directory.Packages.props index 64e46cb..5335eeb 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -39,7 +39,7 @@ - + - + @@ -58,4 +58,4 @@ - \ No newline at end of file + diff --git a/global.json b/global.json index 8976695..7e7e286 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "11.0.100-preview.7.26381.103", + "version": "11.0.100-rc.1.26425.128", "rollForward": "latestFeature", "allowPrerelease": true } diff --git a/tests/VelaShell.Plugin.Redis.Tests/RedisPanelEditingUiTests.cs b/tests/VelaShell.Plugin.Redis.Tests/RedisPanelEditingUiTests.cs index 8ef6f66..3068846 100644 --- a/tests/VelaShell.Plugin.Redis.Tests/RedisPanelEditingUiTests.cs +++ b/tests/VelaShell.Plugin.Redis.Tests/RedisPanelEditingUiTests.cs @@ -696,7 +696,8 @@ public void BinaryValue_MalformedEscape_RefusesToWrite() await PumpAsync(); Assert.Contains("转义写坏了", vm.StatusMessage); - Assert.AreSequenceEqual(original, await ReadRawAsync("bad:blob"), "拒绝写入时服务端的值必须原封不动。"); + byte[] stored = await ReadRawAsync("bad:blob"); + Assert.AreSequenceEqual(original, stored, "拒绝写入时服务端的值必须原封不动。"); }); } diff --git a/tests/VelaShell.Plugin.Redis.Tests/RedisStoreTests.cs b/tests/VelaShell.Plugin.Redis.Tests/RedisStoreTests.cs index 2ae0c14..6452499 100644 --- a/tests/VelaShell.Plugin.Redis.Tests/RedisStoreTests.cs +++ b/tests/VelaShell.Plugin.Redis.Tests/RedisStoreTests.cs @@ -22,8 +22,10 @@ public async Task Favorites_RoundTripPerConnection() await store.SaveFavoritesAsync("redis.example:6379", ["user:1", "lock:a"]); await store.SaveFavoritesAsync("10.0.0.2:6379", ["other:1"]); - Assert.AreSequenceEqual(["user:1", "lock:a"], [.. (await store.LoadFavoritesAsync("redis.example:6379"))]); - Assert.AreSequenceEqual(["other:1"], [.. (await store.LoadFavoritesAsync("10.0.0.2:6379"))]); + List favorites = [.. await store.LoadFavoritesAsync("redis.example:6379")]; + Assert.AreSequenceEqual(["user:1", "lock:a"], favorites); + List otherFavorites = [.. await store.LoadFavoritesAsync("10.0.0.2:6379")]; + Assert.AreSequenceEqual(["other:1"], otherFavorites); } [TestMethod] @@ -59,8 +61,10 @@ public async Task History_IsScopedPerConnection() await store.AppendHistoryAsync("a:6379", "PING"); await store.AppendHistoryAsync("b:6379", "INFO"); - Assert.AreSequenceEqual(["PING"], [.. (await store.LoadHistoryAsync("a:6379"))]); - Assert.AreSequenceEqual(["INFO"], [.. (await store.LoadHistoryAsync("b:6379"))]); + List first = [.. await store.LoadHistoryAsync("a:6379")]; + Assert.AreSequenceEqual(["PING"], first); + List second = [.. await store.LoadHistoryAsync("b:6379")]; + Assert.AreSequenceEqual(["INFO"], second); } [TestMethod] diff --git a/tests/VelaShell.Plugin.S3.Tests/S3FileServiceIntegrationTests.cs b/tests/VelaShell.Plugin.S3.Tests/S3FileServiceIntegrationTests.cs index 9c0700b..ad4cc40 100644 --- a/tests/VelaShell.Plugin.S3.Tests/S3FileServiceIntegrationTests.cs +++ b/tests/VelaShell.Plugin.S3.Tests/S3FileServiceIntegrationTests.cs @@ -306,7 +306,11 @@ public async Task Download_WritesExactContent() await _service.DownloadFileAsync(_session, "/test-bucket/data/blob.bin", local, new SynchronousProgress(progress.Add)); - Assert.AreSequenceEqual(content, await File.ReadAllBytesAsync(local)); + // 读回的字节先落到局部变量:把 await 写进 Assert.AreSequenceEqual 的实参里, + // 第一个实参会先转成 ReadOnlySpan,再在 await 处挂起 —— 恢复后那个 span 是空的, + // 断言于是无条件失败。这是 C# preview「first-class span」下的编译器坑,详见 AGENTS.md。 + byte[] downloaded = await File.ReadAllBytesAsync(local); + Assert.AreSequenceEqual(content, downloaded); Assert.AreEqual(progress[^1].TotalBytes, progress[^1].TransferredBytes, "最后一次上报必须是满进度。"); AssertAllRequestsSigned(); } @@ -337,7 +341,8 @@ public async Task Download_HeadDenied_StillDownloadsViaGet() await _service.DownloadFileAsync(_session, "/test-bucket/public/asset.png", local, new SynchronousProgress(progress.Add)); - Assert.AreSequenceEqual(content, await File.ReadAllBytesAsync(local)); + byte[] downloaded = await File.ReadAllBytesAsync(local); + Assert.AreSequenceEqual(content, downloaded); // 总长度只能来自 GET 响应,但进度依然要收在满格上。 Assert.AreEqual(content.Length, progress[^1].TotalBytes); Assert.AreEqual(progress[^1].TotalBytes, progress[^1].TransferredBytes, "最后一次上报必须是满进度。"); @@ -365,7 +370,8 @@ public async Task Download_DirectReadDenied_FallsBackToPresignedUrl() await _service.DownloadFileAsync(_session, "/test-bucket/locked/asset.bin", local, new SynchronousProgress(progress.Add)); - Assert.AreSequenceEqual(content, await File.ReadAllBytesAsync(local)); + byte[] downloaded = await File.ReadAllBytesAsync(local); + Assert.AreSequenceEqual(content, downloaded); Assert.AreEqual(content.Length, progress[^1].TotalBytes); Assert.AreEqual(progress[^1].TotalBytes, progress[^1].TransferredBytes, "最后一次上报必须是满进度。"); // 预签名那次也必须是签对的:服务器重算签名,对不上会计入 SignatureFailures。 @@ -430,7 +436,8 @@ public async Task Download_ResumesWithRangeRequest() await _service.DownloadFileAsync(_session, "/test-bucket/resume.bin", local, resumeOffset: 1000); - Assert.AreSequenceEqual(content, await File.ReadAllBytesAsync(local)); + byte[] downloaded = await File.ReadAllBytesAsync(local); + Assert.AreSequenceEqual(content, downloaded); AssertAllRequestsSigned(); } finally