- P0-5 护栏不再是空壳:挂 WithSensitiveInfo(阻断) + 长度上限(输入 262144 / 输出 65536 字符,依据=
组装完成的请求体量级与「不误伤正常对话」) + 关键词**仅在配置时**挂;正常对话有用例证明放行
- P0-11 上下文压缩默认启用:CONTEXT_MAX_TOKENS 默认 32768(=loop.DefaultMaxContextTokens,两常量由测试锁死),
0/负 = 关闭(行为退回接线前);长对话真压缩且摘要消息进请求首位、最近一轮保留
- P0-10 身份说明书(owner.yaml identityFile)注入人格上下文,并且**收窄为仅主人会话**:
· 新增最小钩子 loop.Options.PersonaKnowledgeFor(env):**按该轮会话环境**解析(不看提示词、不看会话 id),
块只进每轮视图、**不进 messages**(不进 checkpoint、不进记忆提取面)
· 开关 OWNER_IDENTITY_INJECTION:owner_only(默认) / always(仅自测) / off;非法值与读不动 → WARN + 回退默认(不静默)
· **顺手关掉第二条同型泄漏**:channelbridge 群聊参与度判定器的 PersonaPrompt 此前带身份正文 ⇒ 现改为纯人格指令
· 回归:缺文件/目录/未配置逐字节等于旧输出;静态指令与角色池 Instructions() 全不含正文;
文件里点名的陌生账号仍 IsOwner==false(身份知识绝不参与权限判定)
- 复核:build/vet 绿 + cmd/config/loop 包测试绿;四类断言(主人含 / **群聊与陌生人不含** / always·off 两档 /
回归不变)+ 4 条变异反证(含把判定改成恒真 → 提示词里真的出现身份说明书)均已还原
- 未做(登记):knowledge 块启动时读一次(改文件需重启);`.env.example` 的 OWNER_IDENTITY_INJECTION 行随后补;
`internal/user/owner.go` 注释与 design/01 §2.2、design/11 §0 待同步
126 lines
4.6 KiB
Go
126 lines
4.6 KiB
Go
package config
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// 本文件锁定 OWNER_IDENTITY_INJECTION 的读取与判定(design/11 §0 收窄后的
|
||
// 开关):默认档是 owner_only,取值优先级与其余配置一致(进程环境 > .env >
|
||
// 默认),非法取值**必须报错**,由调用方明确回退并记日志。
|
||
//
|
||
// 读环境/写 .env 的用例按本包既有约定**不并行**(unsetEnvForTest 会改动
|
||
// 进程环境,与并行读取互相干扰)。
|
||
|
||
// writeDotEnv 把给定行写进一个临时 .env 并返回路径。
|
||
func writeDotEnv(t *testing.T, lines ...string) string {
|
||
t.Helper()
|
||
path := filepath.Join(t.TempDir(), ".env")
|
||
if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), 0o600); err != nil {
|
||
t.Fatalf("write .env: %v", err)
|
||
}
|
||
return path
|
||
}
|
||
|
||
// TestLoadOwnerIdentityInjection_DefaultsToOwnerOnly 钉住默认档:没配(进程
|
||
// 环境与 .env 都没有)时是 owner_only,也就是最安全的一侧。
|
||
func TestLoadOwnerIdentityInjection_DefaultsToOwnerOnly(t *testing.T) {
|
||
unsetEnvForTest(t, OwnerIdentityInjectionEnv)
|
||
|
||
got, err := LoadOwnerIdentityInjection(filepath.Join(t.TempDir(), "absent.env"))
|
||
if err != nil {
|
||
t.Fatalf("LoadOwnerIdentityInjection: %v", err)
|
||
}
|
||
if got != OwnerIdentityOwnerOnly {
|
||
t.Fatalf("默认档 = %q, want %q", got, OwnerIdentityOwnerOnly)
|
||
}
|
||
|
||
mode, perr := ParseOwnerIdentityInjection(got)
|
||
if perr != nil || mode != OwnerIdentityOwnerOnly {
|
||
t.Fatalf("Parse(%q) = (%q, %v), want (%q, nil)", got, mode, perr, OwnerIdentityOwnerOnly)
|
||
}
|
||
}
|
||
|
||
// TestLoadOwnerIdentityInjection_DotEnvApplies 证明只写在 .env 里的声明真实
|
||
// 生效 —— 这正是不能直接用 os.Getenv 的原因(.env 不写回进程环境,直接读
|
||
// 会静默忽略用户写在 .env 里的开关)。
|
||
func TestLoadOwnerIdentityInjection_DotEnvApplies(t *testing.T) {
|
||
unsetEnvForTest(t, OwnerIdentityInjectionEnv)
|
||
path := writeDotEnv(t, "# 注释行", OwnerIdentityInjectionEnv+"=always")
|
||
|
||
got, err := LoadOwnerIdentityInjection(path)
|
||
if err != nil {
|
||
t.Fatalf("LoadOwnerIdentityInjection: %v", err)
|
||
}
|
||
if got != OwnerIdentityAlways {
|
||
t.Fatalf(".env 取值 = %q, want %q", got, OwnerIdentityAlways)
|
||
}
|
||
}
|
||
|
||
// TestLoadOwnerIdentityInjection_ProcessEnvWins 证明优先级:进程环境压过 .env
|
||
// (与 internal/config 其余配置同一口径)。
|
||
func TestLoadOwnerIdentityInjection_ProcessEnvWins(t *testing.T) {
|
||
unsetEnvForTest(t, OwnerIdentityInjectionEnv)
|
||
t.Setenv(OwnerIdentityInjectionEnv, OwnerIdentityOff)
|
||
path := writeDotEnv(t, OwnerIdentityInjectionEnv+"=always")
|
||
|
||
got, err := LoadOwnerIdentityInjection(path)
|
||
if err != nil {
|
||
t.Fatalf("LoadOwnerIdentityInjection: %v", err)
|
||
}
|
||
if got != OwnerIdentityOff {
|
||
t.Fatalf("进程环境取值 = %q, want %q(进程环境优先于 .env)", got, OwnerIdentityOff)
|
||
}
|
||
}
|
||
|
||
// TestLoadOwnerIdentityInjection_ReadErrorIsExplicit 钉住「读不动就显式报错」:
|
||
// 一个不可读的 .env(这里是目录路径)返回错误,绝不悄悄变成默认值 ——
|
||
// 调用方据此记日志并明确回退。
|
||
func TestLoadOwnerIdentityInjection_ReadErrorIsExplicit(t *testing.T) {
|
||
unsetEnvForTest(t, OwnerIdentityInjectionEnv)
|
||
|
||
if _, err := LoadOwnerIdentityInjection(t.TempDir()); err == nil {
|
||
t.Fatal("把目录当 .env 读本应报错,却返回 nil")
|
||
}
|
||
}
|
||
|
||
// TestParseOwnerIdentityInjection_ThreeModesAndIllegal 钉住三档取值与非法值:
|
||
// 大小写/空白容忍,空串按默认,其余取值返回**带原始取值的错误**。
|
||
func TestParseOwnerIdentityInjection_ThreeModesAndIllegal(t *testing.T) {
|
||
t.Parallel()
|
||
for _, tc := range []struct {
|
||
raw string
|
||
want string
|
||
}{
|
||
{raw: "", want: OwnerIdentityOwnerOnly},
|
||
{raw: OwnerIdentityOwnerOnly, want: OwnerIdentityOwnerOnly},
|
||
{raw: " OWNER_ONLY ", want: OwnerIdentityOwnerOnly},
|
||
{raw: "Always", want: OwnerIdentityAlways},
|
||
{raw: "off", want: OwnerIdentityOff},
|
||
{raw: " OFF", want: OwnerIdentityOff},
|
||
} {
|
||
got, err := ParseOwnerIdentityInjection(tc.raw)
|
||
if err != nil {
|
||
t.Fatalf("Parse(%q) 报错: %v", tc.raw, err)
|
||
}
|
||
if got != tc.want {
|
||
t.Fatalf("Parse(%q) = %q, want %q", tc.raw, got, tc.want)
|
||
}
|
||
}
|
||
|
||
for _, raw := range []string{"everyone", "true", "1", "owner-only", "允许"} {
|
||
mode, err := ParseOwnerIdentityInjection(raw)
|
||
if err == nil {
|
||
t.Fatalf("Parse(%q) 本应报错(非法取值不得静默生效),却返回 %q", raw, mode)
|
||
}
|
||
if !strings.Contains(err.Error(), raw) {
|
||
t.Fatalf("错误信息里没有原始取值 %q: %v", raw, err)
|
||
}
|
||
if mode != OwnerIdentityOwnerOnly {
|
||
t.Fatalf("Parse(%q) 的回退档 = %q, want %q(回退方向必须是最安全一侧)", raw, mode, OwnerIdentityOwnerOnly)
|
||
}
|
||
}
|
||
}
|