按 design/22 §2/§3/§4.4/§7(3/4/5)。 - ①「消息级角色归属」进入模型上下文(批次 1 只做存储+DTO):历史消息的发送角色现在对模型可见,且**不把系统元数据塞进用户正文**(design/11 §10 红线) - ② IM owner 私聊镜像解析出的角色 = **主会话绑定的角色**(§7-3) - ③ 渠道级绑定:ChannelAccount 加 personaId(channels.json 读写/校验/脱敏沿用既有约定)+ 角色解析优先级 **会话 > 渠道 > 默认** + **群聊/陌生人强制默认角色**(§7-4);admin 渠道配置端点支持 personaId - Docs/api-client 版本 +1 - 验收:go build/vet/test 全绿 + linux/darwin 交叉编译
94 lines
3.3 KiB
Go
94 lines
3.3 KiB
Go
package runtimeconfig
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/config"
|
|
)
|
|
|
|
// This file covers the channel-level role binding of design/22 §2 方案 b: the
|
|
// account's personaId is part of channels.json, so it must survive a save/load
|
|
// round trip, be omitted when unset (an unbound account is not written as
|
|
// "personaId": ""), and never be treated as a secret.
|
|
|
|
// TestChannelsPersonaIDRoundTrip proves the personaId is persisted verbatim and
|
|
// read back on the account it belongs to.
|
|
func TestChannelsPersonaIDRoundTrip(t *testing.T) {
|
|
t.Parallel()
|
|
path := filepath.Join(t.TempDir(), "channels.json")
|
|
accts := map[string][]config.ChannelAccount{
|
|
"telegram": {{ID: "a", PersonaID: "yuki", Config: map[string]any{"bot_token": "tk"}}},
|
|
}
|
|
if err := SaveChannels(path, accts); err != nil {
|
|
t.Fatalf("save: %v", err)
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"personaId": "yuki"`) {
|
|
t.Fatalf("channels.json missing the bound role: %s", raw)
|
|
}
|
|
got, warn := LoadChannels(path, nil)
|
|
if warn != nil {
|
|
t.Fatalf("load warn = %v", warn)
|
|
}
|
|
if len(got["telegram"]) != 1 || got["telegram"][0].PersonaID != "yuki" {
|
|
t.Fatalf("round trip personaId = %+v, want yuki", got["telegram"])
|
|
}
|
|
// A channel role is not a secret: masking the credentials must not hide it.
|
|
if masked := Masked(got["telegram"][0]); masked.PersonaID != "yuki" {
|
|
t.Fatalf("masked personaId = %q, want yuki", masked.PersonaID)
|
|
}
|
|
}
|
|
|
|
// TestChannelsPersonaIDOmittedWhenUnbound locks the omission semantics: an account
|
|
// with no channel binding writes no personaId field at all, so an existing account
|
|
// is not silently bound to an empty-named role.
|
|
func TestChannelsPersonaIDOmittedWhenUnbound(t *testing.T) {
|
|
t.Parallel()
|
|
path := filepath.Join(t.TempDir(), "channels.json")
|
|
if err := SaveChannels(path, map[string][]config.ChannelAccount{
|
|
"telegram": {{ID: "a", Config: map[string]any{"bot_token": "tk"}}},
|
|
}); err != nil {
|
|
t.Fatalf("save: %v", err)
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read: %v", err)
|
|
}
|
|
if strings.Contains(string(raw), "personaId") {
|
|
t.Fatalf("unbound account wrote a personaId: %s", raw)
|
|
}
|
|
got, _ := LoadChannels(path, nil)
|
|
if len(got["telegram"]) != 1 || got["telegram"][0].PersonaID != "" {
|
|
t.Fatalf("loaded personaId = %q, want empty", got["telegram"][0].PersonaID)
|
|
}
|
|
}
|
|
|
|
// TestUpsertChannelPersonaID pins the save semantics the panel relies on: an Upsert
|
|
// carries the whole account, so binding a role stores it, and a later save without
|
|
// a personaId clears the binding instead of leaving a stale one behind.
|
|
func TestUpsertChannelPersonaID(t *testing.T) {
|
|
t.Parallel()
|
|
accounts := map[string][]config.ChannelAccount{
|
|
"telegram": {{ID: "a", Config: map[string]any{"bot_token": "tk"}}},
|
|
}
|
|
bound := UpsertChannel(accounts, "telegram", config.ChannelAccount{
|
|
ID: "a", PersonaID: "yuki", Config: map[string]any{"bot_token": "tk"},
|
|
})
|
|
if bound["telegram"][0].PersonaID != "yuki" {
|
|
t.Fatalf("upsert personaId = %q, want yuki", bound["telegram"][0].PersonaID)
|
|
}
|
|
|
|
cleared := UpsertChannel(bound, "telegram", config.ChannelAccount{
|
|
ID: "a", Config: map[string]any{"bot_token": "tk"},
|
|
})
|
|
if cleared["telegram"][0].PersonaID != "" {
|
|
t.Fatalf("personaId after unbinding save = %q, want empty", cleared["telegram"][0].PersonaID)
|
|
}
|
|
}
|