feat(upserver): 保存M2客户端KV迁移至本人统计写检查点

保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
This commit is contained in:
Liubing\LB
2026-09-30 19:19:05 +08:00
parent 1c0cdf7a99
commit 6813da149a
511 changed files with 151858 additions and 2376 deletions

View File

@@ -20,6 +20,7 @@ static partial class Tests
await Run("B2-C1-metadata-one-auth-shared-cookie", SingleAuthentication);
await Run("B2-C1-lease-once-before-after-close", Admission);
await Run("B2-C1-denied-routes-zero-send", Routes);
await Run("CP2B-T-formal-lease-route-and-cookie-isolation", FormalBusinessLease);
await Run("B2-C1-real-handler-rejected", RealHandlerRejected);
await Run("B2-C1-cyclic-handler-rejected-without-recursive-dispose", CyclicHandlerRejected);
await Run("B2-C1-active-send-drains-before-session", Drain);
@@ -56,6 +57,8 @@ static partial class Tests
await RunLoginSchemaTests();
await RunAggregateTests();
await RunCoordinatorTests();
await RunCP2ARuntimeSelectionTests();
await RunCP2AExistingArchiveTests();
var report = new { runId = RunId, checkpoint = "4-validation", failures, assertions, accessedFormalServer = false, tests = Results };
await File.WriteAllTextAsync(Path.Combine(Root, "result.json"), JsonConvert.SerializeObject(report, Formatting.Indented));
Console.WriteLine($"RESULT tests={Results.Count} failed={failures} assertions={assertions} run={RunId}");
@@ -140,6 +143,12 @@ static partial class Tests
return new Handler((r, t) => { BorrowedSends++; return Borrowed(r, t); }, () =>
{ LeaseDisposes++; Record("lease"); if (FailLeaseDispose) throw new IOException("synthetic cleanup fault"); });
});
internal PlayerKVControlledLoadCapability FormalCapability() => PlayerKVControlledLoadCapability.CreateForGame(jar =>
{
Check(ReferenceEquals(Jar, jar), "formal lease shares exact cookie identity");
return new Handler((r, t) => { BorrowedSends++; return Borrowed(r, t); }, () =>
{ LeaseDisposes++; Record("lease"); if (FailLeaseDispose) throw new IOException("synthetic cleanup fault"); });
});
internal async Task<PlayerKVAccountLease> PreparedLease()
{
Check((await Context.PrepareAsync()).Prepared, "prepared");
@@ -176,6 +185,42 @@ static partial class Tests
await Throws<ArgumentException>(() => l.SendAsync(HttpMethod.Get, route));
Check(f.BorrowedSends == 0, "rejected routes never sent"); await f.Context.CloseAsync();
}
static async Task FormalBusinessLease()
{
var f = new Fixture(); Check((await f.Context.PrepareAsync()).Prepared, "formal prepared");
Check(f.Context.TryAcquireAccountLease(f.FormalCapability(), out var lease), "formal lease admitted");
Check(lease.IsFormal && ReferenceEquals(lease.FormalCookies, f.Jar), "immutable formal mode and shared jar");
var formalClient=(HttpClient)typeof(PlayerKVAccountLease).GetField("client", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(lease)!;
Check(formalClient.Timeout == TimeSpan.FromSeconds(15), "formal transport retains old 15 second timeout");
f.Borrowed = async (request, _) =>
{
Check(request.RequestUri.AbsolutePath == "/mail/fetch", "canonical business path");
Check(request.Method == HttpMethod.Post, "POST method retained");
Check(request.Content.Headers.ContentType.MediaType == "application/json", "JSON content type retained");
Check(await request.Content.ReadAsStringAsync() == "{}", "body retained");
return Json("business");
};
var response = await lease.SendAsync(HttpMethod.Post, "mail/fetch", "{}");
Check(response.IsCurrent && response.StatusCode == 200 && response.Body == "business", "formal response retained");
foreach (var route in new[] { "__kv_validation/ok", "__kv_validation/unauthorized", "account/Login", "playerdata/Get", "/mail/fetch", "//other/x", "http://other/x", "mail/fetch?x=1", "mail/%66etch", "mail/../fetch", "mail\\fetch", "mail//fetch" })
await Throws<ArgumentException>(() => lease.SendAsync(HttpMethod.Get, route));
Check(f.BorrowedSends == 1, "invalid formal routes zero send");
await f.Context.CloseAsync();
var v = new Fixture(); var validation = await v.PreparedLease();
Check(!validation.IsFormal && validation.FormalCookies == null, "validation never exposes formal cookie");
var validationClient=(HttpClient)typeof(PlayerKVAccountLease).GetField("client", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(validation)!;
Check(validationClient.Timeout == TimeSpan.FromSeconds(100), "validation timeout unchanged");
await Throws<ArgumentException>(() => validation.SendAsync(HttpMethod.Get, "mail/fetch"));
Check(v.BorrowedSends == 0, "validation ordinary path zero send");
await v.Context.CloseAsync();
var broken = new Fixture { FailLeaseDispose = true };
Check((await broken.Context.PrepareAsync()).Prepared, "failure case prepared");
Check(broken.Context.TryAcquireAccountLease(broken.FormalCapability(), out var failedLease), "failure case formal lease");
var failedClose = broken.Context.CloseAsync();
await Throws<IOException>(() => failedClose);
Check(!failedLease.IsCurrent && failedLease.FormalCookies == null &&
ReferenceEquals(failedClose, broken.Context.CloseAsync()), "formal close failure remains closed");
}
static async Task RealHandlerRejected()
{
var f = new Fixture(); await f.Context.PrepareAsync();