补充重新核查实库持久化测试并同步整改进度
This commit is contained in:
parent
f9cd19f631
commit
c1faebf9ef
@ -23,10 +23,16 @@ func (c *reconciliationConsoleClient) PrepareRecoveryReconciliation(sourceID str
|
||||
}
|
||||
func (c *reconciliationConsoleClient) ExecuteRecovery(id, token string) (deployment.RecoveryDispositionSummary, error) {
|
||||
value, err := c.dispositionConsoleClient.ExecuteRecovery(id, token)
|
||||
if err != nil { return value, err }
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
value.SourceDispositionID = c.sourceID
|
||||
if value.Status == "succeeded" { value.ResultCode = "committed" }
|
||||
if c.mismatch { value.SourceDispositionID = strings.Repeat("d", 64) }
|
||||
if value.Status == "succeeded" {
|
||||
value.ResultCode = "committed"
|
||||
}
|
||||
if c.mismatch {
|
||||
value.SourceDispositionID = strings.Repeat("d", 64)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
@ -37,26 +43,45 @@ func TestRecoveryReconciliationConsoleConfirmation(t *testing.T) {
|
||||
client := &reconciliationConsoleClient{}
|
||||
answer := "确认重新核查 " + sourceID + " " + dispositionActionLabel(deployment.RecoveryActionVerifyCommit)
|
||||
switch mode {
|
||||
case "rejected": client.status = "rejected"
|
||||
case "cancelled": answer = "确认"
|
||||
case "expired": client.expired = true
|
||||
case "disconnect": client.executeError = true
|
||||
case "mismatch": client.mismatch = true
|
||||
case "rejected":
|
||||
client.status = "rejected"
|
||||
case "cancelled":
|
||||
answer = "确认"
|
||||
case "expired":
|
||||
client.expired = true
|
||||
case "disconnect":
|
||||
client.executeError = true
|
||||
case "mismatch":
|
||||
client.mismatch = true
|
||||
}
|
||||
screen := &recoveryTestScreen{testTerminalInput: testTerminalInput{values: []string{sourceID, answer}}}
|
||||
err := recoveryReconciliationFlow(t.Context(), client, screen)
|
||||
output := strings.Join(screen.lines, "|")
|
||||
if client.prepares != 1 || client.queries != 0 || strings.Contains(output, strings.Repeat("c", 64)) { t.Fatal("终端重复调用或泄露令牌") }
|
||||
if client.prepares != 1 || client.queries != 0 || strings.Contains(output, strings.Repeat("c", 64)) {
|
||||
t.Fatal("终端重复调用或泄露令牌")
|
||||
}
|
||||
if mode == "cancelled" || mode == "expired" {
|
||||
if client.executes != 0 { t.Fatal("取消或过期仍执行核查") }
|
||||
if client.executes != 0 {
|
||||
t.Fatal("取消或过期仍执行核查")
|
||||
}
|
||||
return
|
||||
}
|
||||
if client.executes != 1 || !strings.Contains(output, "原不确定记录永久保留") { t.Fatal("缺少明确警告或重复执行") }
|
||||
if client.executes != 1 || !strings.Contains(output, "原不确定记录永久保留") {
|
||||
t.Fatal("缺少明确警告或重复执行")
|
||||
}
|
||||
if mode == "disconnect" || mode == "mismatch" {
|
||||
if err == nil || !strings.Contains(output, "新核查编号查询") { t.Fatal("失去可靠响应后误报成功") }
|
||||
} else if err != nil { t.Fatal(err) }
|
||||
if mode == "success" && !strings.Contains(output, "占用已安全释放") { t.Fatal("缺少完成结果") }
|
||||
if mode == "rejected" && !strings.Contains(output, "原占用继续保留") { t.Fatal("错误显示拒绝结果") }
|
||||
if err == nil || !strings.Contains(output, "新核查编号查询") {
|
||||
t.Fatal("失去可靠响应后误报成功")
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if mode == "success" && !strings.Contains(output, "占用已安全释放") {
|
||||
t.Fatal("缺少完成结果")
|
||||
}
|
||||
if mode == "rejected" && !strings.Contains(output, "原占用继续保留") {
|
||||
t.Fatal("错误显示拒绝结果")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,9 @@ type localReconciliationJournal struct {
|
||||
}
|
||||
|
||||
func (j *localReconciliationJournal) GetRecoveryDisposition(ctx context.Context, id string) (deployment.RecoveryDisposition, error) {
|
||||
if id == j.source.Intent.ID { return j.source, nil }
|
||||
if id == j.source.Intent.ID {
|
||||
return j.source, nil
|
||||
}
|
||||
return j.localDispositionJournal.GetRecoveryDisposition(ctx, id)
|
||||
}
|
||||
|
||||
@ -38,9 +40,13 @@ func localReconciliationFixture(t *testing.T) (*Server, *localResetSession, *loc
|
||||
service.VerifyRecoveryTarget = func(context.Context, deployment.Server, deployment.RecoveryBaseline, bool) error { return nil }
|
||||
resolve := func(context.Context, string) (deployment.Server, error) { return target, nil }
|
||||
review, err := service.PrepareRecoveryReview(t.Context(), journal.record.ID, "uid:0", deployment.RecoveryActionCommit, resolve)
|
||||
if err != nil { t.Fatal(err) }
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
intent, err := review.DispositionIntent(strings.Repeat("a", 64), strings.Repeat("b", 64))
|
||||
if err != nil { t.Fatal(err) }
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
journal.source = deployment.RecoveryDisposition{Intent: intent, Status: "uncertain", ResultCode: "uncertain", CreatedAt: now, UpdatedAt: now}
|
||||
server.recoveryInspection = service
|
||||
@ -52,25 +58,44 @@ func TestLocalReconciliationRejectsInvalidSession(t *testing.T) {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
server, session, journal := localReconciliationFixture(t)
|
||||
prepared, err := session.PrepareRecoveryReconciliation(t.Context(), journal.source.Intent.ID)
|
||||
if err != nil { t.Fatal(err) }
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
switch mode {
|
||||
case "expired": session.expires = time.Time{}
|
||||
case "cancelled": cancel()
|
||||
case "changed-admin": server.config.Admin.Email = "changed@example.test"
|
||||
case "closed": session.Close()
|
||||
case "reset-stage": session.stage = 1
|
||||
case "wrong-id": prepared.DispositionID = journal.source.Intent.ID
|
||||
case "wrong-token": prepared.Token = strings.Repeat("a", 64)
|
||||
case "changed-peer": session.peer.Identity = "uid:123"
|
||||
case "foreign-session": session = localDispositionNewSession(t, server)
|
||||
case "expired":
|
||||
session.expires = time.Time{}
|
||||
case "cancelled":
|
||||
cancel()
|
||||
case "changed-admin":
|
||||
server.config.Admin.Email = "changed@example.test"
|
||||
case "closed":
|
||||
session.Close()
|
||||
case "reset-stage":
|
||||
session.stage = 1
|
||||
case "wrong-id":
|
||||
prepared.DispositionID = journal.source.Intent.ID
|
||||
case "wrong-token":
|
||||
prepared.Token = strings.Repeat("a", 64)
|
||||
case "changed-peer":
|
||||
session.peer.Identity = "uid:123"
|
||||
case "foreign-session":
|
||||
session = localDispositionNewSession(t, server)
|
||||
case "replaced-confirmation":
|
||||
if _, err := session.PrepareRecoveryReconciliation(ctx, journal.source.Intent.ID); err != nil { t.Fatal(err) }
|
||||
if _, err := session.PrepareRecoveryReconciliation(ctx, journal.source.Intent.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if _, err := session.ExecuteRecovery(ctx, prepared.DispositionID, prepared.Token); err == nil {
|
||||
t.Fatal("无效会话执行了重新核查")
|
||||
}
|
||||
if _, err := session.ExecuteRecovery(t.Context(), prepared.DispositionID, prepared.Token); err == nil {
|
||||
t.Fatal("失败确认被重放")
|
||||
}
|
||||
if journal.begins != 0 || journal.finishes != 0 || session.recoveryConfirmation.DispositionID() != "" {
|
||||
t.Fatal("无效会话写入了审计或保留了确认")
|
||||
}
|
||||
if _, err := session.ExecuteRecovery(ctx, prepared.DispositionID, prepared.Token); err == nil { t.Fatal("无效会话执行了重新核查") }
|
||||
if _, err := session.ExecuteRecovery(t.Context(), prepared.DispositionID, prepared.Token); err == nil { t.Fatal("失败确认被重放") }
|
||||
if journal.begins != 0 || journal.finishes != 0 || session.recoveryConfirmation.DispositionID() != "" { t.Fatal("无效会话写入了审计或保留了确认") }
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -78,11 +103,19 @@ func TestLocalReconciliationRejectsInvalidSession(t *testing.T) {
|
||||
func TestLocalReconciliationRejectsMissingFilesAndQueriesNewAudit(t *testing.T) {
|
||||
server, session, journal := localReconciliationFixture(t)
|
||||
prepared, err := session.PrepareRecoveryReconciliation(t.Context(), journal.source.Intent.ID)
|
||||
if err != nil { t.Fatal(err) }
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := session.ExecuteRecovery(t.Context(), prepared.DispositionID, prepared.Token)
|
||||
if err != nil || result.Status != "rejected" || result.SourceDispositionID != journal.source.Intent.ID { t.Fatalf("缺失文件仍被放行:%+v %v", result, err) }
|
||||
if journal.item.Intent.Operator != "uid:0" || journal.begins != 1 || journal.finishes != 1 || journal.source.Status != "uncertain" { t.Fatal("身份或历史记录被改变") }
|
||||
if err != nil || result.Status != "rejected" || result.SourceDispositionID != journal.source.Intent.ID {
|
||||
t.Fatalf("缺失文件仍被放行:%+v %v", result, err)
|
||||
}
|
||||
if journal.item.Intent.Operator != "uid:0" || journal.begins != 1 || journal.finishes != 1 || journal.source.Status != "uncertain" {
|
||||
t.Fatal("身份或历史记录被改变")
|
||||
}
|
||||
next := localDispositionNewSession(t, server)
|
||||
queried, err := next.GetRecoveryDisposition(t.Context(), result.ID)
|
||||
if err != nil || queried != result { t.Fatal("重连未查到关联审计", err) }
|
||||
if err != nil || queried != result {
|
||||
t.Fatal("重连未查到关联审计", err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,9 @@ func (s *reconciliationSession) PrepareRecoveryReconciliation(ctx context.Contex
|
||||
func (s *reconciliationSession) ExecuteRecovery(ctx context.Context, id, token string) (deployment.RecoveryDispositionSummary, error) {
|
||||
sourceID := s.prepared.Summary.SourceDispositionID
|
||||
value, err := s.dispositionTestSession.ExecuteRecovery(ctx, id, token)
|
||||
if err != nil { return value, err }
|
||||
if err != nil {
|
||||
return value, err
|
||||
}
|
||||
value.SourceDispositionID, value.ResultCode = sourceID, deployment.RecoveryResultCommitted
|
||||
s.result = value
|
||||
return value, nil
|
||||
@ -32,51 +34,81 @@ func TestRecoveryReconciliationProtocolAndQuery(t *testing.T) {
|
||||
session := &reconciliationSession{}
|
||||
_, conn, done := recoveryConnection(t, session)
|
||||
client, err := Begin(t.Context(), conn)
|
||||
if err != nil { t.Fatal(err) }
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sourceID := strings.Repeat("a", 64)
|
||||
prepared, err := client.PrepareRecoveryReconciliation(sourceID)
|
||||
if err != nil || prepared.Summary.SourceDispositionID != sourceID { t.Fatal("原处置绑定丢失", err) }
|
||||
if err != nil || prepared.Summary.SourceDispositionID != sourceID {
|
||||
t.Fatal("原处置绑定丢失", err)
|
||||
}
|
||||
result, err := client.ExecuteRecovery(prepared.DispositionID, prepared.Token)
|
||||
if err != nil || result.SourceDispositionID != sourceID || result.Status != "succeeded" { t.Fatal("重新核查结果无效", err) }
|
||||
if err != nil || result.SourceDispositionID != sourceID || result.Status != "succeeded" {
|
||||
t.Fatal("重新核查结果无效", err)
|
||||
}
|
||||
queried, err := client.GetRecoveryDisposition(result.ID)
|
||||
if err != nil || queried != result { t.Fatal("查询改变了关联结果", err) }
|
||||
if _, err := client.ExecuteRecovery(prepared.DispositionID, prepared.Token); err == nil { t.Fatal("旧令牌被重放") }
|
||||
if err != nil || queried != result {
|
||||
t.Fatal("查询改变了关联结果", err)
|
||||
}
|
||||
if _, err := client.ExecuteRecovery(prepared.DispositionID, prepared.Token); err == nil {
|
||||
t.Fatal("旧令牌被重放")
|
||||
}
|
||||
_ = client.Close()
|
||||
<-done
|
||||
if session.prepares.Load() != 1 || session.executes.Load() != 2 || session.queries.Load() != 1 || session.resets.Load() != 0 { t.Fatal("调用了错误业务入口") }
|
||||
if session.prepares.Load() != 1 || session.executes.Load() != 2 || session.queries.Load() != 1 || session.resets.Load() != 0 {
|
||||
t.Fatal("调用了错误业务入口")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveryReconciliationRejectsUnboundResponse(t *testing.T) {
|
||||
sourceID := strings.Repeat("a", 64)
|
||||
session := &reconciliationSession{}
|
||||
prepared, err := session.PrepareRecoveryReconciliation(t.Context(), sourceID)
|
||||
if err != nil { t.Fatal(err) }
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := Request{Action: "recovery_reconcile_prepare", DispositionID: sourceID}
|
||||
for _, mode := range []string{"source", "action", "same-id", "transaction", "mixed", "expired-range"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
value := prepared
|
||||
response := Response{Preparation: &value}
|
||||
switch mode {
|
||||
case "source": value.Summary.SourceDispositionID = strings.Repeat("d", 64)
|
||||
case "action": value.Summary.Action = deployment.RecoveryActionCommit
|
||||
case "same-id": value.DispositionID = sourceID
|
||||
case "transaction": value.Summary.TransactionID = "../bad"
|
||||
case "mixed": response.Disposition = &deployment.RecoveryDispositionSummary{}
|
||||
case "expired-range": value.Summary.ExpiresAt = value.Summary.ObservedAt.Add(2*time.Minute)
|
||||
case "source":
|
||||
value.Summary.SourceDispositionID = strings.Repeat("d", 64)
|
||||
case "action":
|
||||
value.Summary.Action = deployment.RecoveryActionCommit
|
||||
case "same-id":
|
||||
value.DispositionID = sourceID
|
||||
case "transaction":
|
||||
value.Summary.TransactionID = "../bad"
|
||||
case "mixed":
|
||||
response.Disposition = &deployment.RecoveryDispositionSummary{}
|
||||
case "expired-range":
|
||||
value.Summary.ExpiresAt = value.Summary.ObservedAt.Add(2 * time.Minute)
|
||||
}
|
||||
if validDispositionResponse(request, response) {
|
||||
t.Fatal("接受了混合或无绑定准备响应")
|
||||
}
|
||||
if validDispositionResponse(request, response) { t.Fatal("接受了混合或无绑定准备响应") }
|
||||
})
|
||||
}
|
||||
if validRecoveryAction(deployment.RecoveryActionVerifyCommit) || validRecoveryAction(deployment.RecoveryActionVerifyRollback) { t.Fatal("普通准备允许绕过原处置绑定") }
|
||||
if validRecoveryAction(deployment.RecoveryActionVerifyCommit) || validRecoveryAction(deployment.RecoveryActionVerifyRollback) {
|
||||
t.Fatal("普通准备允许绕过原处置绑定")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveryReconciliationOldServerFailsClosed(t *testing.T) {
|
||||
session := &dispositionTestSession{}
|
||||
_, conn, done := recoveryConnection(t, session)
|
||||
client, err := Begin(t.Context(), conn)
|
||||
if err != nil { t.Fatal(err) }
|
||||
if _, err := client.PrepareRecoveryReconciliation(strings.Repeat("a", 64)); err == nil { t.Fatal("旧服务错误地支持了重新核查") }
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := client.PrepareRecoveryReconciliation(strings.Repeat("a", 64)); err == nil {
|
||||
t.Fatal("旧服务错误地支持了重新核查")
|
||||
}
|
||||
_ = client.Close()
|
||||
<-done
|
||||
if session.prepares.Load() != 0 || session.executes.Load() != 0 { t.Fatal("旧服务错误地执行了原动作") }
|
||||
if session.prepares.Load() != 0 || session.executes.Load() != 0 {
|
||||
t.Fatal("旧服务错误地执行了原动作")
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +76,23 @@ func runExternalRecovery(t *testing.T, fixture recoveryExternalFixture) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
runReconciliationSuccess(t, NewSQLDeploymentTransactionRepository(db, cfg.Driver), db, action)
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db = openRecoveryExternal(t, cfg)
|
||||
recoveryCount(t, db, "recovery_dispositions", 2)
|
||||
recoveryCount(t, db, "recovery_events", 4)
|
||||
recoveryCount(t, db, "recovery_claims", 0)
|
||||
pending, err := NewSQLDeploymentTransactionRepository(db, cfg.Driver).ListPendingDeploymentTransactions(t.Context())
|
||||
if err != nil || len(pending) != 0 {
|
||||
t.Fatal("重新打开实库后成功收尾丢失", err)
|
||||
}
|
||||
})
|
||||
for _, stage := range []string{"started", "finished"} {
|
||||
t.Run("reconciliation_audit/"+string(action)+"/"+stage, func(t *testing.T) {
|
||||
runExternalReconciliationAuditFailure(t, fixture, action, stage)
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, action := range []deployment.RecoveryAction{deployment.RecoveryActionRecord, deployment.RecoveryActionCommit, deployment.RecoveryActionRollback} {
|
||||
for _, stage := range []string{"none", "started", "finished"} {
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"maps"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/database"
|
||||
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
||||
)
|
||||
|
||||
// 重开真实隔离数据库,验证审计失败不丢失占用,也不将只读核查变成远端重放。
|
||||
func runExternalReconciliationAuditFailure(t *testing.T, fixture recoveryExternalFixture, action deployment.RecoveryAction, stage string) {
|
||||
t.Helper()
|
||||
cfg := fixture.database(t)
|
||||
db := openRecoveryExternal(t, cfg)
|
||||
if err := database.Migrate(t.Context(), db, cfg.Driver); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
repo := NewSQLDeploymentTransactionRepository(db, cfg.Driver)
|
||||
service, client, server, record, source := seedUncertainReconciliation(t, repo, db, action)
|
||||
files := maps.Clone(client.files)
|
||||
resolve := func(context.Context, string) (deployment.Server, error) { return server, nil }
|
||||
confirmation, token, err := service.PrepareRecoveryReconciliation(t.Context(), source.Intent.ID, "unix:0", resolve)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fixture.inject(t, db, cfg, stage)
|
||||
result, err := service.ExecuteRecoveryDisposition(t.Context(), confirmation, "unix:0", token, resolve)
|
||||
if !errors.Is(err, deployment.ErrRecoveryDispositionUnavailable) || result.Status == "succeeded" {
|
||||
t.Fatal("重新核查审计失败发布成功结果", err)
|
||||
}
|
||||
if client.writes != 0 || !reflect.DeepEqual(files, client.files) {
|
||||
t.Fatal("审计失败核查修改远端文件")
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db = openRecoveryExternal(t, cfg)
|
||||
repo = NewSQLDeploymentTransactionRepository(db, cfg.Driver)
|
||||
assertReconciliationSource(t, repo, source)
|
||||
recoveryCount(t, db, "recovery_claims", 1)
|
||||
wantOwner := source.Intent.ID
|
||||
if stage == "finished" {
|
||||
wantOwner = confirmation.DispositionID()
|
||||
recoveryCount(t, db, "recovery_dispositions", 2)
|
||||
recoveryCount(t, db, "recovery_events", 3)
|
||||
stored, err := repo.GetRecoveryDisposition(t.Context(), wantOwner)
|
||||
if err != nil || stored.Status != "running" || stored.ResultCode != "" || stored.Intent.SourceDispositionID != source.Intent.ID {
|
||||
t.Fatal("结束审计失败未持久保留运行中处置", err)
|
||||
}
|
||||
} else {
|
||||
recoveryCount(t, db, "recovery_dispositions", 1)
|
||||
recoveryCount(t, db, "recovery_events", 2)
|
||||
}
|
||||
var owner string
|
||||
if err := db.QueryRowContext(t.Context(), repo.query("SELECT disposition_id FROM recovery_claims WHERE transaction_id = ?"), record.ID).Scan(&owner); err != nil || owner != wantOwner {
|
||||
t.Fatal("重开后占用归属错误", err)
|
||||
}
|
||||
pending, err := repo.ListPendingDeploymentTransactions(t.Context())
|
||||
if err != nil || len(pending) != 1 || pending[0].Status != deployment.TransactionStatusRecoveryRequired {
|
||||
t.Fatal("审计失败改写了原事务终态", err)
|
||||
}
|
||||
restarted := deployment.NewServiceWithJournal(nil, repo)
|
||||
if err := restarted.RecoverPending(t.Context(), func(context.Context, string) (deployment.Server, error) {
|
||||
t.Error("重开后自动恢复试图重放已占用事务")
|
||||
return deployment.Server{}, errors.New("禁止连接远端")
|
||||
}); !errors.Is(err, deployment.ErrRecoveryDispositionBusy) {
|
||||
t.Fatal("重开后自动恢复未遵守持久占用", err)
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,18 @@
|
||||
# Linux 生产流程测试记录
|
||||
|
||||
## 2026年10月3日23:58 重新核查源码验证
|
||||
|
||||
第一台隔离源码目录:`/var/tmp/scert-local-recovery-20261003-1828/backend`;使用 nobody 和原生 Go 1.26.6,不替换运行中的正式服务。
|
||||
|
||||
- [✓] `reconciliation-latest.log`:五包专项退出码零,筛选包含 Recovery、Disposition、Reconciliation,覆盖新本机会话测试。
|
||||
- [✓] `reconciliation-race.log`:同五包竞态退出码零。
|
||||
- [~] `reconciliation-external.log`:显式启用隔离 MySQL 8.0 与 PostgreSQL 14,验证迁移二十五、中断重入、两个收尾方向、审计失败及连接池重开;尚未取得最终结果。
|
||||
- [~] `reconciliation-full.log`、`reconciliation-vet.log`:最新后端全量和静态检查正在顺序运行。
|
||||
|
||||
上批 `STRICT_UMASK_FULL_EXIT=0` 仅覆盖关闭修复及备份测试权限基线;不能替代本批结果。五包竞态结束后只补充了外部实库测试,产品实现未再改变。所有日志位于上述源码目录的父目录。
|
||||
|
||||
当前线上仍为 `a5dfd42`、schema24;真实处置 `bc4099fd53c10ce0ae643ae2a89c2597fa48ed7ea6d6661fe4126f7f310b7007` 及占用未手动修改。正式新包、schema25、真实本机菜单五收尾均待验。
|
||||
|
||||
## 2026年10月3日23:14 真实中断恢复与关闭误判
|
||||
|
||||
### 重载阶段中断与自动恢复
|
||||
|
||||
@ -1,5 +1,12 @@
|
||||
# Scert 开发任务清单
|
||||
|
||||
## 2026年10月3日23:58 最新开发状态
|
||||
|
||||
- [x] 不确定处置重新核查已接通执行器、数据库、本机协议及管理终端;仅允许已结束 uncertain,保留原审计,成功才原子结束原事务并释放占用。
|
||||
- [✓] 最新五包专项及竞态通过;旧确认重放、身份变化、残留文件、HTTPS 失败、最终关闭失败及会话混淆均有回归覆盖。
|
||||
- [~] 新增隔离实库审计失败与重开验证,正在运行 MySQL/PostgreSQL、最新全量和静态检查。
|
||||
- [ ] 正式 Linux 发布及实机收尾尚未完成;当前线上仍为迁移二十四,不宣称已部署新能力。
|
||||
|
||||
## 2026年10月3日23:14 真实故障整改进度
|
||||
|
||||
- [✓] 正式包真实进程中断后双机自动回滚,任务保留重启失败历史,第二台网站继续通过严格 HTTPS。
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
# Scert 整改任务执行清单
|
||||
|
||||
## 2026年10月3日23:58 当前执行状态
|
||||
|
||||
- [x] 独立重新核查实现、本机菜单五及迁移二十五已完成编码,原 uncertain 永久保留,只核验原方向,不执行远端写操作。
|
||||
- [✓] 五包专项及竞态通过;覆盖最新本机会话、协议和终端用例。
|
||||
- [~] 后端全量、静态检查、隔离 MySQL/PostgreSQL 迁移与执行器回归正在运行,结果以测试记录后续更新为准。
|
||||
- [ ] 正式包升级、实机重新核查释放占用、人工回滚与断连验证仍待执行。
|
||||
- [!] 公网、系统通知授权及首次初始化等生产门禁未关闭;不将源码测试代替上线验收。
|
||||
|
||||
## 2026年10月3日23:14 当前整改任务
|
||||
|
||||
- [✓] 真实部署重载阶段 SIGKILL 后,systemd 自动重启及双机回滚通过;第二台 HTTPS 正常。
|
||||
|
||||
@ -1,5 +1,18 @@
|
||||
# Scert 整改、合并与优化清单
|
||||
|
||||
## 2026年10月3日23:58 独立重新核查实现与回归
|
||||
|
||||
本节优先于下方历史进度,仅处理 Linux;生产验收仍未完成。
|
||||
|
||||
- [x] 不确定处置独立重新核查已实现:受保护本机菜单五,方向绑定原处置,只读远端核验,以新审计收尾,原不确定记录保留;不接管运行中处置。
|
||||
- [✓] 最新五包专项及竞态通过,包含部署、仓储、本机协议、管理终端和 HTTP 会话的 Recovery、Disposition、Reconciliation 用例;不代表真实远端收尾已通过。
|
||||
- [✓] 上批关闭修复及备份权限夹具在严格 umask 下后端全量通过,静态检查通过;此结论不包含后续重新核查代码。
|
||||
- [~] 最新代码后端全量、静态检查、隔离 MySQL/PostgreSQL 迁移二十五及执行器验证正在运行。已补充审计失败、连接池重开和持久占用验证。
|
||||
- [ ] 测试通过后构建干净 Linux 候选包,通过正式安装器升级并以正式本机入口收尾实机 uncertain;禁止手动改终态、删占用或重放原动作。
|
||||
- [!] 第一台公网备案拦截、Smail 系统通知授权,以及三数据库首次 Web、人工回滚和断连、DNS/ACME 完整验收仍未关闭。
|
||||
|
||||
修改前检查点 `f9cd19f` 已中文提交并推送。第二台空间已核实充足,不再作为整改阻塞。
|
||||
|
||||
## 2026年10月3日23:14 崩溃恢复实测与关闭误判修复
|
||||
|
||||
- [✓] 正式 API 任务 `c68e3956-1b6d-42c7-ba9a-753ef2d1a44b` 在第二目标重载阶段被真实 SIGKILL 中断;systemd 自动重启,两条事务均 rolled_back,任务保留 service_restart 失败记录。
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
## 当前发布状态
|
||||
|
||||
2026年10月3日:功能正在原生 Linux 验证,尚未随正式候选包部署。以下说明适用于包含该功能的新版本,不能据此认定线上已经提供此菜单。
|
||||
2026年10月3日23:58:只读核查和人工处置已在迁移二十四候选包提供;新增“重新核查不确定处置”正在原生 Linux 回归,尚未部署,需包含迁移二十五的新版本。各项实机验收状态以生产测试记录为准。
|
||||
|
||||
## 入口与权限
|
||||
|
||||
@ -33,6 +33,14 @@ systemctl start Scert-admin
|
||||
- 输入异常、过期、读取失败、响应超限或管理员配置发生变化时,终止会话,不将部分观察作为成功结果。
|
||||
- 核查与密码重置不能在同一会话互相切换;需要切换时退出后重新启动。
|
||||
|
||||
## 尚未提供的能力
|
||||
## 重新核查不确定处置
|
||||
|
||||
独立人工处置、审计和基于可信证据的提交或回滚仍在开发。不要手工修改数据库事务终态,不要删锁后宣称历史部署成功。完整状态以整改清单和生产测试记录为准。
|
||||
以下流程仅适用于包含迁移二十五的新版本:
|
||||
|
||||
1. 启动本机管理员工具,选择菜单 `5`,输入已持久化为 uncertain 的原处置编号。
|
||||
2. 核对原编号、新核查编号、事务、服务器与原动作方向。保留新核查编号,用于断连后查询。
|
||||
3. 完整输入终端显示的中文确认文字。取消或过期不会提交,不能切换原提交或回滚方向。
|
||||
4. 产品只重新核验,不重放远端文件修改或重载。全部文件、残留、锁、严格 HTTPS 及关闭检查通过后,以新审计结束原事务并释放占用,原不确定记录保留。
|
||||
5. 断连或没有可靠结果时,重启工具选择菜单 `4`,按新核查编号查询;不要再次提交原动作或手动删锁。
|
||||
|
||||
运行中处置、证据不足、身份变化及现场不满足收尾条件均拒绝,不因等待超时接管。人工处置成功场景、不同版本证书回滚和完整首次生产验收仍须实测;不能把新增菜单视作这些验收已通过。
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
# 部署事务人工处置设计
|
||||
|
||||
## 2026年10月3日23:58 独立重新核查设计已实现
|
||||
|
||||
原不确定处置不改写。通过受保护本机独立入口指定原处置编号,重新绑定身份、事务摘要和现场摘要,自动沿用原提交或回滚方向;普通准备入口不接受重新核查动作。
|
||||
|
||||
- 只接受已有完整开始、结束审计的 uncertain;不接管 running,不以等待时间代替执行者停止证据。
|
||||
- 原事务必须处于对应 verified 阶段,文件符合原方向摘要,临时文件、备份和锁均不存在;严格 HTTPS 后再次核验现场,连接正常关闭才允许成功。
|
||||
- 核查不执行远端写文件、重命名、删除、解锁、命令或服务重载。
|
||||
- schema25 保存原处置关联;开始审计与占用交接原子提交,成功审计、原事务终态及占用释放原子提交。
|
||||
- 核查拒绝或不确定时归还原占用;结束审计失败保留新 running 占用,不能自动重试或改为成功。
|
||||
|
||||
五包专项与竞态已通过,实库重开验证正在执行;本节说明实现边界,不代表新包已经部署或所有历史恢复问题已经解决。
|
||||
|
||||
## 2026年10月3日23:14 实机暴露的关闭与重新核查边界
|
||||
|
||||
真实人工提交已完成文件核验、备份删除、commit_verified 持久化和远端解锁,但 SSH 先主动关闭、SFTP 后关闭返回预期连接已关闭错误,导致执行器报告 uncertain。新增 Linux 真实协议回归已复现,修复限制为正常主动关闭后的 EOF 或连接已关闭,不吞掉既有断连、其他关闭错误或动作错误。重复和并发 Close 共享首次结果。
|
||||
|
||||
@ -1,5 +1,16 @@
|
||||
# 部署事务人工核查任务清单
|
||||
|
||||
## 2026年10月3日23:58 重新核查任务进度
|
||||
|
||||
- [x] R6-A:绑定原处置、方向、服务器和现场摘要,拒绝运行中或缺失可信证据的来源。
|
||||
- [x] R6-B:迁移二十五、原子占用交接及新审计已实现,保留原 uncertain;正在补充实库重开验证。
|
||||
- [x] R6-C:只读文件及残留核验、严格 HTTPS 后复核、关闭检查完成;专项负向和竞态通过,不执行远端写操作。
|
||||
- [x] R6-D:本机菜单五、完整确认、单次令牌、会话绑定、断连查询已实现,五包专项和竞态通过;三数据库门禁仍待本批结果。
|
||||
- [~] 最新后端全量、静态检查和隔离 MySQL/PostgreSQL 正在运行。上批严格 umask 全量已通过,不再沿用旧的“等待权限回归”状态。
|
||||
- [ ] R6-E:正式 Linux 包升级和实机 uncertain 收尾尚未执行,其他无可信基线历史事务继续保留。
|
||||
|
||||
本节优先于下方历史清单。未清理生产数据库、未创建辅助验收脚本、未开放新网络端口。
|
||||
|
||||
## 2026年10月3日23:30 不确定处置重新核查任务
|
||||
|
||||
沿用已确认的 Linux 范围,不增加网络入口,不直接修改现场事务或删除占用。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user