Merge pull request #49 from Autumn-27/fix/prompt-var-duplicate-now
fix(prompt-vars): 消除变量目录与全局变量撞名导致的前端 key 重复
This commit is contained in:
3 files changed
+78
-3
No files matched your search
@@ -117,7 +117,8 @@ func intp(v int) *int { return &v }
|
||||
var builtinAgents = []builtinAgent{
|
||||
{"goals", "目标拆解", "goals", "把渗透任务目标拆解成若干独立、可验证的子目标。", []promptVar{
|
||||
{"EngagementDescription", "任务描述(测试对象/背景)", "测试 example.com 站点", "exploration"},
|
||||
{"Now", "服务端当前时间(RFC3339)", "2026-06-25T00:00:00Z", "runtime"},
|
||||
// Now 是全局 runtime 变量(见 server.globalPromptVars),不再在各 agent 目录里
|
||||
// 重复定义,否则 withGlobalVars 追加时会与全局项撞名。
|
||||
}, false, nil},
|
||||
{"planner", "规划", "planner", "读取态势、判定目标,只在确有未覆盖的新方向时补充探索意图(每任务一个规划循环)。", []promptVar{
|
||||
{"Goal", "任务总目标", "拿下 example.com 的管理员权限", "exploration"},
|
||||
@@ -163,7 +164,9 @@ ON CONFLICT (agent_id, var_name) DO UPDATE
|
||||
}
|
||||
// Drop catalog entries for variables that were renamed, so the white-list no
|
||||
// longer advertises a name templates can't resolve (EngagementTitle→Description).
|
||||
if _, err := d.Exec(`DELETE FROM agent_prompt_vars WHERE var_name IN ('EngagementTitle', 'CoverageGaps')`); err != nil {
|
||||
// 'Now' 从各 agent 目录提升为全局 runtime 变量后,旧库里 goals 仍残留一条 'Now'
|
||||
// 会与全局项撞名(前端变量列表 key 重复);一并清掉。
|
||||
if _, err := d.Exec(`DELETE FROM agent_prompt_vars WHERE var_name IN ('EngagementTitle', 'CoverageGaps', 'Now')`); err != nil {
|
||||
return fmt.Errorf("cleanup renamed vars: %w", err)
|
||||
}
|
||||
// Default-on interactive_shell for the runtime agents (planner/worker/mainagent/auto)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/Autumn-27/artex/db"
|
||||
)
|
||||
|
||||
// A stored catalog entry that collides with a global runtime var (e.g. a legacy
|
||||
// seeded goals."Now") must not produce a duplicate: the global wins and the name
|
||||
// appears exactly once, so the UI never sees duplicate React keys.
|
||||
func TestWithGlobalVarsDedupesCollidingNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
stored := []db.PromptVar{
|
||||
{Name: "EngagementDescription", Description: "task", Source: "exploration"},
|
||||
{Name: "Now", Description: "stale per-agent copy", Source: "runtime"},
|
||||
}
|
||||
out := withGlobalVars(stored)
|
||||
|
||||
counts := map[string]int{}
|
||||
var now db.PromptVar
|
||||
for _, v := range out {
|
||||
counts[v.Name]++
|
||||
if v.Name == "Now" {
|
||||
now = v
|
||||
}
|
||||
}
|
||||
if counts["Now"] != 1 {
|
||||
t.Fatalf("Now appeared %d times, want 1: %+v", counts["Now"], out)
|
||||
}
|
||||
if counts["EngagementDescription"] != 1 {
|
||||
t.Fatalf("non-colliding stored var was dropped or duplicated: %+v", out)
|
||||
}
|
||||
// The surviving Now must be the authoritative global definition, not the stale
|
||||
// stored one.
|
||||
if now.Description == "stale per-agent copy" {
|
||||
t.Fatalf("stored var shadowed the global instead of the other way around: %+v", now)
|
||||
}
|
||||
// Every global is present exactly once.
|
||||
for _, g := range globalPromptVars {
|
||||
if counts[g.Name] != 1 {
|
||||
t.Fatalf("global %q present %d times, want 1", g.Name, counts[g.Name])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The common case (no collisions) keeps every stored var and appends all globals.
|
||||
func TestWithGlobalVarsKeepsDistinctVars(t *testing.T) {
|
||||
t.Parallel()
|
||||
stored := []db.PromptVar{{Name: "Goal", Source: "exploration"}}
|
||||
out := withGlobalVars(stored)
|
||||
if len(out) != len(stored)+len(globalPromptVars) {
|
||||
t.Fatalf("len=%d, want %d: %+v", len(out), len(stored)+len(globalPromptVars), out)
|
||||
}
|
||||
if out[0].Name != "Goal" {
|
||||
t.Fatalf("stored var order not preserved: %+v", out)
|
||||
}
|
||||
}
|
||||
+15
-1
@@ -2008,8 +2008,22 @@ var globalPromptVars = []db.PromptVar{
|
||||
|
||||
// withGlobalVars appends the universal runtime vars onto an agent's own catalog,
|
||||
// so validation / the UI variable list / preview all recognize {{.Now}} etc.
|
||||
// A stored catalog entry that collides with a global name is dropped: the global
|
||||
// runtime var is authoritative (it's what the render path actually resolves), and
|
||||
// this keeps the returned list name-unique so the UI never sees duplicate keys.
|
||||
func withGlobalVars(vars []db.PromptVar) []db.PromptVar {
|
||||
return append(append([]db.PromptVar{}, vars...), globalPromptVars...)
|
||||
globalNames := make(map[string]bool, len(globalPromptVars))
|
||||
for _, g := range globalPromptVars {
|
||||
globalNames[g.Name] = true
|
||||
}
|
||||
out := make([]db.PromptVar, 0, len(vars)+len(globalPromptVars))
|
||||
for _, v := range vars {
|
||||
if globalNames[v.Name] {
|
||||
continue // shadowed by the authoritative global runtime var
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return append(out, globalPromptVars...)
|
||||
}
|
||||
|
||||
// validateTemplate parses the template and rejects any {{.Var}} not in the catalog.
|
||||
|
||||
Reference in new issue
Block a user