This commit is contained in:
@@ -94,6 +94,14 @@
|
||||
4. 生产预备份、独立恢复演练、持久化协调作业、授权 API、真实进度、发布页面与完整存储治理仍未交付;以上验收项目保持待验证。
|
||||
5. 基础模块已随 `0.1.96 / 9105f8316cc870a62b5ddacd1496f0cb0f798d87` 提交、推送并部署到本地测试治理平台;运行 API/worker 的二进制摘要已读回校验。该部署不等于业务功能接线或完整验收通过;未执行生产同步、恢复或真实业务清理。完整验收仍受本节及下述缺失接线阻塞。
|
||||
|
||||
## 真实测试源只读诊断(未通过)
|
||||
|
||||
`TestLibrarySourceReadOnlyDiagnostic` 仅在明确 opt-in 下运行,数据库连接强制只读;只调用 ES 查询/PIT 和 OSS 对象读取,不执行生产操作、源数据清理或服务暂停。测试凭据仅通过子进程环境注入,不记录配置值。
|
||||
|
||||
本次实际执行退出码 1:对内库 125 条索引记录中有 13 条关联到未被完整文档集合纳入的分块;进一步只读查询定位为 4 个未删除分块(3 个 text、1 个 summary)缺少同租户的父文档,不是临时上下文内容比较失败。现有身份校验保持拒绝,没有忽略这些记录或据此放行。独立 OSS 读取校验覆盖 21 个资源、53,490,166 字节。对外库读到 0 文档、0 分块、1 个 Wiki 页面,候选结构归档可读回,但未冻结且模型兼容身份为明确诊断占位,不能当作版本验收通过。
|
||||
|
||||
证据:`backups/library-source-readonly-20261028/`。真实源诊断失败与默认 CI 的环境跳过分别记录;此次发现不授权清理这批分块或索引,之前仅限死信 ID 1 的授权不扩展使用。
|
||||
|
||||
## 联合隔离捕获验收
|
||||
|
||||
新增 `TestLibraryCaptureIsolatedStack`,在同一次 `CaptureLibrary` 调用中连接真实隔离 PostgreSQL、随机 Elasticsearch 索引、真实 Docker 暂停/恢复控制器和本地资源文件读取器。写入服务使用无网络、无宿主挂载的 busybox 夹具,不是实际 WeKnora;资源读取不是实际 OSS/S3 验收。
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package v071
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"knowledge-governance/internal/librarysnapshot"
|
||||
)
|
||||
|
||||
// Live read-only diagnostic, not a cross-system frozen version or release gate.
|
||||
// Credentials are injected into this process only; never logged or persisted.
|
||||
func TestLibrarySourceReadOnlyDiagnostic(t *testing.T) {
|
||||
if os.Getenv("KG_LIBRARY_SOURCE_READONLY") != "1" {
|
||||
t.Skip("requires explicit read-only test-source credentials")
|
||||
}
|
||||
var config struct {
|
||||
DatabaseURL string
|
||||
TenantID uint64
|
||||
Scopes map[string]string
|
||||
Search SearchExporterConfig
|
||||
Object S3ObjectStoreConfig
|
||||
}
|
||||
if json.Unmarshal([]byte(os.Getenv("KG_LIBRARY_SOURCE_CONFIG")), &config) != nil || config.TenantID == 0 || len(config.Scopes) != 2 || config.Scopes["internal"] == "" || config.Scopes["external"] == "" {
|
||||
t.Fatal("invalid read-only diagnostic configuration")
|
||||
}
|
||||
cfg, err := pgxpool.ParseConfig(config.DatabaseURL)
|
||||
if err != nil {
|
||||
t.Fatal("invalid diagnostic PG configuration")
|
||||
}
|
||||
cfg.ConnConfig.RuntimeParams["default_transaction_read_only"] = "on"
|
||||
cfg.ConnConfig.RuntimeParams["statement_timeout"] = "30000"
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||
defer cancel()
|
||||
db, err := pgxpool.NewWithConfig(ctx, cfg)
|
||||
if err != nil {
|
||||
t.Fatal("open diagnostic PG")
|
||||
}
|
||||
defer db.Close()
|
||||
var readOnly string
|
||||
if err = db.QueryRow(ctx, "SHOW default_transaction_read_only").Scan(&readOnly); err != nil || readOnly != "on" {
|
||||
t.Fatal("read-only session not enforced")
|
||||
}
|
||||
if err = checkLibrarySourceSettlement(ctx, db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
search, err := NewSearchExporter(config.Search)
|
||||
if err != nil {
|
||||
t.Fatal("invalid diagnostic search configuration")
|
||||
}
|
||||
objects, err := NewS3ObjectStore(config.Object)
|
||||
if err != nil {
|
||||
t.Fatal("invalid diagnostic object configuration")
|
||||
}
|
||||
for _, scope := range []string{"internal", "external"} {
|
||||
t.Run(scope, func(t *testing.T) {
|
||||
identity := PostgresIdentity{TenantID: config.TenantID, KnowledgeBaseID: config.Scopes[scope]}
|
||||
pg, err := ExportLibrary(ctx, db, identity)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
es, err := search.ExportLibrary(ctx, SearchIdentity{TenantID: identity.TenantID, KnowledgeBaseID: identity.KnowledgeBaseID}, 100000)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = validateLibraryCandidate(identity, pg, es); err != nil {
|
||||
missing, deleted, wrongParent := 0, 0, 0
|
||||
reasons := map[string]int{}
|
||||
chunks := map[string]libraryRow{}
|
||||
for _, raw := range pg.Table("chunks").Rows {
|
||||
var r libraryRow
|
||||
if json.Unmarshal(raw, &r) != nil {
|
||||
t.Fatal("invalid diagnostic chunk")
|
||||
}
|
||||
chunks[r.ID] = r
|
||||
}
|
||||
for _, doc := range es.Documents {
|
||||
var r libraryRow
|
||||
if json.Unmarshal(doc.Source, &r) != nil {
|
||||
t.Fatal("invalid diagnostic index")
|
||||
}
|
||||
if c, ok := chunks[r.ChunkID]; ok {
|
||||
if r.KnowledgeID != c.KnowledgeID {
|
||||
wrongParent++
|
||||
}
|
||||
continue
|
||||
}
|
||||
var exists bool
|
||||
if e := db.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM chunks WHERE id=$1 AND tenant_id=$2)`, r.ChunkID, identity.TenantID).Scan(&exists); e != nil {
|
||||
t.Fatal("diagnostic relation query failed")
|
||||
}
|
||||
if exists {
|
||||
deleted++
|
||||
var reason string
|
||||
if e := db.QueryRow(ctx, `SELECT CASE WHEN c.deleted_at IS NOT NULL THEN 'chunk_soft_deleted' WHEN k.id IS NULL THEN 'parent_missing' WHEN k.deleted_at IS NOT NULL THEN 'parent_soft_deleted' ELSE 'scope_mismatch' END FROM chunks c LEFT JOIN knowledges k ON k.id=c.knowledge_id AND k.tenant_id=c.tenant_id WHERE c.id=$1 AND c.tenant_id=$2`, r.ChunkID, identity.TenantID).Scan(&reason); e != nil {
|
||||
t.Fatal("diagnostic exclusion query failed")
|
||||
}
|
||||
reasons[reason]++
|
||||
} else {
|
||||
missing++
|
||||
}
|
||||
}
|
||||
t.Errorf("candidate rejected: %v; index_records=%d absent_chunk_records=%d existing_but_excluded_chunk_records=%d wrong_parent_records=%d", err, len(es.Documents), missing, deleted, wrongParent)
|
||||
t.Logf("index exclusion reasons: %v", reasons)
|
||||
probe, e := NewObjectExporter(objects, ObjectExporterLimits{MaxObjectBytes: 64 * 1024 * 1024, MaxTotalBytes: 256 * 1024 * 1024})
|
||||
if e != nil {
|
||||
t.Fatal(e)
|
||||
}
|
||||
assets, e := probe.Export(ctx, pg)
|
||||
if e != nil {
|
||||
t.Errorf("independent read-only resource probe: %v", e)
|
||||
} else {
|
||||
t.Logf("independent resource probe objects=%d bytes=%d", len(assets.Objects), assets.TotalBytes)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Logf("READONLY_DIAGNOSTIC frozen=false scope=%s documents=%d chunks=%d resources=%d wiki_pages=%d index_records=%d", scope, pg.Table("knowledges").RowCount, pg.Table("chunks").RowCount, pg.Table("resources").RowCount, pg.Table("wiki_pages").RowCount, len(es.Documents))
|
||||
root := t.TempDir()
|
||||
if err = os.Chmod(root, 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archive, err := librarysnapshot.Open(root, 64*1024*1024)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer archive.Close()
|
||||
// The model name is an explicit diagnostic placeholder, not deployment compatibility evidence.
|
||||
compatibility := librarysnapshot.Compatibility{WeKnoraVersion: "v0.7.1", SchemaVersion: 74, RetrieverEngine: "elasticsearch_v8", EmbeddingModel: "diagnostic-unverified-model", EmbeddingDimension: 1024, IndexMappingSHA256: es.MappingSHA256}
|
||||
id, err := ArchiveLibraryCandidate(ctx, archive, scope, identity, compatibility, pg, es, objects, time.Now().UTC())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
manifest, err := archive.Load(id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var count, bytes int64
|
||||
for _, part := range manifest.Parts {
|
||||
if part.Kind == "resource" {
|
||||
count++
|
||||
bytes += part.Bytes
|
||||
}
|
||||
}
|
||||
if count != int64(pg.Table("resources").RowCount) {
|
||||
t.Fatal("resource archive coverage mismatch")
|
||||
}
|
||||
t.Logf("READONLY_DIAGNOSTIC frozen=false scope=%s archived_resources=%d resource_bytes=%d archive_verified=true compatibility_verified=false", scope, count, bytes)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,11 @@
|
||||
"⚠️ 文件缺失或解析不了 ⇒ 门禁红:范围声明不出来时,不许默认放行。"
|
||||
],
|
||||
"optional": [
|
||||
{
|
||||
"package": "knowledge-governance/internal/releaseadapter/v071",
|
||||
"test": "TestLibrarySourceReadOnlyDiagnostic",
|
||||
"reason": "仅明确只读测试源授权及进程内凭据下运行;真实 PG、ES、OSS 不冻结,属于诊断而非一致性或生产放行;默认 CI 不注入测试源凭据,实际失败必须另行保留而非算通过"
|
||||
},
|
||||
{
|
||||
"package": "knowledge-governance/internal/releaseadapter/v071",
|
||||
"test": "TestLibraryCaptureIsolatedStack",
|
||||
|
||||
Reference in New Issue
Block a user