From d3151795f155c8a5c43e37aaa5263e1e6b6fa934 Mon Sep 17 00:00:00 2001 From: xuans Date: Thu, 1 Oct 2026 13:09:11 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E9=83=A8=E7=BD=B2=E9=94=81?= =?UTF-8?q?=E5=86=B2=E7=AA=81=E4=BA=8B=E5=8A=A1=E5=9B=9E=E5=BD=92=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 覆盖无锁持有和部分锁持有两种冲突场景,验证事务最终状态、已有锁保护以及本次锁清理。 --- .../deployment/transaction_recovery_test.go | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/backend/internal/deployment/transaction_recovery_test.go b/backend/internal/deployment/transaction_recovery_test.go index a814807..dc70ff7 100644 --- a/backend/internal/deployment/transaction_recovery_test.go +++ b/backend/internal/deployment/transaction_recovery_test.go @@ -109,3 +109,65 @@ func TestRecoverPendingKeepsUnprovableState(t *testing.T) { t.Fatalf("未记录人工核查状态:%+v", journal.updated) } } + +func TestDeployLockConflictFinalizesTransaction(t *testing.T) { + server := testServer("lock-conflict").Normalize() + client := newMemoryClient() + blockedLock := server.CertificatePath + ".scert-lock" + client.files[blockedLock] = memoryFile{} + journal := &memoryTransactionJournal{} + service := NewServiceWithJournal(memoryFactory{clients: map[string]*memoryClient{server.ID: client}}, journal) + ctx := WithTransactionContext(t.Context(), TransactionContext{TaskID: "task", AttemptID: "attempt", AttemptNumber: 1}) + + report := service.Deploy(ctx, []Target{{Server: server}}, testArtifact(t)) + if report.Status != "failed" || len(report.Results) != 1 || report.Results[0].FailureCode != FailureLock { + t.Fatalf("锁冲突应失败:%+v", report) + } + if len(journal.finished) != 1 || journal.finished[0].status != TransactionStatusRolledBack { + t.Fatalf("锁冲突后必须完成回滚事务:%+v", journal.finished) + } + if _, ok := client.files[blockedLock]; !ok { + t.Fatal("已有锁不能被清理") + } + for _, path := range []string{server.CertificatePath, server.CertificateChainPath, server.PrivateKeyPath} { + lockPath := path + ".scert-lock" + if lockPath != blockedLock { + if _, ok := client.files[lockPath]; ok { + t.Fatalf("锁冲突失败后不应残留本次锁:%s", lockPath) + } + } + } +} + +func TestDeployPartialLockConflictReleasesOwnedLocks(t *testing.T) { + server := testServer("partial-lock-conflict").Normalize() + client := newMemoryClient() + blockedLock := server.PrivateKeyPath + ".scert-lock" + client.files[blockedLock] = memoryFile{} + journal := &memoryTransactionJournal{} + service := NewServiceWithJournal(memoryFactory{clients: map[string]*memoryClient{server.ID: client}}, journal) + ctx := WithTransactionContext(t.Context(), TransactionContext{TaskID: "task", AttemptID: "attempt", AttemptNumber: 1}) + + report := service.Deploy(ctx, []Target{{Server: server}}, testArtifact(t)) + if report.Status != "failed" || len(report.Results) != 1 || report.Results[0].FailureCode != FailureLock { + t.Fatalf("部分锁冲突应失败:%+v", report) + } + if len(journal.finished) != 1 || journal.finished[0].status != TransactionStatusRolledBack { + t.Fatalf("部分锁冲突后必须完成回滚事务:%+v", journal.finished) + } + if _, ok := client.files[blockedLock]; !ok { + t.Fatal("已有锁不能被清理") + } + for _, path := range []string{server.CertificatePath, server.CertificateChainPath, server.PrivateKeyPath} { + lockPath := path + ".scert-lock" + if lockPath == blockedLock { + continue + } + if _, ok := client.files[lockPath]; ok { + t.Fatalf("部分锁冲突失败后不应残留本次锁:%s", lockPath) + } + if _, ok := client.files[lockPath+"/owner"]; ok { + t.Fatalf("部分锁冲突失败后不应残留锁归属:%s", lockPath) + } + } +}