补齐人工处置PostgreSQL和MySQL实库执行器验证

This commit is contained in:
xuans 2026-10-03 21:49:06 +08:00
parent 0f2e6bfcce
commit e0d16a6ab1
10 changed files with 539 additions and 1 deletions

View File

@ -0,0 +1,67 @@
package database
import (
"fmt"
"testing"
"git.pcux.cn/xuans/Scert/backend/internal/testsupport/postgrestest"
)
func TestPostgresRealRecoveryDispositionMigration(t *testing.T) {
server := postgrestest.New(t)
for version := 22; version <= 24; version++ {
t.Run(fmt.Sprint(version), func(t *testing.T) {
cfg := server.Database(t)
db, err := Open(t.Context(), cfg)
if err != nil {
t.Fatal("隔离迁移连接失败")
}
t.Cleanup(func() { _ = db.Close() })
if _, err := db.DB.ExecContext(t.Context(), migrationTableSQL(cfg.Driver)); err != nil {
t.Fatal(err)
}
for previous := 1; previous < version; previous++ {
if err := applyMigration(t.Context(), db.DB, cfg.Driver, previous); err != nil {
t.Fatal(err)
}
}
content, err := migrationFiles.ReadFile(fmt.Sprintf("migrations/postgres/%04d_%s.sql", version, migrationName(version)))
if err != nil {
t.Fatal(err)
}
// 已执行 DDL 但版本尚未落库时中止事务,不能留下半迁移结构。
tx, err := db.DB.BeginTx(t.Context(), nil)
if err != nil {
t.Fatal(err)
}
for _, query := range migrationStatements(content) {
if _, err := tx.ExecContext(t.Context(), query); err != nil {
_ = tx.Rollback()
t.Fatal(err)
}
}
if err := tx.Rollback(); err != nil {
t.Fatal(err)
}
current, err := currentMigrationVersion(t.Context(), db.DB)
if err != nil || current != version-1 {
t.Fatal("回滚后版本记录改变", err)
}
for range 2 {
if err := Migrate(t.Context(), db.DB, cfg.Driver); err != nil {
t.Fatal("中断后的迁移重入失败", err)
}
}
current, err = currentMigrationVersion(t.Context(), db.DB)
if err != nil || current != latestMigrationVersion {
t.Fatal("迁移未达到当前版本", err)
}
for _, table := range []string{"recovery_dispositions", "recovery_claims", "recovery_events"} {
var count int
if err := db.DB.QueryRowContext(t.Context(), "SELECT COUNT(*) FROM "+table).Scan(&count); err != nil || count != 0 {
t.Fatal("处置表未正确初始化", err)
}
}
})
}
}

View File

