package presetcoding import ( "strings" "testing" "git.flytoex.net/yuanwei/flyto-agent/pkg/promptkit" ) // TestEnglishBundle_BaseSectionsCount enforces the 6 always-on count // (mirror cc Layer 1). Adding a 7th would violate ADR-0005 § 2.2 which // caps base at 6 (down from 8 in the legacy bundle, fixing Bug F). // // TestEnglishBundle_BaseSectionsCount 强制 6 段 always-on (mirror cc // Layer 1). 加第 7 段会违反 ADR-0005 § 2.2 (从旧 8 段砍到 6 段, 修 Bug F). func TestEnglishBundle_BaseSectionsCount(t *testing.T) { b := NewEnglishBundle() got := b.BaseSections() if len(got) != 6 { t.Fatalf("BaseSections count = %d, want 6 (ADR-0005 § 2.2)", len(got)) } wantNames := []string{"intro", "system", "doing_tasks", "actions", "tone_and_style", "output_efficiency"} for i, sec := range got { if sec.Name() != wantNames[i] { t.Errorf("BaseSections[%d].Name() = %q, want %q (order locked for cache prefix stability)", i, sec.Name(), wantNames[i]) } } } // TestEnglishBundle_BaseSectionsRenderable verifies each base section // renders non-empty content. A regression here means a section's content // constant got accidentally cleared. // // TestEnglishBundle_BaseSectionsRenderable 验证每个 base 段渲染非空. 此处 // 回归说明 section 内容常量被意外清空. func TestEnglishBundle_BaseSectionsRenderable(t *testing.T) { b := NewEnglishBundle() ctx := promptkit.BuildContext{Cwd: "/tmp"} for _, sec := range b.BaseSections() { text, err := sec.Render(ctx) if err != nil { t.Errorf("Render %q error: %v", sec.Name(), err) continue } if text == "" { t.Errorf("Render %q returned empty (base sections are always-on)", sec.Name()) } } } // TestEnglishBundle_GitProtocolGated verifies sectionGitProtocol fires only // when HasGitRepo=true. Fixes ADR-0005 Bug F (git protocol unconditional // injection in legacy bundle). // // TestEnglishBundle_GitProtocolGated 验证 sectionGitProtocol 仅 HasGitRepo // =true 时触发. 修 ADR-0005 Bug F (旧 bundle git 协议无条件注入). func TestEnglishBundle_GitProtocolGated(t *testing.T) { b := NewEnglishBundle() var gitSec promptkit.Section for _, sec := range b.ConditionalSections(promptkit.BuildContext{}) { if sec.Name() == "git_protocol" { gitSec = sec break } } if gitSec == nil { t.Fatal("git_protocol section not found in ConditionalSections") } // HasGitRepo=false → empty got, err := gitSec.Render(promptkit.Context{HasGitRepo: false}) if err != nil { t.Fatalf("Render error: %v", err) } if got != "" { t.Errorf("git_protocol with HasGitRepo=false should be empty, got: %s", got) } // HasGitRepo=true → emits sectionGitProtocol got, err = gitSec.Render(promptkit.Context{HasGitRepo: true}) if err != nil { t.Fatalf("Render error: %v", err) } if !strings.Contains(got, "Git safety protocol") { t.Errorf("git_protocol with HasGitRepo=true should emit protocol, got: %s", got) } } // TestEnglishBundle_FlytoMdEmptyWhenNoFile verifies flyto_md returns empty // when no FLYTO.md found. Fixes ADR-0005 Bug E + I (override path no longer // silently injects FLYTO.md or forks git rev-parse — handled by Section gating). // // TestEnglishBundle_FlytoMdEmptyWhenNoFile 验证 flyto_md 没找到 FLYTO.md // 时返回空. 修 ADR-0005 Bug E + I (override 路径不再静默注 FLYTO.md 或 fork // git rev-parse — 通过 Section 控制实现). func TestEnglishBundle_FlytoMdEmptyWhenNoFile(t *testing.T) { b := NewEnglishBundle() var flytoSec promptkit.Section for _, sec := range b.ConditionalSections(promptkit.BuildContext{}) { if sec.Name() == "flyto_md" { flytoSec = sec break } } if flytoSec == nil { t.Fatal("flyto_md section not found") } // Cwd="" → empty (no search performed). // Cwd 为空 → 空 (不进行搜索). got, err := flytoSec.Render(promptkit.Context{Cwd: ""}) if err != nil { t.Fatalf("Render error: %v", err) } if got != "" { t.Errorf("flyto_md with Cwd='' should be empty, got: %s", got) } // Cwd=/dev/null (a path with no FLYTO.md possible) → empty. // Cwd=/dev/null (不可能有 FLYTO.md 的路径) → 空. got, err = flytoSec.Render(promptkit.Context{Cwd: "/nonexistent-path-xyz-9876"}) if err != nil { t.Fatalf("Render error: %v", err) } if got != "" { t.Errorf("flyto_md with no FLYTO.md should be empty, got: %s", got) } } // TestEnglishBundle_EnvInfoIncludesCwd verifies env_info always emits Cwd // when Cwd is non-empty. env_info is always-on for the coding preset. // // TestEnglishBundle_EnvInfoIncludesCwd 验证 env_info 在 Cwd 非空时总发 Cwd. // env_info 对编程 preset 是 always-on. func TestEnglishBundle_EnvInfoIncludesCwd(t *testing.T) { b := NewEnglishBundle() var envSec promptkit.Section for _, sec := range b.ConditionalSections(promptkit.BuildContext{}) { if sec.Name() == "env_info" { envSec = sec break } } if envSec == nil { t.Fatal("env_info section not found") } got, err := envSec.Render(promptkit.Context{Cwd: "/tmp/test-cwd-9876"}) if err != nil { t.Fatalf("Render error: %v", err) } if !strings.Contains(got, "/tmp/test-cwd-9876") { t.Errorf("env_info should include Cwd, got: %s", got) } if !strings.Contains(got, "# Environment") { t.Errorf("env_info should start with Environment heading, got: %s", got) } } // TestEnglishBundle_BuildIntegration runs promptkit.Build end-to-end and // verifies the cache boundary lands after base sections (before conditional). // This is the critical invariant for prompt cache stability. // // TestEnglishBundle_BuildIntegration 端到端跑 promptkit.Build, 验证 cache // 边界落在 base 段之后 (conditional 段之前). 这是 prompt cache 稳定性的 // 关键不变量. func TestEnglishBundle_BuildIntegration(t *testing.T) { b := NewEnglishBundle() ctx := promptkit.BuildContext{Cwd: "/tmp/test-bundle-build", HasGitRepo: false} out, boundary, err := promptkit.Build(b, ctx) if err != nil { t.Fatalf("Build error: %v", err) } if out == "" { t.Fatal("Build returned empty prompt") } if boundary == 0 { t.Fatal("cache boundary should be non-zero after base sections") } prefix := out[:boundary] // Every base section should be in the cache prefix. // 每个 base 段应在 cache 前缀里. for _, baseName := range []string{"interactive agent", "# System", "# Doing tasks", "# Executing actions", "# Tone and style", "# Output efficiency"} { if !strings.Contains(prefix, baseName) { t.Errorf("cache prefix missing base content %q", baseName) } } // No conditional content should leak into cache prefix. // conditional 内容不应漏进 cache 前缀. for _, condName := range []string{"# Using your tools", "# Searching and reading code", "# Environment"} { if strings.Contains(prefix, condName) { t.Errorf("cache prefix leaked conditional content %q", condName) } } } // TestEnglishBundle_ReminderTriggersEmptyAtC4 enforces that ReminderTriggers // is empty until C7 wires the reminder system. If a future commit adds // triggers without updating this test (and ADR-0005 § 2.2), it's a smell // that reminder rewiring snuck in without a planned commit. // // TestEnglishBundle_ReminderTriggersEmptyAtC4 强制 ReminderTriggers 在 C7 // reminder 系统重接前为空. 未来 commit 加 trigger 不更新此测试 (及 ADR-0005 // § 2.2) 说明 reminder 重接偷溜进来没有 planned commit. func TestEnglishBundle_ReminderTriggersEmptyAtC4(t *testing.T) { b := NewEnglishBundle() if got := b.ReminderTriggers(); len(got) != 0 { t.Errorf("ReminderTriggers should be empty until C7 wires reminder system; got %d triggers", len(got)) } }