Files
knowledge-governance/cmd/worker/library_role_test.go
T
姚章浩 e6472b0ec6 feat(library): 云测试发布目标接入的角色分离、快照搬运与图谱门禁
为什么改:发布执行链是"与目标同机"假设写死的(本机 docker.sock、本机 drain.json、
本机 Redis/PG),把目标换成云测试不能只改展示 URL;直接搬整套 worker 又会抢走
本机封存/QA/周报队列并按本机配置写错目标。所以先把"谁消费哪段队列"变成显式角色,
再补齐跨主机搬运与目标侧门禁。

角色(LIBRARY_WORKER_ROLE=full/release-executor/capture)不只影响 handler 列表,
还决定是否拥有 DDL(release-executor 只读校验迁移版本,落后即拒绝启动)、
收拾哪些 library_jobs.kind、是否跑 release 接管扫描、是否起 scheduler/QA/周报/探测。
capture 保留本机原有全部业务;release-executor 只挂 sync/restore/recover/baseline。

快照跨主机:新增内容寻址的 bundle(版本清单先落、blob 按 sha256 命名、清单最后
Link 作提交点),pack/unpack 幂等且拒绝覆盖与同名不同内容;执行器使用前仍逐 blob
重算 sha256,因此传输不可变有两道证据。

目标侧门禁:目标知识库 graph_enabled=true 时拒绝发布(发布只写 PG/ES/OSS,不写
Neo4j;未知也拒绝),只读检测、不改目标配置、不做图谱迁移。

云端入口:新增 drain 感知的 ingress 闸门(建连超时与读超时分离,半关闭单向传播,
SIGTERM 排空后写 drain.json 并 exit 0/2)、compose 叠加片段(app 不再发布端口、
frontend 与 adapter 的入口收敛到闸门、指向真实网络 weknora-test-v071-network)、
执行器启动脚本与治理 PG 反向隧道脚本(默认 unix socket,不占 TCP)。

验证:deploy/cloud-test/test_gate.py 13 项全 PASS(含 12 秒静默流、半关闭、2MiB 完整、
排空 exit 0/active_at_close=1、排空超时 exit 2、上游不可达不挂住);go test 相关包全绿;
scripts/ci_checks.py 16 项全部 success(EXIT=0,1241 用例通过/0 失败/191 已声明可选集成跳过);
云测试只读预检 17 项 / 6 FAIL,FAIL 项即当前部署缺口(无闸门、app 仍发布端口、
adapter 与 frontend 仍直连 app、云端归档根未建),未部署、未改云端任何状态。
2026-10-30 18:38:38 +08:00

88 lines
3.5 KiB
Go

package main
import (
"context"
"strings"
"testing"
"knowledge-governance/internal/appconfig"
"knowledge-governance/internal/jobs"
postgresstore "knowledge-governance/internal/platform/postgres"
"knowledge-governance/internal/versionhistory"
)
// 发布执行器必须只挂四类 release 作业:多挂一类就会抢走本机队列里别的执行者的作业。
func TestRegisterLibraryReleaseHandlersCoversOnlyReleaseJobs(t *testing.T) {
handlers := map[string]jobs.Handler{}
registerLibraryReleaseHandlers(handlers, nil, appconfig.Config{}, nil)
want := []string{librarySyncJobType, libraryRestoreJobType, libraryRecoverJobType, libraryBaselineJobType}
if len(handlers) != len(want) {
t.Fatalf("registered %d handlers, want %d: %#v", len(handlers), len(want), handlers)
}
for _, jobType := range want {
if handlers[jobType] == nil {
t.Fatalf("handler %q is not registered", jobType)
}
}
}
// 准入未通过时 handler 必须"明确失败且不做任何动作":不能靠 nil store 或进程内状态蒙过去。
func TestRegisterLibraryReleaseHandlersFailsClosedWhenNotAdmitted(t *testing.T) {
handlers := map[string]jobs.Handler{}
registerLibraryReleaseHandlers(handlers, nil, appconfig.Config{}, errNotAdmitted{})
for _, jobType := range []string{librarySyncJobType, libraryRestoreJobType, libraryRecoverJobType, libraryBaselineJobType} {
err := handlers[jobType](context.Background(), jobs.Job{})
if err == nil || !strings.Contains(err.Error(), "准入未通过") {
t.Fatalf("handler %q error = %v, want admission refusal", jobType, err)
}
}
}
type errNotAdmitted struct{}
func (errNotAdmitted) Error() string { return "fixture: target instance unknown" }
// capture 角色不能注册任何 source 之外的队列;release-executor 反之。
func TestWorkerHandlersFollowWorkerRole(t *testing.T) {
release := appconfig.Config{
LibraryWorkerRole: appconfig.LibraryWorkerRoleReleaseExecutor,
LibraryAgentSocket: "/run/library-agent/agent.sock",
TestWeKnoraEnabled: true,
TestWeKnoraAPIKeyFile: "/nonexistent/api-key",
PPScanEnabled: true,
VersionHistoryGiteaBaseURL: "https://gitea.invalid/Weknora/knowledge-governance",
VersionHistoryAllowedOrigins: []string{"https://gitea.invalid"},
}
handlers, err := workerHandlers(postgresstore.NewStore(nil), release)
if err != nil {
t.Fatalf("release-executor workerHandlers() error = %v", err)
}
for _, forbidden := range []string{libraryCaptureJobType, "test.upload", "test.poll", "test.validate", "test.remove", "source.scan.pp", "source.catalog.refresh", "artifact.fetch", versionhistory.JobType} {
if handlers[forbidden] != nil {
t.Fatalf("release-executor registered %q", forbidden)
}
}
if handlers["demo"] == nil {
t.Fatal("demo handler must stay registered")
}
capture := appconfig.Config{
LibraryWorkerRole: appconfig.LibraryWorkerRoleCapture,
LibraryAgentSocket: "/run/library-agent/agent.sock",
TestWeKnoraEnabled: true,
TestWeKnoraAPIKeyFile: "/nonexistent/api-key",
}
if _, err := workerHandlers(nil, capture); err == nil || !strings.Contains(err.Error(), "test WeKnora API key") {
t.Fatalf("capture role must reach the test WeKnora wiring, got %v", err)
}
capture.TestWeKnoraEnabled = false
capture.PPScanEnabled = false
handlers, err = workerHandlers(postgresstore.NewStore(nil), capture)
if err != nil {
t.Fatalf("capture workerHandlers() error = %v", err)
}
if handlers[libraryCaptureJobType] == nil {
t.Fatal("capture role must register library.capture")
}
}