@ -0,0 +1,212 @@
package repository
import (
"context"
"database/sql"
"errors"
"maps"
"reflect"
"strings"
"sync"
"sync/atomic"
"testing"
"github.com/go-sql-driver/mysql"
"git.pcux.cn/xuans/Scert/backend/internal/config"
"git.pcux.cn/xuans/Scert/backend/internal/database"
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
"git.pcux.cn/xuans/Scert/backend/internal/testsupport/mysqltest"
"git.pcux.cn/xuans/Scert/backend/internal/testsupport/postgrestest"
)
type recoveryExternalFixture struct {
database func(*testing.T) config.DatabaseConfig
inject func(*testing.T, *sql.DB, config.DatabaseConfig, string)
}
func TestRecoveryExecutorPostgresReal(t *testing.T) {
server := postgrestest.New(t)
runExternalRecovery(t, recoveryExternalFixture{database: server.Database, inject: func(t *testing.T, db *sql.DB, _ config.DatabaseConfig, stage string) {
for _, query := range []string{
`CREATE FUNCTION reject_recovery_event() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN IF NEW.event_type = '` + stage + `' THEN RAISE EXCEPTION 'injected'; END IF; RETURN NEW; END $$`,
`CREATE TRIGGER reject_recovery_event BEFORE INSERT ON recovery_events FOR EACH ROW EXECUTE FUNCTION reject_recovery_event()`,
} {
if _, err := db.ExecContext(t.Context(), query); err != nil {
t.Fatal("安装隔离审计故障失败", err)
}
}
}})
}
func TestRecoveryExecutorMySQLReal(t *testing.T) {
server := mysqltest.New(t)
runExternalRecovery(t, recoveryExternalFixture{database: server.Database, inject: func(t *testing.T, _ *sql.DB, cfg config.DatabaseConfig, stage string) {
parsed, err := mysql.ParseDSN(cfg.DSN)
if err != nil || !strings.HasPrefix(parsed.DBName, "scert_test_") {
t.Fatal("拒绝对非隔离库注入故障")
}
schema := "`" + strings.ReplaceAll(parsed.DBName, "`", "``") + "`"
query := `CREATE TRIGGER ` + schema + `.reject_recovery_event BEFORE INSERT ON ` + schema + `.recovery_events FOR EACH ROW BEGIN IF NEW.event_type = '` + stage + `' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'injected'; END IF; END`
// MySQL 开启二进制日志时,仅隔离管理连接创建触发器;业务账户不提权。
if _, err := server.Admin().ExecContext(t.Context(), query); err != nil {
t.Fatal("安装隔离审计故障失败", err)
}
}})
}
func openRecoveryExternal(t *testing.T, cfg config.DatabaseConfig) *sql.DB {
t.Helper()
db, err := database.Open(t.Context(), cfg)
if err != nil {
t.Fatal("连接隔离实库失败")
}
t.Cleanup(func() { _ = db.Close() })
return db.DB
}
// 只替换远端文件实现,准备、确认、执行、迁移、占用和审计均走实际产品代码。
func runExternalRecovery(t *testing.T, fixture recoveryExternalFixture) {
t.Helper()
for _, action := range []deployment.RecoveryAction{deployment.RecoveryActionRecord, deployment.RecoveryActionCommit, deployment.RecoveryActionRollback} {
for _, stage := range []string{"none", "started", "finished"} {
t.Run(string(action)+"/"+stage, func(t *testing.T) {
cfg := fixture.database(t)
db := openRecoveryExternal(t, cfg)
if err := database.Migrate(t.Context(), db, cfg.Driver); err != nil {
t.Fatal(err)
}
repo, _, service, client, server, record := seedRecoveryExecutor(t, NewSQLDeploymentTransactionRepository(db, cfg.Driver), db)
before, err := repo.ListPendingDeploymentTransactions(t.Context())
if err != nil {
t.Fatal(err)
}
filesBefore := maps.Clone(client.files)
resolve := func(context.Context, string) (deployment.Server, error) { return server, nil }
confirmation, token, err := service.PrepareRecoveryConfirmation(t.Context(), record.ID, "unix:0", action, resolve)
if err != nil {
t.Fatal("实库准备失败", err)
}
if stage != "none" {
fixture.inject(t, db, cfg, stage)
}
item, err := service.ExecuteRecoveryDisposition(t.Context(), confirmation, "unix:0", token, resolve)
if stage == "none" {
if err != nil || item.Status != "succeeded" {
t.Fatal("实库执行未成功", err)
}
recoveryCount(t, db, "recovery_dispositions", 1)
recoveryCount(t, db, "recovery_claims", 0)
recoveryCount(t, db, "recovery_events", 2)
} else if !errors.Is(err, deployment.ErrRecoveryDispositionUnavailable) || item.Status == "succeeded" {
t.Fatal("审计故障未拒绝成功结果", err)
}
if _, err := service.ExecuteRecoveryDisposition(t.Context(), confirmation, "unix:0", token, resolve); !errors.Is(err, deployment.ErrRecoveryConfirmation) {
t.Fatal("实库执行确认被重放", err)
}
// 实际关闭连接池后重新打开,不能靠旧仓储对象的内存证明持久性。
if err := db.Close(); err != nil {
t.Fatal(err)
}
db = openRecoveryExternal(t, cfg)
if err := database.Migrate(t.Context(), db, cfg.Driver); err != nil {
t.Fatal("实库重开迁移失败", err)
}
repo = NewSQLDeploymentTransactionRepository(db, cfg.Driver)
pending, err := repo.ListPendingDeploymentTransactions(t.Context())
if err != nil {
t.Fatal(err)
}
if stage == "started" {
for _, table := range []string{"recovery_dispositions", "recovery_claims", "recovery_events"} {
recoveryCount(t, db, table, 0)
}
if !reflect.DeepEqual(before, pending) || !reflect.DeepEqual(filesBefore, client.files) {
t.Fatal("开始审计失败修改了原事务或远端")
}
return
}
stored, err := repo.GetRecoveryDisposition(t.Context(), confirmation.DispositionID())
if err != nil {
t.Fatal("重开后处置记录丢失", err)
}
restarted := deployment.NewServiceWithJournal(nil, repo)
view, err := restarted.GetRecoveryDisposition(t.Context(), confirmation.DispositionID())
if err != nil || view.Validate() != nil || !reflect.DeepEqual(view, stored.Summary()) {
t.Fatal("重开后脱敏查询与持久结果不一致", err)
}
if stage == "finished" {
recoveryCount(t, db, "recovery_claims", 1)
recoveryCount(t, db, "recovery_events", 1)
if stored.Status != "running" || len(pending) != 1 || pending[0].Status != deployment.TransactionStatusRecoveryRequired {
t.Fatal("结束审计失败丢失占用或发布了事务终态")
}
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)
}
} else if stored.Status != "succeeded" {
t.Fatal("成功结果重开后丢失")
}
if action == deployment.RecoveryActionRecord {
if !reflect.DeepEqual(before, pending) || !reflect.DeepEqual(filesBefore, client.files) {
t.Fatal("仅记录动作改变原事务、证据或远端文件")
}
} else {
expected, result := "new", deployment.RecoveryResultCommitted
if action == deployment.RecoveryActionRollback {
expected, result = "old", deployment.RecoveryResultRolledBack
}
for _, path := range record.Paths {
if client.files[path] != expected {
t.Fatal("远端动作与明确选择不一致")
}
}
if stage == "none" && (len(pending) != 0 || stored.ResultCode != result) {
t.Fatal("成功结果与原事务终态不一致")
}
}
})
}
}
t.Run("concurrent_idempotency", func(t *testing.T) {
cfg := fixture.database(t)
db := openRecoveryExternal(t, cfg)
if err := database.Migrate(t.Context(), db, cfg.Driver); err != nil {
t.Fatal(err)
}
repo, _, intent := seedRecoveryDisposition(t, NewSQLDeploymentTransactionRepository(db, cfg.Driver), db, deployment.RecoveryActionRecord)
var created atomic.Int32
var group sync.WaitGroup
for range 8 {
group.Go(func() {
_, fresh, err := repo.BeginRecoveryDisposition(t.Context(), intent)
if err != nil {
t.Errorf("并发幂等请求失败:%v", err)
}
if fresh {
created.Add(1)
}
})
}
group.Wait()
if created.Load() != 1 {
t.Fatal("并发请求没有精确创建一次处置")
}
conflict := intent
conflict.Operator = "unix:1"
if _, _, err := repo.BeginRecoveryDisposition(t.Context(), conflict); !errors.Is(err, deployment.ErrRecoveryDispositionConflict) {
t.Fatal("不同操作者复用了幂等结果", err)
}
conflict = intent
conflict.ID, conflict.RequestKey = strings.Repeat("e", 64), strings.Repeat("f", 64)
if _, _, err := repo.BeginRecoveryDisposition(t.Context(), conflict); !errors.Is(err, deployment.ErrRecoveryDispositionBusy) {
t.Fatal("其他请求覆盖已有占用", err)
}
for _, table := range []string{"recovery_dispositions", "recovery_claims", "recovery_events"} {
recoveryCount(t, db, table, 1)
}
})
}

