feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
This commit is contained in:
136
CodexOutput/upserver/KVGameLoadTests/CP2AExistingArchiveTests.cs
Normal file
136
CodexOutput/upserver/KVGameLoadTests/CP2AExistingArchiveTests.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using game.PlayerDataStorage;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
static partial class Tests
|
||||
{
|
||||
static async Task RunCP2AExistingArchiveTests()
|
||||
{
|
||||
await Run("CP2A-formal-load-rejects-new-or-empty-archive-before-apply", () => OnMain(CP2AExistingArchive));
|
||||
await Run("CP2A-formal-transport-rejects-shared-inner-before-login", () => OnMain(CP2ASharedInner));
|
||||
await Run("CP2A-formal-close-failure-revokes-handle-and-blocks-next", () => OnMain(CP2ACloseFailure));
|
||||
await Run("CP2A-formal-sockets-without-cookie-contract-rejected", () =>
|
||||
{
|
||||
var capability=PlayerKVControlledLoadCapability.CreateForGame(_ => new SocketsHttpHandler());
|
||||
bool rejected=false;
|
||||
try { using var handler=capability.CreateHandler(new CookieContainer()); }
|
||||
catch(InvalidOperationException) { rejected=true; }
|
||||
Check(rejected,"formal factory must not accept an unverified SocketsHttpHandler cookie policy");
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
static async Task CP2ACloseFailure()
|
||||
{
|
||||
var f = new LoadFixture();
|
||||
var formal = new Uri(PlayerKVRuntimeSelection.FormalApiUrl);
|
||||
var capability = PlayerKVGameLoadCapability.CreateForGame(
|
||||
new PlayerKVIdentitySnapshot(null, false, PlayerKVIdentityChannel.none, Array.Empty<PlayerKVIdentityChannel>()),
|
||||
SchemaSnapshot(), formal, Path.Combine(Root, "cp2a", Guid.NewGuid().ToString("N")),
|
||||
jar => new Handler((_, _) =>
|
||||
{
|
||||
jar.Add(formal, new Cookie("sid", "synthetic"));
|
||||
return Task.FromResult(Json(JsonConvert.SerializeObject(new {
|
||||
state = 0, internalId = 17, newCreate = false,
|
||||
createTime = "2026-01-01T00:00:00Z", svrTime = "2026-01-02T00:00:00Z"
|
||||
})));
|
||||
}),
|
||||
_ => new Handler((_, _) => Task.FromResult(Json(JsonConvert.SerializeObject(new { data = ModernRaw() })))),
|
||||
_ => new Handler((_, _) => Task.FromResult(Json("{}")), () => throw new InvalidOperationException("synthetic close")));
|
||||
var result = await f.Coordinator.LoadForGameAsync("101", capability);
|
||||
var hasOriginal=f.Coordinator.TryGetCurrentGameHandle(result.Epoch, out var original);
|
||||
Check(result.State == PlayerKVGameLoadState.GameLoaded && hasOriginal, "formal handle published");
|
||||
var closing = f.Coordinator.Revoke();
|
||||
Check(!f.Coordinator.TryGetCurrentGameHandle(result.Epoch, out _) &&
|
||||
!original.ReadLocalSnapshot().Success, "close invalidates handle synchronously");
|
||||
try { await closing; throw new Exception("close fault was ignored"); }
|
||||
catch(InvalidOperationException ex) { Check(ex.Message.Contains("synthetic close"), "close failure retained"); }
|
||||
var next = new LoadFixture(f.Registry);
|
||||
var denied = await next.Coordinator.LoadForGameAsync("102", next.Capability());
|
||||
Check(denied.Error == PlayerKVGameLoadError.PreviousCloseFailed && next.Auth == 0,
|
||||
"failed close prevents next context and sends no authentication");
|
||||
}
|
||||
|
||||
sealed class CP2AWrapper : DelegatingHandler
|
||||
{ internal CP2AWrapper(HttpMessageHandler inner) { InnerHandler = inner; } }
|
||||
|
||||
static async Task CP2ASharedInner()
|
||||
{
|
||||
var f = new LoadFixture();
|
||||
int sent = 0;
|
||||
var shared = new Handler((_, _) => { sent++; return Task.FromResult(Json("{}")); });
|
||||
var capability = PlayerKVGameLoadCapability.CreateForGame(
|
||||
new PlayerKVIdentitySnapshot(null, false, PlayerKVIdentityChannel.none, Array.Empty<PlayerKVIdentityChannel>()),
|
||||
SchemaSnapshot(), new Uri(PlayerKVRuntimeSelection.FormalApiUrl),
|
||||
Path.Combine(Root, "cp2a", Guid.NewGuid().ToString("N")),
|
||||
_ => new CP2AWrapper(shared), _ => new CP2AWrapper(shared),
|
||||
_ => new Handler((_, _) => Task.FromResult(Json("{}"))));
|
||||
var result = await f.Coordinator.LoadForGameAsync("101", capability);
|
||||
Check(result.State == PlayerKVGameLoadState.Rejected && sent == 0,
|
||||
"shared inner handler graph must fail before account/Login");
|
||||
await f.Coordinator.Revoke();
|
||||
}
|
||||
|
||||
static async Task CP2AExistingArchive()
|
||||
{
|
||||
var method = typeof(PlayerKVGameLoadCapability).GetMethod("CreateForGame", BindingFlags.Static | BindingFlags.NonPublic);
|
||||
Check(method != null, "formal factory must exist without using editor validation factory");
|
||||
var formal = new Uri("http://s101.local:18080/");
|
||||
foreach (var (newlyCreated, empty, accepted) in new[] { (true, false, false), (false, true, false), (false, false, true) })
|
||||
{
|
||||
var f = new LoadFixture();
|
||||
CookieContainer jar = null;
|
||||
Func<CookieContainer, HttpMessageHandler> auth = cookies =>
|
||||
{
|
||||
jar = cookies;
|
||||
return new Handler((request, _) =>
|
||||
{
|
||||
Check(request.RequestUri.AbsolutePath == "/account/Login", "exactly one account login route");
|
||||
f.Auth++;
|
||||
cookies.Add(formal, new Cookie("sid", "synthetic"));
|
||||
return Task.FromResult(Json(JsonConvert.SerializeObject(new {
|
||||
state = 0, internalId = 17, newCreate = newlyCreated,
|
||||
createTime = "2026-01-01T00:00:00Z", svrTime = "2026-01-02T00:00:00Z"
|
||||
})));
|
||||
});
|
||||
};
|
||||
Func<CookieContainer, HttpMessageHandler> kv = cookies =>
|
||||
{
|
||||
Check(ReferenceEquals(cookies, jar), "KV uses authentication cookie jar");
|
||||
return new Handler((request, _) =>
|
||||
{
|
||||
Check(request.RequestUri.AbsolutePath == "/playerdata/Get", "read-only KV fetch");
|
||||
f.Gets++;
|
||||
return Task.FromResult(Json(JsonConvert.SerializeObject(new {
|
||||
data = empty ? new Dictionary<string, string>() : ModernRaw()
|
||||
})));
|
||||
});
|
||||
};
|
||||
Func<CookieContainer, HttpMessageHandler> lease = cookies =>
|
||||
{
|
||||
Check(ReferenceEquals(cookies, jar), "account lease uses authentication cookie jar");
|
||||
return new Handler((_, _) => throw new InvalidOperationException("CP2A must send no business API request"));
|
||||
};
|
||||
var capability = (PlayerKVGameLoadCapability)method.Invoke(null, new object[] {
|
||||
new PlayerKVIdentitySnapshot(null, false, PlayerKVIdentityChannel.none, Array.Empty<PlayerKVIdentityChannel>()),
|
||||
SchemaSnapshot(), formal, Path.Combine(Root, "cp2a", Guid.NewGuid().ToString("N")), auth, kv, lease
|
||||
});
|
||||
try
|
||||
{
|
||||
var result = await f.Coordinator.LoadForGameAsync("101", capability);
|
||||
Check((result.State == PlayerKVGameLoadState.GameLoaded) == accepted,
|
||||
"only a pre-existing complete archive becomes game-loaded");
|
||||
Check(f.Auth == 1 && f.Gets == 1, "no second login or KV fetch");
|
||||
Check(f.Notify == (accepted ? 1 : 0), "ineligible archive never reaches Apply notification");
|
||||
if (accepted)
|
||||
Check(f.Coordinator.TryGetCurrentGameHandle(result.Epoch, out var owner) && owner.PlayerId == "101",
|
||||
"eligible archive yields original fixed account handle");
|
||||
else
|
||||
Check(!f.Coordinator.TryGetCurrentGameHandle(result.Epoch, out _), "rejected archive has no usable handle");
|
||||
}
|
||||
finally { await f.Coordinator.Revoke(); }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user