a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
273 lines
9.0 KiB
Go
273 lines
9.0 KiB
Go
package profiles
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Per-profile instruction-body byte ceilings. The body is re-read into
|
|
// the model on every API call of every session (via the @-include), so
|
|
// a ballooning body is a per-turn tax — blowing a ceiling fails loudly
|
|
// instead of silently re-inflating the ambient prefix.
|
|
//
|
|
// core inherits the pre-profiles global-section ceiling (6.5 KiB); full
|
|
// gets slack for its longer surface description only; localization is
|
|
// the diet profile and must stay under 2 KiB (~0.45k tokens).
|
|
var bodyByteCeilings = map[string]int{
|
|
"core": 6656,
|
|
"full": 6912,
|
|
"localization": 2304, // explore opener + row joined at the explore-branch consolidation
|
|
}
|
|
|
|
func TestProfileBodyByteCeilings(t *testing.T) {
|
|
for _, p := range Table() {
|
|
ceiling, ok := bodyByteCeilings[p.Name]
|
|
if !ok {
|
|
t.Errorf("profile %q has no body byte ceiling — add one to bodyByteCeilings", p.Name)
|
|
continue
|
|
}
|
|
got := len(p.Body())
|
|
t.Logf("profile %-13s body bytes=%d (ceiling %d)", p.Name, got, ceiling)
|
|
if got > ceiling {
|
|
t.Errorf("profile %q body is %d bytes, over the %d ceiling", p.Name, got, ceiling)
|
|
}
|
|
if got == 0 {
|
|
t.Errorf("profile %q renders an empty body", p.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// positioningCues are the load-bearing fragments EVERY profile must
|
|
// keep, lean ones included: the mandatory-rule sentinel, the deny-hook
|
|
// warning, the one-shot opener, the memory triggers, the discovery
|
|
// path, the CLI fallback, and the switch-back line. Trimming any of
|
|
// these is what costs tool adoption — the diet only ever removes
|
|
// elaboration around them.
|
|
var positioningCues = []string{
|
|
"## MANDATORY: Use Gortex MCP tools", // idempotency sentinel + the rule itself
|
|
"MUST prefer graph queries",
|
|
"deny",
|
|
"smart_context",
|
|
"distill_session",
|
|
"surface_memories",
|
|
"save_note",
|
|
"store_memory",
|
|
"tools_search",
|
|
"gortex://guide",
|
|
"gortex call",
|
|
"gortex instructions switch",
|
|
"NEW sessions only",
|
|
}
|
|
|
|
func TestEveryProfileKeepsPositioningCues(t *testing.T) {
|
|
for _, p := range Table() {
|
|
body := p.Body()
|
|
for _, cue := range positioningCues {
|
|
if !strings.Contains(body, cue) {
|
|
t.Errorf("profile %q body lost the positioning cue %q", p.Name, cue)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestLocalizationEagerToolsAllCued is the no-drift gate between the
|
|
// localization tool preset and the localization instructions body:
|
|
// every eager tool must be documented (table row or prose cue), and
|
|
// every table row must correspond to an eager tool.
|
|
func TestLocalizationEagerToolsAllCued(t *testing.T) {
|
|
p, ok := ByName("localization")
|
|
if !ok {
|
|
t.Fatal("localization profile missing from the table")
|
|
}
|
|
body := p.Body()
|
|
rowTools := map[string]bool{}
|
|
for _, r := range localizationRows {
|
|
rowTools[r.tool] = true
|
|
}
|
|
for _, tool := range p.EagerTools {
|
|
if !rowTools[tool] && !localizationNonTableTools[tool] {
|
|
t.Errorf("eager tool %q has neither a table row nor a prose cue — add one", tool)
|
|
}
|
|
if !strings.Contains(body, tool) {
|
|
t.Errorf("eager tool %q does not appear in the localization body", tool)
|
|
}
|
|
}
|
|
eager := map[string]bool{}
|
|
for _, tool := range p.EagerTools {
|
|
eager[tool] = true
|
|
}
|
|
for _, r := range localizationRows {
|
|
if !eager[r.tool] {
|
|
t.Errorf("table row for %q has no matching eager tool — remove the row or add the tool", r.tool)
|
|
}
|
|
}
|
|
for tool := range localizationNonTableTools {
|
|
if !eager[tool] {
|
|
t.Errorf("prose cue for %q has no matching eager tool", tool)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGenerateAndSwitch(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
if err := Generate(dir); err != nil {
|
|
t.Fatalf("generate: %v", err)
|
|
}
|
|
for _, p := range Table() {
|
|
raw, err := os.ReadFile(filepath.Join(dir, p.Name+".md"))
|
|
if err != nil {
|
|
t.Fatalf("profile file %s: %v", p.Name, err)
|
|
}
|
|
if string(raw) != p.Body() {
|
|
t.Errorf("profile file %s.md is not the rendered body", p.Name)
|
|
}
|
|
}
|
|
|
|
// Before any switch, the active copy is the default profile.
|
|
active, err := os.ReadFile(filepath.Join(dir, ActiveFileName))
|
|
if err != nil {
|
|
t.Fatalf("active.md: %v", err)
|
|
}
|
|
def, _ := ByName(DefaultName)
|
|
if string(active) != def.Body() {
|
|
t.Error("active.md is not a byte copy of the default profile before any switch")
|
|
}
|
|
if got := ActiveName(dir); got != DefaultName {
|
|
t.Errorf("ActiveName = %q before any switch, want %q", got, DefaultName)
|
|
}
|
|
|
|
// Switch to localization: active.md becomes a byte copy, state
|
|
// records the name, and it is never a symlink.
|
|
p, err := Switch(dir, "localization")
|
|
if err != nil {
|
|
t.Fatalf("switch: %v", err)
|
|
}
|
|
if p.Name != "localization" {
|
|
t.Errorf("switch returned profile %q", p.Name)
|
|
}
|
|
active, err = os.ReadFile(filepath.Join(dir, ActiveFileName))
|
|
if err != nil {
|
|
t.Fatalf("active.md after switch: %v", err)
|
|
}
|
|
loc, _ := ByName("localization")
|
|
if string(active) != loc.Body() {
|
|
t.Error("active.md is not a byte copy of the switched profile")
|
|
}
|
|
if fi, err := os.Lstat(filepath.Join(dir, ActiveFileName)); err != nil || fi.Mode()&os.ModeSymlink != 0 {
|
|
t.Errorf("active.md must be a regular file, got mode %v err %v", fi.Mode(), err)
|
|
}
|
|
if got := ActiveName(dir); got != "localization" {
|
|
t.Errorf("ActiveName = %q after switch, want localization", got)
|
|
}
|
|
if Active(dir).HookTier != HookTierStandard {
|
|
t.Error("localization ships the standard hook tier (the lean tier is opt-in machinery)")
|
|
}
|
|
|
|
// Re-generate keeps the switched selection.
|
|
if err := Generate(dir); err != nil {
|
|
t.Fatalf("re-generate: %v", err)
|
|
}
|
|
active, _ = os.ReadFile(filepath.Join(dir, ActiveFileName))
|
|
if string(active) != loc.Body() {
|
|
t.Error("re-generate clobbered the active selection")
|
|
}
|
|
|
|
// Unknown profile is refused and changes nothing.
|
|
if _, err := Switch(dir, "nope"); err == nil {
|
|
t.Error("switch to unknown profile must error")
|
|
}
|
|
if got := ActiveName(dir); got != "localization" {
|
|
t.Errorf("failed switch mutated state: ActiveName = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestActiveStateDegradesToDefault(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// Missing state file.
|
|
if got := ActiveName(dir); got != DefaultName {
|
|
t.Errorf("missing state: ActiveName = %q, want %q", got, DefaultName)
|
|
}
|
|
// Corrupt state file.
|
|
if err := os.WriteFile(filepath.Join(dir, stateFileName), []byte("{nope"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := ActiveName(dir); got != DefaultName {
|
|
t.Errorf("corrupt state: ActiveName = %q, want %q", got, DefaultName)
|
|
}
|
|
// Unknown name in state file.
|
|
if err := os.WriteFile(filepath.Join(dir, stateFileName), []byte(`{"name":"gone"}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := ActiveName(dir); got != DefaultName {
|
|
t.Errorf("unknown state name: ActiveName = %q, want %q", got, DefaultName)
|
|
}
|
|
}
|
|
|
|
func TestActiveEnvOverride(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if _, err := Switch(dir, "full"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv(ActiveEnv, "localization")
|
|
if got := ActiveName(dir); got != "localization" {
|
|
t.Errorf("env override ignored: ActiveName = %q", got)
|
|
}
|
|
// Unknown env value falls through to the recorded state.
|
|
t.Setenv(ActiveEnv, "not-a-profile")
|
|
if got := ActiveName(dir); got != "full" {
|
|
t.Errorf("unknown env value must fall through to state, got %q", got)
|
|
}
|
|
}
|
|
|
|
// Single-home markers: content that relocated to gortex://guide and the
|
|
// schema resources must never re-inflate an instruction body. Mirrors
|
|
// the pre-profiles guard on the CLAUDE.md rule block (the profile file
|
|
// is that block's new home).
|
|
var relocatedContentMarkers = []string{
|
|
"`local` / `anthropic` / `openai` / `azure` / `ollama` / `claudecli` / `codex` / `copilot` / `cursor` / `opencode` / `gemini` / `bedrock` / `deepseek`", // provider matrix
|
|
"Tarjan's SCC", // analyze catalog deep-dive
|
|
"compact tabular text, lossy", // wire-format deep-dive
|
|
"k8s_resources", // analyze kind catalog
|
|
"error-not-wrapped", // search_ast detector catalog
|
|
"gortex://report", // analyzer rollup roster
|
|
}
|
|
|
|
// fullPolicyTokens is the policy core the standard-depth profiles must
|
|
// carry — the machine-level single home for the full memory-workflow
|
|
// triggers. The localization profile intentionally keeps only the
|
|
// positioning cues (TestEveryProfileKeepsPositioningCues).
|
|
var fullPolicyTokens = []string{
|
|
"search_symbols", "find_usages", "get_callers", "get_call_chain",
|
|
"get_symbol_source", "get_editing_context", "read_file",
|
|
"smart_context", "edit_file", "rename_symbol", "compress_bodies",
|
|
"distill_session", "surface_memories", "save_note", "store_memory",
|
|
"query_notes", "query_memories",
|
|
"tools_search", "gortex://guide", "gortex daemon start",
|
|
}
|
|
|
|
func TestBodies_PolicyCoreAndSingleHome(t *testing.T) {
|
|
for _, name := range []string{"core", "full"} {
|
|
p, ok := ByName(name)
|
|
if !ok {
|
|
t.Fatalf("profile %q missing", name)
|
|
}
|
|
body := p.Body()
|
|
for _, token := range fullPolicyTokens {
|
|
if !strings.Contains(body, token) {
|
|
t.Errorf("%s body no longer mentions %q — policy core regression", name, token)
|
|
}
|
|
}
|
|
}
|
|
for _, p := range Table() {
|
|
body := p.Body()
|
|
for _, banned := range relocatedContentMarkers {
|
|
if strings.Contains(body, banned) {
|
|
t.Errorf("%s body re-carries relocated content %q — single-home violation", p.Name, banned)
|
|
}
|
|
}
|
|
}
|
|
}
|