Files
AskaEth da3a386daf
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(cyrene,config,loop): 护栏/压缩/身份知识三条落地(P0-5/P0-10/P0-11)+ 身份注入收窄到主人会话
- 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 待同步
2026-10-04 14:30:26 +08:00

126 lines
4.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}
}