View File

@ -77,6 +77,11 @@ func executorIntegrationFixture(t *testing.T) (*SQLDeploymentTransactionReposito
_, db := newTaskRepositoryTest(t)
t.Cleanup(func() { _ = db.Close() })
repo := NewSQLDeploymentTransactionRepository(db)
return seedRecoveryExecutor(t, repo, db)
}
func seedRecoveryExecutor(t *testing.T, repo *SQLDeploymentTransactionRepository, db *sql.DB) (*SQLDeploymentTransactionRepository, *sql.DB, *deployment.Service, *executorIntegrationClient, deployment.Server, deployment.TransactionRecord) {
t.Helper()
server := deployment.Server{
ID: "executor-server", Name: "executor-server", Address: "192.0.2.10", Port: 22, Username: "deploy",
AuthMethod: deployment.SSHAuthPassword, PasswordRef: "secret://ssh/password/test", HostKeyFingerprint: "SHA256:test",

View File

@ -0,0 +1,197 @@
package postgrestest
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"database/sql"
"encoding/hex"
"encoding/pem"
"fmt"
"math/big"
"net"
"net/url"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
"git.pcux.cn/xuans/Scert/backend/internal/config"
)
// Server 只管理本次测试创建的隔离集群,不接受已有实例的连接串。
type Server struct {
root string
bin string
port string
password string
admin *sql.DB
sequence int
}
func New(t *testing.T) *Server {
t.Helper()
bin, root := os.Getenv("SCERT_POSTGRES_TEST_BIN"), os.Getenv("SCERT_POSTGRES_TEST_ROOT")
if bin == "" {
t.Skip("未启用 PostgreSQL 隔离实库测试")
}
if !filepath.IsAbs(bin) || !filepath.IsAbs(root) {
t.Fatal("PostgreSQL 工具与隔离产物目录必须使用绝对路径")
}
if err := os.MkdirAll(root, 0o700); err != nil {
t.Fatal(err)
}
root, err := os.MkdirTemp(root, "scert-postgres-integration-")
if err != nil {
t.Fatal(err)
}
var secret [32]byte
if _, err := rand.Read(secret[:]); err != nil {
t.Fatal(err)
}
f := &Server{root: root, bin: bin, password: hex.EncodeToString(secret[:])}
t.Logf("PostgreSQL 隔离产物保留于:%s", root)
ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, filepath.Join(bin, "postgres"), "--version")
cmd.Env = f.environment()
version, err := cmd.Output()
if err != nil || !strings.HasPrefix(string(version), "postgres (PostgreSQL) ") {
t.Fatal("无法确认隔离 PostgreSQL 版本")
}
t.Logf("隔离服务端:%s", strings.TrimSpace(string(version)))
passwordFile := filepath.Join(root, "bootstrap-password.txt")
write(t, passwordFile, []byte(f.password+"\n"))
data := filepath.Join(root, "cluster")
f.control(t, "initdb", "-D", data, "-U", "postgres", "--pwfile="+passwordFile, "--auth=scram-sha-256", "--encoding=UTF8", "--no-locale")
f.createTLS(t)
listener, err := net.Listen("tcp4", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
f.port = strconv.Itoa(listener.Addr().(*net.TCPAddr).Port)
if err := listener.Close(); err != nil {
t.Fatal(err)
}
settings := "shared_buffers = '16MB'\nwork_mem = '1MB'\nmaintenance_work_mem = '16MB'\nmax_connections = 24\nmax_parallel_workers = 0\nmax_worker_processes = 0\n"
settings += "listen_addresses = '127.0.0.1'\nport = " + f.port + "\nssl = on\nssl_cert_file = 'server.crt'\nssl_key_file = 'server.key'\npassword_encryption = 'scram-sha-256'\nunix_socket_directories = ''\n"
write(t, filepath.Join(data, "postgresql.conf"), []byte(settings))
write(t, filepath.Join(data, "pg_hba.conf"), []byte("hostssl all all 127.0.0.1/32 scram-sha-256\nhost all all 0.0.0.0/0 reject\nhost all all ::0/0 reject\n"))
// 即使启动中途失败,也只停止本测试目录中的集群;不删除产物。
t.Cleanup(func() {
if f.admin != nil {
_ = f.admin.Close()
}
if _, err := os.Stat(filepath.Join(data, "postmaster.pid")); os.IsNotExist(err) {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, filepath.Join(bin, "pg_ctl"), "-D", data, "-m", "fast", "-w", "-t", "20", "stop")
cmd.Env = f.environment()
cmd.WaitDelay = 2 * time.Second
if err := cmd.Run(); err != nil {
t.Errorf("停止隔离 PostgreSQL 失败:%v", err)
}
})
f.control(t, "pg_ctl", "-D", data, "-l", filepath.Join(root, "postgres.log"), "-w", "-t", "30", "start")
f.admin, err = sql.Open("pgx", f.dsn("postgres", "postgres"))
if err != nil || f.admin.PingContext(t.Context()) != nil {
t.Fatal("隔离 PostgreSQL 严格 TLS 管理连接失败")
}
return f
}
func (f *Server) Database(t *testing.T) config.DatabaseConfig {
t.Helper()
f.sequence++
name, owner := fmt.Sprintf("scert_test_%d", f.sequence), fmt.Sprintf("scert_owner_%d", f.sequence)
// 每个用例都有独立数据库和无超级权限的属主,不能访问业务数据库。
for _, query := range []string{
"CREATE ROLE " + owner + " LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '" + f.password + "'",
"CREATE DATABASE " + name + " OWNER " + owner,
"REVOKE ALL ON DATABASE " + name + " FROM PUBLIC",
} {
if _, err := f.admin.ExecContext(t.Context(), query); err != nil {
t.Fatal("创建隔离 PostgreSQL 数据库及受限属主失败")
}
}
return config.DatabaseConfig{Driver: "postgres", DSN: f.dsn(name, owner), MaxOpenConns: 8, MaxIdleConns: 2}
}
func (f *Server) dsn(name, user string) string {
u := &url.URL{Scheme: "postgres", Host: net.JoinHostPort("127.0.0.1", f.port), Path: "/" + name, User: url.UserPassword(user, f.password)}
u.RawQuery = url.Values{"sslmode": {"verify-full"}, "sslrootcert": {filepath.Join(f.root, "cluster", "server.crt")}, "connect_timeout": {"5"}}.Encode()
return u.String()
}
func (f *Server) environment() []string {
env := []string{}
for _, entry := range os.Environ() {
key, _, _ := strings.Cut(entry, "=")
upper := strings.ToUpper(key)
if strings.HasPrefix(upper, "PG") || upper == "PATH" || upper == "LC_ALL" || upper == "LANG" {
continue
}
env = append(env, entry)
}
return append(env, "PATH="+f.bin+string(os.PathListSeparator)+os.Getenv("PATH"), "LC_ALL=C", "LANG=C")
}
func (f *Server) control(t *testing.T, tool string, args ...string) {
t.Helper()
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
defer cancel()
cmd := exec.CommandContext(ctx, filepath.Join(f.bin, tool), args...)
cmd.Env = f.environment()
// 后台进程不能继承 Go 输出复制管道,否则父进程会一直等待。
output, err := os.OpenFile(filepath.Join(f.root, tool+".log"), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
if err != nil {
t.Fatal(err)
}
defer output.Close()
cmd.Stdout, cmd.Stderr = output, output
cmd.WaitDelay = 2 * time.Second
if err := cmd.Run(); err != nil {
t.Fatalf("隔离工具 %s 失败,请检查保留日志:%v", tool, err)
}
}
func write(t *testing.T, path string, value []byte) {
t.Helper()
if err := os.WriteFile(path, value, 0o600); err != nil {
t.Fatal(err)
}
}
func (f *Server) createTLS(t *testing.T) {
t.Helper()
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatal(err)
}
certificate := &x509.Certificate{
SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "Scert isolated PostgreSQL"},
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(24 * time.Hour),
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, IsCA: true, BasicConstraintsValid: true,
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
}
der, err := x509.CreateCertificate(rand.Reader, certificate, certificate, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
private, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
t.Fatal(err)
}
write(t, filepath.Join(f.root, "cluster", "server.crt"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}))
write(t, filepath.Join(f.root, "cluster", "server.key"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: private}))
}

