using Admin.NET.Core.Update;
using Xunit;
namespace Admin.NET.Plugin.AiDOP.Tests.Migration;
///
/// 已登记「迁移记录被误记」重整策略的契约。
///
/// 被守护的核心不变量有两条,方向相反、同等重要:
///
/// ① 不能误伤正常环境。闸门 1–4 任一不成立必须返回 None——
/// 全新环境(无日志行)、已升级完成的环境(hash 已是目标值)、失败重试中的环境(非 Success),
/// 这三种都是正常状态,一旦在这里 Reject 就是把好好的库打死。
/// 其中「已升级完成」最危险:它与事故环境只差 file_hash 一个字段。
///
/// ② 命中事故签名之后不能沉默放行。闸门 5–7 是安全前提,
/// 任一不成立必须 Reject 停机,绝不允许降级成「跳过重整继续启动」——
/// 该通道会修改迁移历史,沉默放行等于在无人知晓的情况下改写执行史。
///
/// 策略是纯函数,故这些用例不需要数据库;副作用(UPDATE / 抛异常)由
/// AutoVersionUpdate 执行,其真实运行结果由隔离库的 First Run / Second Run 覆盖。
///
public class MigrationCollisionPolicyTests
{
private const string V = "1.0.536";
private const string CollidedHash = "970E5A8F0133944CB04AC95E67112A2EC6E2E45F06AE61C3F2049163F7DC9A61";
private const string TargetHash = "5A18A15AB379167A151C317A18617E2841EC4E808C8FD727216360D81EA76776";
private const string DisplacedV = "1.0.539";
private const string DisplacedHash = "9B627E41C15EBB5195C035592870E56A9EDE5704FA423DDEC5A4A8FF94A6CE42";
/// 事故现场的完整参数;各用例只改动自己要验证的那一个维度。
private static MigrationCollisionOutcome Decide(
string version = V,
int matchedRowCount = 1,
string? loggedStatus = "Success",
string? loggedFileHash = CollidedHash,
bool targetDiskHashMatches = true,
string? displacedLoggedStatus = "Success",
string? displacedLoggedFileHash = DisplacedHash,
bool displacedDiskHashMatches = true)
=> MigrationCollisionPolicy.Decide(
version, matchedRowCount, loggedStatus, loggedFileHash, targetDiskHashMatches,
displacedLoggedStatus, displacedLoggedFileHash, displacedDiskHashMatches);
// ── 正向:只有事故现场本身才允许重整 ────────────────────────────────────────────
[Fact]
public void ExactAccidentSignature_ResetsRecord()
{
var outcome = Decide();
Assert.Equal(MigrationCollisionDecision.ResetCollidedRecord, outcome.Decision);
Assert.Equal("COLLISION_STATUS_RESET", outcome.Plan!.Strategy);
Assert.Equal(DisplacedV, outcome.Plan.DisplacedVersion);
Assert.Null(outcome.RejectReason);
}
/// 不同驱动回读 hash / status 的形态不一致,大小写与首尾空白不应改变裁决。
[Theory]
[InlineData("Success", "970e5a8f0133944cb04ac95e67112a2ec6e2e45f06ae61c3f2049163f7dc9a61")]
[InlineData("SUCCESS", " " + CollidedHash + " ")]
[InlineData("success", CollidedHash)]
public void CaseInsensitiveAndTrimmed_StillResets(string status, string hash)
=> Assert.Equal(
MigrationCollisionDecision.ResetCollidedRecord,
Decide(loggedStatus: status, loggedFileHash: hash).Decision);
// ── 闸门 1–4:正常环境一律 None,绝不 Reject ──────────────────────────────────
[Fact]
public void UnregisteredVersion_IsNone()
{
var outcome = Decide(version: "1.0.551");
Assert.Equal(MigrationCollisionDecision.None, outcome.Decision);
Assert.Null(outcome.Plan);
}
/// 全新环境:该版本从未跑过 ⇒ 正常首次执行,不是事故场景。
[Fact]
public void FreshEnvironmentWithNoRow_IsNone()
=> Assert.Equal(MigrationCollisionDecision.None, Decide(matchedRowCount: 0, loggedStatus: null, loggedFileHash: null).Decision);
/// 唯一键理论上保证不会有多行;真出现了也只能停手,不能猜该动哪一行。
[Fact]
public void MultipleRowsForVersion_IsNone()
=> Assert.Equal(MigrationCollisionDecision.None, Decide(matchedRowCount: 2).Decision);
[Theory]
[InlineData("Failed")]
[InlineData("Running")]
[InlineData("Skipped")]
[InlineData("Superseded")]
public void NonSuccessStatus_IsNone(string status)
=> Assert.Equal(MigrationCollisionDecision.None, Decide(loggedStatus: status).Decision);
///
/// 最危险的一条:已经升级完成的环境,记录里是目标 hash + Success。
/// 它与事故环境只差 file_hash 一个字段,若这里 Reject 就会把所有正常环境打死,
/// 必须放行给 ShouldSkipScript 按常规「hash 未变 ⇒ 跳过」处理。
///
[Fact]
public void AlreadyUpgradedEnvironment_IsNone_NotReject()
{
var outcome = Decide(loggedFileHash: TargetHash);
Assert.Equal(MigrationCollisionDecision.None, outcome.Decision);
Assert.Null(outcome.RejectReason);
}
/// 某个我们没见过的 1.0.536 ⇒ 不认领,交给既有 hash 守卫去拒绝启动。
[Fact]
public void UnknownHashForRegisteredVersion_IsNone()
=> Assert.Equal(
MigrationCollisionDecision.None,
Decide(loggedFileHash: "0000000000000000000000000000000000000000000000000000000000000000").Decision);
// ── 闸门 5–7:命中签名后,安全前提不成立一律 Reject ────────────────────────────
/// 命中事故签名但磁盘不是目标 revision ⇒ 重整后会跑出未预期的脚本,必须停机。
[Fact]
public void SignatureMatchedButWrongRevision_Rejects()
{
var outcome = Decide(targetDiskHashMatches: false);
Assert.Equal(MigrationCollisionDecision.Reject, outcome.Decision);
Assert.Contains(TargetHash, outcome.RejectReason);
}
///
/// 最关键的一道:被顶替的内容必须已被合法记录在案。
/// 拿不出这个证据就重整,等于可能抹掉一次真实发生过的执行史。
///
[Theory]
[InlineData(null, DisplacedHash)]
[InlineData("Failed", DisplacedHash)]
[InlineData("Success", null)]
[InlineData("Success", "0000000000000000000000000000000000000000000000000000000000000000")]
public void DisplacedContentNotProvenRecorded_Rejects(string? status, string? hash)
{
var outcome = Decide(displacedLoggedStatus: status, displacedLoggedFileHash: hash);
Assert.Equal(MigrationCollisionDecision.Reject, outcome.Decision);
Assert.Contains(DisplacedV, outcome.RejectReason);
}
/// 磁盘上的被顶替脚本也必须仍是登记的那一份,否则「已记录的」与「本 revision 携带的」不是同一个东西。
[Fact]
public void DisplacedScriptOnDiskDiffers_Rejects()
=> Assert.Equal(MigrationCollisionDecision.Reject, Decide(displacedDiskHashMatches: false).Decision);
// ── 幂等:三种中断/完成状态下的收敛行为 ──────────────────────────────────────
/// First Run 成功后:hash 已是目标值 ⇒ 永不再触发。
[Fact]
public void AfterSuccessfulFirstRun_NeverTriggersAgain()
=> Assert.Equal(MigrationCollisionDecision.None, Decide(loggedFileHash: TargetHash).Decision);
/// 重整后崩溃(Superseded + 旧 hash):闸门 3 不成立 ⇒ None;
/// 而 ShouldSkipScript 对非 Success 行返回 false,脚本照常执行,仍然收敛。
[Fact]
public void AfterResetButBeforeExecution_IsNone_SoNormalFlowProceeds()
=> Assert.Equal(
MigrationCollisionDecision.None,
Decide(loggedStatus: MigrationCollisionPolicy.SupersededStatus).Decision);
/// 正式脚本执行失败(目标 hash + Failed):走常规重试,不再经过本策略。
[Fact]
public void AfterFailedExecution_IsNone()
=> Assert.Equal(
MigrationCollisionDecision.None,
Decide(loggedStatus: "Failed", loggedFileHash: TargetHash).Decision);
// ── 登记表本身的约束 ────────────────────────────────────────────────────────
///
/// 登记表只应包含这一条已评审的事故。新增条目 = 授权一次对迁移历史的修改,
/// 必须先更新本用例,以强制评审发生。
///
[Fact]
public void RegistryContainsOnlyTheReviewedCollision()
{
var plan = Assert.Single(MigrationCollisionPolicy.KnownCollisions);
Assert.Equal(V, plan.Version);
Assert.Equal(CollidedHash, plan.CollidedFileHash);
Assert.Equal(TargetHash, plan.ExpectedDiskHash);
Assert.Equal(DisplacedV, plan.DisplacedVersion);
Assert.Equal(DisplacedHash, plan.DisplacedFileHash);
}
///
/// 重整写入的中间状态必须避开全部既有状态值,否则会被正常流程误解
/// (例如被 successVersions 统计、或被失败恢复策略认领)。
///
[Fact]
public void SupersededStatusDoesNotCollideWithExistingStatuses()
{
foreach (var existing in new[] { "Running", "Success", "Failed", "Skipped" })
Assert.NotEqual(existing, MigrationCollisionPolicy.SupersededStatus, StringComparer.OrdinalIgnoreCase);
}
}