From c1faebf9ef296d7ab88423bd2c3b93fe72aefd7a Mon Sep 17 00:00:00 2001 From: xuans Date: Sun, 4 Oct 2026 00:01:25 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E5=85=85=E9=87=8D=E6=96=B0=E6=A0=B8?= =?UTF-8?q?=E6=9F=A5=E5=AE=9E=E5=BA=93=E6=8C=81=E4=B9=85=E5=8C=96=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=B9=B6=E5=90=8C=E6=AD=A5=E6=95=B4=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../recovery_reconciliation_test.go | 55 +++++++++---- .../local_recovery_reconciliation_test.go | 77 +++++++++++++------ .../recovery_reconciliation_test.go | 72 ++++++++++++----- .../recovery_executor_external_test.go | 16 ++++ .../recovery_reconciliation_external_test.go | 73 ++++++++++++++++++ doc/Linux生产流程测试记录.md | 13 ++++ doc/Scert开发任务清单.md | 7 ++ doc/Scert整改任务执行清单.md | 8 ++ doc/Scert整改合并优化清单.md | 13 ++++ doc/本机部署核查使用说明.md | 14 +++- doc/部署事务人工处置设计.md | 12 +++ doc/部署事务人工核查任务清单.md | 11 +++ 12 files changed, 311 insertions(+), 60 deletions(-) create mode 100644 backend/internal/repository/recovery_reconciliation_external_test.go diff --git a/backend/internal/adminconsole/recovery_reconciliation_test.go b/backend/internal/adminconsole/recovery_reconciliation_test.go index 3fec488..a6c6037 100644 --- a/backend/internal/adminconsole/recovery_reconciliation_test.go +++ b/backend/internal/adminconsole/recovery_reconciliation_test.go @@ -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("错误显示拒绝结果") + } }) } } diff --git a/backend/internal/httpapi/local_recovery_reconciliation_test.go b/backend/internal/httpapi/local_recovery_reconciliation_test.go index 8b804df..eca466c 100644 --- a/backend/internal/httpapi/local_recovery_reconciliation_test.go +++ b/backend/internal/httpapi/local_recovery_reconciliation_test.go @@ -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) } @@ -31,16 +33,20 @@ func localReconciliationFixture(t *testing.T) (*Server, *localResetSession, *loc Files: map[string]string{}, TemporaryFiles: map[string]string{}, Baseline: deployment.RecoveryBaseline{Endpoint: "https://example.test", Domains: []string{"example.test"}, ExpectedFingerprint: "new"}} for _, path := range journal.record.Paths { journal.record.LockPaths = append(journal.record.LockPaths, path+".scert-lock") - journal.record.Backups = append(journal.record.Backups, deployment.TransactionBackup{Original: path, Saved: path+".scert-backup-"+journal.record.ID}) + journal.record.Backups = append(journal.record.Backups, deployment.TransactionBackup{Original: path, Saved: path + ".scert-backup-" + journal.record.ID}) journal.record.Evidence.Files[path] = strings.Repeat("e", 64) } service := deployment.NewServiceWithJournal(localDispositionFactory{}, journal) 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) + } } diff --git a/backend/internal/localreset/recovery_reconciliation_test.go b/backend/internal/localreset/recovery_reconciliation_test.go index 25c4d7c..78a2f72 100644 --- a/backend/internal/localreset/recovery_reconciliation_test.go +++ b/backend/internal/localreset/recovery_reconciliation_test.go @@ -9,7 +9,7 @@ import ( "git.pcux.cn/xuans/Scert/backend/internal/deployment" ) -type reconciliationSession struct { dispositionTestSession } +type reconciliationSession struct{ dispositionTestSession } func (s *reconciliationSession) PrepareRecoveryReconciliation(ctx context.Context, sourceID string) (RecoveryPreparation, error) { value, err := s.PrepareRecovery(ctx, "tx-1", deployment.RecoveryActionVerifyCommit) @@ -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("旧服务错误地执行了原动作") + } } diff --git a/backend/internal/repository/recovery_executor_external_test.go b/backend/internal/repository/recovery_executor_external_test.go index 9a42acd..9dac5eb 100644 --- a/backend/internal/repository/recovery_executor_external_test.go +++ b/backend/internal/repository/recovery_executor_external_test.go @@ -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"} { diff --git a/backend/internal/repository/recovery_reconciliation_external_test.go b/backend/internal/repository/recovery_reconciliation_external_test.go new file mode 100644 index 0000000..ac4bcc2 --- /dev/null +++ b/backend/internal/repository/recovery_reconciliation_external_test.go @@ -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) + } +} diff --git a/doc/Linux生产流程测试记录.md b/doc/Linux生产流程测试记录.md index 2731b58..d11f32f 100644 --- a/doc/Linux生产流程测试记录.md +++ b/doc/Linux生产流程测试记录.md @@ -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 真实中断恢复与关闭误判 ### 重载阶段中断与自动恢复 diff --git a/doc/Scert开发任务清单.md b/doc/Scert开发任务清单.md index 517991a..b199a9e 100644 --- a/doc/Scert开发任务清单.md +++ b/doc/Scert开发任务清单.md @@ -1,5 +1,12 @@ # Scert 开发任务清单 +## 2026年10月3日23:58 最新开发状态 + +- [x] 不确定处置重新核查已接通执行器、数据库、本机协议及管理终端;仅允许已结束 uncertain,保留原审计,成功才原子结束原事务并释放占用。 +- [✓] 最新五包专项及竞态通过;旧确认重放、身份变化、残留文件、HTTPS 失败、最终关闭失败及会话混淆均有回归覆盖。 +- [~] 新增隔离实库审计失败与重开验证,正在运行 MySQL/PostgreSQL、最新全量和静态检查。 +- [ ] 正式 Linux 发布及实机收尾尚未完成;当前线上仍为迁移二十四,不宣称已部署新能力。 + ## 2026年10月3日23:14 真实故障整改进度 - [✓] 正式包真实进程中断后双机自动回滚,任务保留重启失败历史,第二台网站继续通过严格 HTTPS。 diff --git a/doc/Scert整改任务执行清单.md b/doc/Scert整改任务执行清单.md index 887af22..3a694f7 100644 --- a/doc/Scert整改任务执行清单.md +++ b/doc/Scert整改任务执行清单.md @@ -1,5 +1,13 @@ # Scert 整改任务执行清单 +## 2026年10月3日23:58 当前执行状态 + +- [x] 独立重新核查实现、本机菜单五及迁移二十五已完成编码,原 uncertain 永久保留,只核验原方向,不执行远端写操作。 +- [✓] 五包专项及竞态通过;覆盖最新本机会话、协议和终端用例。 +- [~] 后端全量、静态检查、隔离 MySQL/PostgreSQL 迁移与执行器回归正在运行,结果以测试记录后续更新为准。 +- [ ] 正式包升级、实机重新核查释放占用、人工回滚与断连验证仍待执行。 +- [!] 公网、系统通知授权及首次初始化等生产门禁未关闭;不将源码测试代替上线验收。 + ## 2026年10月3日23:14 当前整改任务 - [✓] 真实部署重载阶段 SIGKILL 后,systemd 自动重启及双机回滚通过;第二台 HTTPS 正常。 diff --git a/doc/Scert整改合并优化清单.md b/doc/Scert整改合并优化清单.md index 26bae34..eaa2a0f 100644 --- a/doc/Scert整改合并优化清单.md +++ b/doc/Scert整改合并优化清单.md @@ -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 失败记录。 diff --git a/doc/本机部署核查使用说明.md b/doc/本机部署核查使用说明.md index e3cf00d..0822683 100644 --- a/doc/本机部署核查使用说明.md +++ b/doc/本机部署核查使用说明.md @@ -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`,按新核查编号查询;不要再次提交原动作或手动删锁。 + +运行中处置、证据不足、身份变化及现场不满足收尾条件均拒绝,不因等待超时接管。人工处置成功场景、不同版本证书回滚和完整首次生产验收仍须实测;不能把新增菜单视作这些验收已通过。 diff --git a/doc/部署事务人工处置设计.md b/doc/部署事务人工处置设计.md index 779bbb3..7fc57fc 100644 --- a/doc/部署事务人工处置设计.md +++ b/doc/部署事务人工处置设计.md @@ -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 共享首次结果。 diff --git a/doc/部署事务人工核查任务清单.md b/doc/部署事务人工核查任务清单.md index b198b6b..5f48e63 100644 --- a/doc/部署事务人工核查任务清单.md +++ b/doc/部署事务人工核查任务清单.md @@ -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 范围,不增加网络入口,不直接修改现场事务或删除占用。