View File

@ -1,5 +1,17 @@
# Linux 生产流程测试记录
## 2026年10月3日21:48 外部数据库专项
- [✓] 21:41 实查 `scert-local-actions-fixed-20261003-2125.service` 已 inactive、MainPID=0、Result=success,`LOCAL_ACTIONS_FIXED_HTTP_FULL_RACE_EXIT=0`;HTTP 全包竞态约 393.752 秒,五项检查完整通过。
- [x] 新四份测试源码归档 SHA-256 为 `421b52f1916f7be8cf088e0c0bd9623ad71e777f7b1439277a7e58faa4045e24`,本地与第一台一致;旧任务终止后才更新隔离源码。
- [x] PostgreSQL 14.24 工具经 APT 下载校验后仅解压在 `/var/tmp/scert-pg14-disposition-20261003`,未安装系统服务。Git 隔离目录属主检查首次阻止任务启动,改用单条命令的 safe.directory 后启动,没有修改全局 Git 配置或重复启动测试。
- [✓] `scert-external-disposition-20261003-2143.service` 的 `external-disposition.log`:PostgreSQL 迁移 22—24 三场景通过,数据库包约 3.672 秒;执行器十场景通过,仓储包约 7.603 秒;`EXTERNAL_POSTGRES_EXIT=0`。
- [✓] 同日志 MySQL 5.7.44 执行器十场景通过,约 25.574 秒;`EXTERNAL_MYSQL_EXIT=0`。三动作分别覆盖正常、开始审计失败、结束审计失败,另外包含八并发幂等;重开的是实际连接池,远端文件和站点核验是测试实现。
- [x] 21:46 查询无 PostgreSQL 残留进程,测试自动停止独立集群;仅 SQL 存储专项,不替代 PostgreSQL 16 备份工具、正式包或首次 Web 验收。
- [✓] 本地 `npm.cmd test`:275 项通过,零失败、零跳过。
- [~] 最新 SQLite 执行器、完整静态检查及后端普通全量仍在上述同一单元继续执行;不提前标记整组成功。
## 2026年10月3日21:32 本机接入修复版回归
- [x] 首轮任务 `scert-local-actions-20261003-2116.service` 已失败终止,`LOCAL_ACTIONS_RACE_EXIT=1`。分片测试夹具使用默认四 KiB 读取器导致过早失败;已改用协议最大帧大小的读取器,没有放宽生产协议。HTTP 全包竞态五分钟超时,堆栈位于既有 SQLite 迁移,不能记为通过。

