- internal/bootstrap.EnsureInstance on the serve path: normalized writable roots, symlink-aware validation, data-root vs vault containment rejection, filesystem-root refusal - instance-marker.json in data root: first boot creates; foreign instance ID fails closed (explicit adopt/migrate required, KTD18) - instance.lock non-blocking flock detects duplicate starts locally without any governance tooling; ownership held until process exit - Config.InstanceID (INSTANCE_ID env/file key; default 'default') joins the instance-settings bootstrap denylist (identity cannot be self-overwritten) - auth is Bearer+localStorage per origin (no cookies) - browser isolation holds across ports; documented in the unit template - deploy/systemd/agent-system@.service template (instance %i: env file, DATA_DIR, hardened service scope) - tests: marker create/foreign reject, vault overlap, invalid IDs, root refusal, in-process duplicate-start detection
89 lines
2.8 KiB
Go
89 lines
2.8 KiB
Go
package bootstrap
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"chat-agent-system/internal/config"
|
|
)
|
|
|
|
func testCfg(t *testing.T, id, dataDir, vault string) *config.Config {
|
|
t.Helper()
|
|
return &config.Config{InstanceID: id, DataDir: dataDir, VaultStateRoot: vault}
|
|
}
|
|
|
|
func TestEnsureInstanceCreatesMarkerAndLock(t *testing.T) {
|
|
dir := t.TempDir()
|
|
cfg := testCfg(t, "alpha", filepath.Join(dir, "alpha"), "")
|
|
own, err := EnsureInstance(cfg)
|
|
if err != nil {
|
|
t.Fatalf("ensure: %v", err)
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(dir, "alpha", "instance-marker.json"))
|
|
if err != nil {
|
|
t.Fatalf("marker missing: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), "\"instance_id\": \"alpha\"") {
|
|
t.Fatalf("marker content: %s", raw)
|
|
}
|
|
// 二次取得同实例:同进程内 flock 可重入(同 fd 语义),但模拟误重复
|
|
// 启动需要另一个进程;此处验证所有权对象可用并释放。
|
|
own.Release()
|
|
}
|
|
|
|
func TestEnsureInstanceRejectsForeignMarker(t *testing.T) {
|
|
dir := t.TempDir()
|
|
alpha := filepath.Join(dir, "alpha")
|
|
if _, err := EnsureInstance(testCfg(t, "alpha", alpha, "")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 复用同一 data dir 但自称另一实例:必须拒绝(KTD18 不自动认领)
|
|
if _, err := EnsureInstance(testCfg(t, "beta", alpha, "")); err == nil {
|
|
t.Fatal("foreign instance marker must be rejected")
|
|
}
|
|
}
|
|
|
|
func TestEnsureInstanceRejectsVaultOverlap(t *testing.T) {
|
|
dir := t.TempDir()
|
|
data := filepath.Join(dir, "data")
|
|
vault := filepath.Join(data, "vault") // vault 在 data 内:非独立
|
|
if _, err := EnsureInstance(testCfg(t, "gamma", data, vault)); err == nil {
|
|
t.Fatal("vault inside data dir must be rejected")
|
|
}
|
|
vaultOut := filepath.Join(dir, "vault")
|
|
if _, err := EnsureInstance(testCfg(t, "gamma", data, vaultOut)); err != nil {
|
|
t.Fatalf("independent vault should pass: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestEnsureInstanceRejectsInvalidID(t *testing.T) {
|
|
dir := t.TempDir()
|
|
for _, bad := range []string{"", "a/b", "..", "a b", "."} {
|
|
if _, err := EnsureInstance(testCfg(t, bad, filepath.Join(dir, "x"), "")); err == nil {
|
|
t.Fatalf("invalid instance id %q must be rejected", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEnsureInstanceRejectsRootAsDataDir(t *testing.T) {
|
|
if _, err := EnsureInstance(testCfg(t, "root-try", "/", "")); err == nil {
|
|
t.Fatal("filesystem root as data dir must be rejected")
|
|
}
|
|
}
|
|
|
|
func TestEnsureInstanceDetectsDuplicateStart(t *testing.T) {
|
|
dir := t.TempDir()
|
|
data := filepath.Join(dir, "dup")
|
|
own, err := EnsureInstance(testCfg(t, "dup", data, ""))
|
|
if err != nil {
|
|
t.Fatalf("first acquire: %v", err)
|
|
}
|
|
defer own.Release()
|
|
// 同机第二次取得(独立 open file description):必须拒绝误重复启动
|
|
if _, err := EnsureInstance(testCfg(t, "dup", data, "")); err == nil {
|
|
t.Fatal("duplicate start must be detected and rejected")
|
|
}
|
|
}
|