View File

@ -1,5 +1,13 @@
# Scert 开发任务清单
## 2026年10月3日21:48 当前开发进度
- [✓] 本机协议/终端接入的五项 Linux 回归全部正常结束;`0f2e6bf` 已提交推送。
- [x] 新增隔离 PostgreSQL 测试支持和跨 PostgreSQL/MySQL 的真实执行器矩阵,复用远端夹具,数据库和服务执行链均使用实际产品代码。
- [✓] PostgreSQL 迁移三场景、执行器十场景及 MySQL 执行器十场景通过;含审计失败、真实连接池重开、占用和并发幂等。
- [✓] 前端 275 项测试通过;最新后端剩余回归继续运行,正式包与生产闭环尚未完成。
## 2026年10月3日21:32 当前开发进度
- [x] 受保护本机会话、协议和终端三动作及结果查询已编码;确认绑定会话和真实对端,一次消费,失败或断连不重放。

View File

@ -1,12 +1,21 @@
# Scert 整改任务执行清单
## 2026年10月3日21:48 最新执行结论
- [✓] 本机接入整组 Linux 回归正常结束,五项退出码零;检查点 `0f2e6bf` 已推送。
- [✓] PostgreSQL 14.24 迁移三场景、执行器十场景和 MySQL 5.7.44 执行器十场景通过;仅隔离实库,不是实际 SSH/HTTPS 验收。
- [✓] 前端 275 项测试通过;第二台磁盘剩余 26 GB,不再作为阻塞。
- [~] 最新 SQLite 执行器、静态检查和后端全量仍运行;之后构建正式 Linux 包。
- [ ] 三数据库首次 Web、系统邮件和双机生产门禁仍待完成。
## 2026年10月3日21:32 最新执行结论
- [x] R3-F/G/H 本机会话、协议隔离、终端风险确认及断连查询已接入。
- [✓] 修复版 Linux 三包竞态、HTTP 本机专项竞态、静态检查和后端全量退出码零。
- [~] HTTP 全包竞态仍运行,不能把运行中的整组标通过;首次夹具失败和 HTTP 五分钟超时不隐去。
- [~] 继续 R4 必要外部数据库处置原子性与重开查询;后续为 R5 正式 Linux 包与双机验收。
- [!] 第二台磁盘已实查剩余 26 GB,不再属于阻塞;系统邮件和完整生产门禁仍未通过。
- [x] 第二台磁盘已实查剩余 26 GB,不再属于阻塞;系统邮件和完整生产门禁仍未通过。
## 2026年10月3日21:00 最新执行结论

View File

@ -1,5 +1,15 @@
# Scert 整改、合并与优化清单
## 2026年10月3日21:48 实库与本机入口进度
- [✓] R3 本机接入修复版任务已正常结束,五项退出码零,HTTP 全包竞态约 393.752 秒;代码检查点 `0f2e6bf` 已推送。
- [✓] R4 PostgreSQL 14.24 隔离实库:迁移 22—24 三个事务回滚重入场景,以及执行器三动作、两类审计故障、重开查询/占用、八并发幂等共十个场景通过。
- [✓] R4 MySQL 5.7.44 同一执行器十场景通过,业务连接仍为受限账户;只在本次隔离数据库注入审计故障。
- [~] 最新 SQLite 执行器竞态、静态检查和普通后端全量正在继续;外部实库证明不包括实际远端文件或 TLS 站点动作。
- [✓] 当前前端 275 项测试全部通过,无失败和跳过。
- [ ] 下一项为正式 Linux 候选包和双机生产验证;PostgreSQL 本轮为 SQL 持久化专项,不代表 PostgreSQL 16 备份工具、首次 Web 或最终生产验收。
## 2026年10月3日21:32 最新整改进度
本节优先于历史状态;仅 Linux,生产验收仍未通过。

View File

@ -1,5 +1,14 @@
# 部署事务人工处置设计
## 2026年10月3日21:48 外部持久化证据
- [✓] 本机接入五项回归已全部通过,包括 HTTP 全包竞态。
- [✓] 真实 PostgreSQL 14.24 和 MySQL 5.7.44 的产品执行器通过三动作、开始与结束审计故障、重开查询/占用和并发幂等;远端文件仍为测试实现。
- [✓] PostgreSQL 新迁移 22—24 在事务 DDL 执行后回滚,再次迁移可达到连续版本 24,并可重复迁移。
- [x] PostgreSQL 仅回环且 verify-full,临时集群随机凭据,业务连接不是超级用户;测试不接受现有实例连接串,不清理业务数据库。
- [ ] 正式安装包与真实 SSH 文件及 HTTPS 站点闭环仍需直接验证。
## 2026年10月3日21:32 本机接入实现
- [x] 准备确认对象仅留在服务端会话,终端只取得短期令牌和脱敏摘要;执行先清空确认,所有失败尝试均不能再次使用。

View File

@ -1,5 +1,14 @@
# 部署事务人工核查任务清单
## 2026年10月3日21:48 外部数据库复核
- [✓] R3-F/G/H 本机接入源码回归全部结束,HTTP 全包竞态通过;正式包入口尚待验证。
- [✓] R4-D PostgreSQL 14.24:迁移 22—24 中止事务后重入通过;三动作、开始/结束审计失败、连接池重开查询与占用,以及八并发幂等通过。
- [✓] R4-E MySQL 5.7.44:执行器同一十场景通过,不提升业务账户权限。
- [~] 新增测试后的 SQLite 执行器回归、静态检查与后端全量正在执行。
- [ ] R5 正式包、实际终端与双机文件/HTTPS 动作,以及完整首次 Web 与系统邮件门禁尚未完成。
## 2026年10月3日21:32 本机接入与下一批任务
沿用已确认需求,不增加恢复网络端口,不处理 Windows。