1	package televibe
2
3	import (
4		"fmt"
5		"os"
6
7		"github.com/pelletier/go-toml/v2"
8
9		"televibe/ai"
10		"televibe/jira"
11		"televibe/reputation"
12		"televibe/risk"
13		"televibe/telemetry"
14	)
15
16	type Config struct {
17		Teleport   TeleportConfig    `toml:"teleport"`
18		AI         ai.Config         `toml:"ai"`
19		Jira       jira.Config       `toml:"jira"`
20		Risk       risk.Config       `toml:"risk"`
21		Reputation reputation.Config `toml:"reputation"`
22		Telemetry  telemetry.Config  `toml:"telemetry"`
23		Log        LogConfig         `toml:"log"`
24	}
25
26	type TeleportConfig struct {
27		Addr         string `toml:"addr"`
28		IdentityFile string `toml:"identity_file"`
29	}
30
31	type LogConfig struct {
32		Level string `toml:"level"`
33	}
34
35	func LoadConfig(path string) (*Config, error) {
36		data, err := os.ReadFile(path)
37		if err != nil {
38			return nil, fmt.Errorf("read config: %w", err)
39		}
40		cfg := &Config{}
41		if err := toml.Unmarshal(data, cfg); err != nil {
42			return nil, fmt.Errorf("parse config: %w", err)
43		}
44		if err := applyEnv(cfg); err != nil {
45			return nil, fmt.Errorf("apply env overrides: %w", err)
46		}
47		cfg.applyDefaults()
48		if err := cfg.validate(); err != nil {
49			return nil, err
50		}
51		return cfg, nil
52	}
53
54	func (c *Config) applyDefaults() {
55		c.Jira.ApplyDefaults()
56		c.Risk.ApplyDefaults()
57		c.Reputation.ApplyDefaults()
58		c.Telemetry.ApplyDefaults()
59		if c.Log.Level == "" {
60			c.Log.Level = "info"
61		}
62	}
63
64	func (c *Config) validate() error {
65		if c.Teleport.Addr == "" {
66			return fmt.Errorf("teleport.addr is required")
67		}
68		if c.Teleport.IdentityFile == "" {
69			return fmt.Errorf("teleport.identity_file is required")
70		}
71		if err := c.AI.Validate(); err != nil {
72			return fmt.Errorf("ai: %w", err)
73		}
74		if err := c.Jira.Validate(); err != nil {
75			return fmt.Errorf("jira: %w", err)
76		}
77		if err := c.Reputation.Validate(); err != nil {
78			return fmt.Errorf("reputation: %w", err)
79		}
80		return nil
81	}
82

package televibe

import (
	"fmt"
	"os"

	"github.com/pelletier/go-toml/v2"

	"televibe/ai"
	"televibe/jira"
	"televibe/reputation"
	"televibe/risk"
	"televibe/telemetry"
)

type Config struct {
	Teleport   TeleportConfig    `toml:"teleport"`
	AI         ai.Config         `toml:"ai"`
	Jira       jira.Config       `toml:"jira"`
	Risk       risk.Config       `toml:"risk"`
	Reputation reputation.Config `toml:"reputation"`
	Telemetry  telemetry.Config  `toml:"telemetry"`
	Log        LogConfig         `toml:"log"`
}

type TeleportConfig struct {
	Addr         string `toml:"addr"`
	IdentityFile string `toml:"identity_file"`
}

type LogConfig struct {
	Level string `toml:"level"`
}

func LoadConfig(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read config: %w", err)
	}
	cfg := &Config{}
	if err := toml.Unmarshal(data, cfg); err != nil {
		return nil, fmt.Errorf("parse config: %w", err)
	}
	if err := applyEnv(cfg); err != nil {
		return nil, fmt.Errorf("apply env overrides: %w", err)
	}
	cfg.applyDefaults()
	if err := cfg.validate(); err != nil {
		return nil, err
	}
	return cfg, nil
}

func (c *Config) applyDefaults() {
	c.Jira.ApplyDefaults()
	c.Risk.ApplyDefaults()
	c.Reputation.ApplyDefaults()
	c.Telemetry.ApplyDefaults()
	if c.Log.Level == "" {
		c.Log.Level = "info"
	}
}

func (c *Config) validate() error {
	if c.Teleport.Addr == "" {
		return fmt.Errorf("teleport.addr is required")
	}
	if c.Teleport.IdentityFile == "" {
		return fmt.Errorf("teleport.identity_file is required")
	}
	if err := c.AI.Validate(); err != nil {
		return fmt.Errorf("ai: %w", err)
	}
	if err := c.Jira.Validate(); err != nil {
		return fmt.Errorf("jira: %w", err)
	}
	if err := c.Reputation.Validate(); err != nil {
		return fmt.Errorf("reputation: %w", err)
	}
	return nil
}


bugbot also identified this, can you check if this is a problem? Zero-value defaults prevent disabling reputation penalties via config  Medium Severity  ApplyDefaults uses == 0 guards for PenaltyMissi…


Edit

	cfg := &Config{}
	if err := toml.Unmarshal(data, cfg); err != nil {
		return nil, fmt.Errorf("parse config: %w", err)
	}
	if err := applyEnv(cfg); err != nil {
		return nil, fmt.Errorf("apply env overrides: %w", err)
	}
	cfg.applyDefaults()
	if err := cfg.validate(); err != nil {
		return nil, err
	}
	return cfg, nil
}

	// Defaults go first so that TOML and env can override them — including
	// with explicit zero values. The reverse order conflates "unset" with
	// "set to 0", silently resurrecting defaults for fields a user meant
	// to disable (e.g. reputation.penalty_missing_custx = 0).
	cfg := &Config{}
	cfg.applyDefaults()
	if err := toml.Unmarshal(data, cfg); err != nil {
		return nil, fmt.Errorf("parse config: %w", err)
	}
	if err := applyEnv(cfg); err != nil {
		return nil, fmt.Errorf("apply env overrides: %w", err)
	}
	if err := cfg.validate(); err != nil {
		return nil, err
	}
	return cfg, nil
}


The file /Users/steve.briggs/projects/operations/apps/televibe/config.go has been updated successfully.

	// Defaults go first so that TOML and env can override them — including
	// with explicit zero values. The reverse order conflates "unset" with
	// "set to 0", silently resurrecting defaults for fields a user meant
	// to disable (e.g. reputation.penalty_missing_custx = 0).
	cfg := &Config{}
	cfg.applyDefaults()
	if err := toml.Unmarshal(data, cfg); err != nil {
		return nil, fmt.Errorf("parse config: %w", err)
	}
	if err := applyEnv(cfg); err != nil {
		return nil, fmt.Errorf("apply env overrides: %w", err)
	}
	if err := cfg.validate(); err != nil {
		return nil, err
	}
	return cfg, nil
}

package televibe

import (
	"fmt"
	"os"

	"github.com/pelletier/go-toml/v2"

	"televibe/ai"
	"televibe/jira"
	"televibe/reputation"
	"televibe/risk"
	"televibe/telemetry"
)

type Config struct {
	Teleport   TeleportConfig    `toml:"teleport"`
	AI         ai.Config         `toml:"ai"`
	Jira       jira.Config       `toml:"jira"`
	Risk       risk.Config       `toml:"risk"`
	Reputation reputation.Config `toml:"reputation"`
	Telemetry  telemetry.Config  `toml:"telemetry"`
	Log        LogConfig         `toml:"log"`
}

type TeleportConfig struct {
	Addr         string `toml:"addr"`
	IdentityFile string `toml:"identity_file"`
}

type LogConfig struct {
	Level string `toml:"level"`
}

func LoadConfig(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("read config: %w", err)
	}
	cfg := &Config{}
	if err := toml.Unmarshal(data, cfg); err != nil {
		return nil, fmt.Errorf("parse config: %w", err)
	}
	if err := applyEnv(cfg); err != nil {
		return nil, fmt.Errorf("apply env overrides: %w", err)
	}
	cfg.applyDefaults()
	if err := cfg.validate(); err != nil {
		return nil, err
	}
	return cfg, nil
}

func (c *Config) applyDefaults() {
	c.Jira.ApplyDefaults()
	c.Risk.ApplyDefaults()
	c.Reputation.ApplyDefaults()
	c.Telemetry.ApplyDefaults()
	if c.Log.Level == "" {
		c.Log.Level = "info"
	}
}

func (c *Config) validate() error {
	if c.Teleport.Addr == "" {
		return fmt.Errorf("teleport.addr is required")
	}
	if c.Teleport.IdentityFile == "" {
		return fmt.Errorf("teleport.identity_file is required")
	}
	if err := c.AI.Validate(); err != nil {
		return fmt.Errorf("ai: %w", err)
	}
	if err := c.Jira.Validate(); err != nil {
		return fmt.Errorf("jira: %w", err)
	}
	if err := c.Reputation.Validate(); err != nil {
		return fmt.Errorf("reputation: %w", err)
	}
	return nil
}

+  // Defaults go first so that TOML and env can override them — including

+  // with explicit zero values. The reverse order conflates "unset" with

+  // "set to 0", silently resurrecting defaults for fields a user meant

+  // to disable (e.g. reputation.penalty_missing_custx = 0).


Now update the stale comment in the reputation test and add an integration test that exercises the LoadConfig pipeline.


func TestAssess_DisabledPenaltyIsSkipped(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 0 // explicit disable
	cfg.PenaltyThinDescription = 5
	cfg.StartingScore = 100
	// ApplyDefaults already ran in baseCfg, but setting to 0 after is a valid
	// "turn this off" gesture — ApplyDefaults would resurrect it, so rebuild:
	s, _ := NewStore(cfg)

func TestAssess_DisabledPenaltyIsSkipped(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 0 // explicit disable
	cfg.PenaltyThinDescription = 5
	cfg.StartingScore = 100
	s, _ := NewStore(cfg)


The file /Users/steve.briggs/projects/operations/apps/televibe/reputation/reputation_test.go has been updated successfully.

func TestAssess_DisabledPenaltyIsSkipped(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 0 // explicit disable
	cfg.PenaltyThinDescription = 5
	cfg.StartingScore = 100
	s, _ := NewStore(cfg)

package reputation

import (
	"context"
	"os"
	"path/filepath"
	"testing"
)

func baseCfg(path string) Config {
	c := Config{PersistPath: path, FloorScore: 0}
	c.ApplyDefaults()
	return c
}

func TestScore_DefaultsToStartingScore(t *testing.T) {
	s, err := NewStore(baseCfg(""))
	if err != nil {
		t.Fatalf("NewStore: %v", err)
	}
	if got := s.Score("alice"); got != 100 {
		t.Errorf("Score(alice) = %d, want 100", got)
	}
}

func TestAssess_AppliesConfiguredPenalties(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 30
	cfg.PenaltyThinDescription = 15
	s, _ := NewStore(cfg)

	a, err := s.Assess(context.Background(), "alice", map[string]int{
		"missing_custx_ticket": 30,
		"thin_description":     15,
		"privilege_concern":    10, // not a reputation signal, ignored
	})
	if err != nil {
		t.Fatalf("Assess: %v", err)
	}
	if a.Previous != 100 {
		t.Errorf("Previous = %d, want 100", a.Previous)
	}
	if a.Score != 55 {
		t.Errorf("Score = %d, want 55", a.Score)
	}
	if len(a.Penalties) != 2 {
		t.Errorf("len(Penalties) = %d, want 2", len(a.Penalties))
	}
}

func TestAssess_ClampsToFloor(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 40
	s, _ := NewStore(cfg)
	ctx := context.Background()

	for i := 0; i < 5; i++ {
		if _, err := s.Assess(ctx, "alice", map[string]int{"missing_custx_ticket": 1}); err != nil {
			t.Fatalf("Assess: %v", err)
		}
	}
	if got := s.Score("alice"); got != 0 {
		t.Errorf("floored score = %d, want 0", got)
	}
}

func TestAssess_NoSignalsIsNoOp(t *testing.T) {
	s, _ := NewStore(baseCfg(""))
	a, err := s.Assess(context.Background(), "bob", map[string]int{"description_quality": 3})
	if err != nil {
		t.Fatalf("Assess: %v", err)
	}
	if a.Score != 100 || len(a.Penalties) != 0 {
		t.Errorf("unexpected assessment: %+v", a)
	}
}

func TestPersistence_RoundTrip(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "sub", "rep.json") // exercise MkdirAll

	cfg := baseCfg(path)
	cfg.PenaltyMissingCUSTX = 10
	s, err := NewStore(cfg)
	if err != nil {
		t.Fatalf("NewStore: %v", err)
	}
	if _, err := s.Assess(context.Background(), "alice", map[string]int{"missing_custx_ticket": 1}); err != nil {
		t.Fatalf("Assess: %v", err)
	}

	s2, err := NewStore(cfg)
	if err != nil {
		t.Fatalf("NewStore (reload): %v", err)
	}
	if got := s2.Score("alice"); got != 90 {
		t.Errorf("after reload, Score(alice) = %d, want 90", got)
	}
	if got := s2.Score("bob"); got != 100 {
		t.Errorf("unseen user Score(bob) = %d, want 100", got)
	}

	st, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat: %v", err)
	}
	if got := st.Mode().Perm(); got != 0o600 {
		t.Errorf("mode = %o, want 0600", got)
	}
}

func TestValidate(t *testing.T) {
	cases := []struct {
		name string
		cfg  Config
		ok   bool
	}{
		{"floor_above_ceiling", Config{FloorScore: 50, CeilingScore: 10, StartingScore: 20}, false},
		{"starting_out_of_range", Config{FloorScore: 0, CeilingScore: 100, StartingScore: 150}, false},
		{"ok", Config{FloorScore: 0, CeilingScore: 100, StartingScore: 50}, true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := tc.cfg.Validate()
			if (err == nil) != tc.ok {
				t.Errorf("Validate() err = %v, ok = %v", err, tc.ok)
			}
		})
	}
}

func TestRange_IteratesAllUsers(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 10
	cfg.PenaltyThinDescription = 5
	s, _ := NewStore(cfg)
	ctx := context.Background()
	_, _ = s.Assess(ctx, "alice", map[string]int{"missing_custx_ticket": 1})
	_, _ = s.Assess(ctx, "bob", map[string]int{"thin_description": 1})

	seen := map[string]int{}
	s.Range(func(u string, sc int) { seen[u] = sc })
	if len(seen) != 2 {
		t.Errorf("len(seen) = %d, want 2", len(seen))
	}
	if seen["alice"] != 90 || seen["bob"] != 95 {
		t.Errorf("unexpected seen: %+v", seen)
	}
}

func TestAssess_DisabledPenaltyIsSkipped(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 0 // explicit disable
	cfg.PenaltyThinDescription = 5
	cfg.StartingScore = 100
	// ApplyDefaults already ran in baseCfg, but setting to 0 after is a valid
	// "turn this off" gesture — ApplyDefaults would resurrect it, so rebuild:
	s, _ := NewStore(cfg)

	a, _ := s.Assess(context.Background(), "alice", map[string]int{
		"missing_custx_ticket": 1,
		"thin_description":     1,
	})
	if a.Score != 95 {
		t.Errorf("Score = %d, want 95 (only thin_description applied)", a.Score)
	}
	for _, p := range a.Penalties {
		if p.Reason == "missing_custx_ticket" {
			t.Errorf("missing_custx_ticket penalty should be skipped when amount = 0")
		}
	}
}

-  // ApplyDefaults already ran in baseCfg, but setting to 0 after is a valid

-  // "turn this off" gesture — ApplyDefaults would resurrect it, so rebuild:


Write

package televibe

import (
	"os"
	"path/filepath"
	"testing"
)

func writeConfig(t *testing.T, body string) string {
	t.Helper()
	dir := t.TempDir()
	path := filepath.Join(dir, "televibe.toml")
	if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
		t.Fatalf("write config: %v", err)
	}
	return path
}

// Explicit zeros in TOML must not be silently resurrected by ApplyDefaults.
// This is the integration-level expression of the bug where a user who
// wants to disable a penalty by setting it to 0 would otherwise get the
// default value back.
func TestLoadConfig_ExplicitZeroDisablesDefaultBackedField(t *testing.T) {
	path := writeConfig(t, `
[teleport]
addr          = "proxy.example.com:443"
identity_file = "/tmp/televibe.pem"

[ai]
provider = "anthropic"

[ai.anthropic]
api_key = "sk-test"

[jira]
url       = "https://example.atlassian.net"
username  = "bot@example.com"
api_token = "token"
project   = "ITSM"

[risk]
ticket_threshold     = 0
escalation_threshold = 0

[reputation]
penalty_missing_custx    = 0
penalty_thin_description = 0
`)

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	if cfg.Reputation.PenaltyMissingCUSTX != 0 {
		t.Errorf("PenaltyMissingCUSTX = %d, want 0 (TOML disable must win over default)", cfg.Reputation.PenaltyMissingCUSTX)
	}
	if cfg.Reputation.PenaltyThinDescription != 0 {
		t.Errorf("PenaltyThinDescription = %d, want 0", cfg.Reputation.PenaltyThinDescription)
	}
	if cfg.Risk.TicketThreshold != 0 {
		t.Errorf("Risk.TicketThreshold = %d, want 0", cfg.Risk.TicketThreshold)
	}
	if cfg.Risk.EscalationThreshold != 0 {
		t.Errorf("Risk.EscalationThreshold = %d, want 0", cfg.Risk.EscalationThreshold)
	}
}

// Absent keys should still receive defaults — flipping the layering order
// must not regress the common case of "I didn't set this; give me sane defaults".
func TestLoadConfig_AbsentKeysGetDefaults(t *testing.T) {
	path := writeConfig(t, `
[teleport]
addr          = "proxy.example.com:443"
identity_file = "/tmp/televibe.pem"

[ai]
provider = "anthropic"

[ai.anthropic]
api_key = "sk-test"

[jira]
url       = "https://example.atlassian.net"
username  = "bot@example.com"
api_token = "token"
project   = "ITSM"
`)

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	if got, want := cfg.Risk.TicketThreshold, 25; got != want {
		t.Errorf("Risk.TicketThreshold = %d, want %d (default)", got, want)
	}
	if got, want := cfg.Risk.EscalationThreshold, 15; got != want {
		t.Errorf("Risk.EscalationThreshold = %d, want %d (default)", got, want)
	}
	if got, want := cfg.Reputation.PenaltyMissingCUSTX, 10; got != want {
		t.Errorf("PenaltyMissingCUSTX = %d, want %d (default)", got, want)
	}
	if got, want := cfg.Reputation.StartingScore, 100; got != want {
		t.Errorf("StartingScore = %d, want %d (default)", got, want)
	}
	if got, want := cfg.Log.Level, "info"; got != want {
		t.Errorf("Log.Level = %q, want %q (default)", got, want)
	}
}

// Env var overrides still take precedence over both TOML and defaults,
// including when the TOML value is an explicit zero.
func TestLoadConfig_EnvBeatsTOMLZero(t *testing.T) {
	t.Setenv("TV_RISK_TICKET_THRESHOLD", "42")
	path := writeConfig(t, `
[teleport]
addr          = "proxy.example.com:443"
identity_file = "/tmp/televibe.pem"

[ai]
provider = "anthropic"

[ai.anthropic]
api_key = "sk-test"

[jira]
url       = "https://example.atlassian.net"
username  = "bot@example.com"
api_token = "token"
project   = "ITSM"

[risk]
ticket_threshold = 0
`)

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}
	if cfg.Risk.TicketThreshold != 42 {
		t.Errorf("Risk.TicketThreshold = %d, want 42 (env > TOML > default)", cfg.Risk.TicketThreshold)
	}
}


File created successfully at: /Users/steve.briggs/projects/operations/apps/televibe/config_test.go

package televibe

import (
	"os"
	"path/filepath"
	"testing"
)

func writeConfig(t *testing.T, body string) string {
	t.Helper()
	dir := t.TempDir()
	path := filepath.Join(dir, "televibe.toml")
	if err := os.WriteFile(path, []byte(body), 0o600); err != nil {
		t.Fatalf("write config: %v", err)
	}
	return path
}

// Explicit zeros in TOML must not be silently resurrected by ApplyDefaults.
// This is the integration-level expression of the bug where a user who
// wants to disable a penalty by setting it to 0 would otherwise get the
// default value back.
func TestLoadConfig_ExplicitZeroDisablesDefaultBackedField(t *testing.T) {
	path := writeConfig(t, `
[teleport]
addr          = "proxy.example.com:443"
identity_file = "/tmp/televibe.pem"

[ai]
provider = "anthropic"

[ai.anthropic]
api_key = "sk-test"

[jira]
url       = "https://example.atlassian.net"
username  = "bot@example.com"
api_token = "token"
project   = "ITSM"

[risk]
ticket_threshold     = 0
escalation_threshold = 0

[reputation]
penalty_missing_custx    = 0
penalty_thin_description = 0
`)

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	if cfg.Reputation.PenaltyMissingCUSTX != 0 {
		t.Errorf("PenaltyMissingCUSTX = %d, want 0 (TOML disable must win over default)", cfg.Reputation.PenaltyMissingCUSTX)
	}
	if cfg.Reputation.PenaltyThinDescription != 0 {
		t.Errorf("PenaltyThinDescription = %d, want 0", cfg.Reputation.PenaltyThinDescription)
	}
	if cfg.Risk.TicketThreshold != 0 {
		t.Errorf("Risk.TicketThreshold = %d, want 0", cfg.Risk.TicketThreshold)
	}
	if cfg.Risk.EscalationThreshold != 0 {
		t.Errorf("Risk.EscalationThreshold = %d, want 0", cfg.Risk.EscalationThreshold)
	}
}

// Absent keys should still receive defaults — flipping the layering order
// must not regress the common case of "I didn't set this; give me sane defaults".
func TestLoadConfig_AbsentKeysGetDefaults(t *testing.T) {
	path := writeConfig(t, `
[teleport]
addr          = "proxy.example.com:443"
identity_file = "/tmp/televibe.pem"

[ai]
provider = "anthropic"

[ai.anthropic]
api_key = "sk-test"

[jira]
url       = "https://example.atlassian.net"
username  = "bot@example.com"
api_token = "token"
project   = "ITSM"
`)

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	if got, want := cfg.Risk.TicketThreshold, 25; got != want {
		t.Errorf("Risk.TicketThreshold = %d, want %d (default)", got, want)
	}
	if got, want := cfg.Risk.EscalationThreshold, 15; got != want {
		t.Errorf("Risk.EscalationThreshold = %d, want %d (default)", got, want)
	}
	if got, want := cfg.Reputation.PenaltyMissingCUSTX, 10; got != want {
		t.Errorf("PenaltyMissingCUSTX = %d, want %d (default)", got, want)
	}
	if got, want := cfg.Reputation.StartingScore, 100; got != want {
		t.Errorf("StartingScore = %d, want %d (default)", got, want)
	}
	if got, want := cfg.Log.Level, "info"; got != want {
		t.Errorf("Log.Level = %q, want %q (default)", got, want)
	}
}

// Env var overrides still take precedence over both TOML and defaults,
// including when the TOML value is an explicit zero.
func TestLoadConfig_EnvBeatsTOMLZero(t *testing.T) {
	t.Setenv("TV_RISK_TICKET_THRESHOLD", "42")
	path := writeConfig(t, `
[teleport]
addr          = "proxy.example.com:443"
identity_file = "/tmp/televibe.pem"

[ai]
provider = "anthropic"

[ai.anthropic]
api_key = "sk-test"

[jira]
url       = "https://example.atlassian.net"
username  = "bot@example.com"
api_token = "token"
project   = "ITSM"

[risk]
ticket_threshold = 0
`)

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}
	if cfg.Risk.TicketThreshold != 42 {
		t.Errorf("Risk.TicketThreshold = %d, want 42 (env > TOML > default)", cfg.Risk.TicketThreshold)
	}
}


Bash

go build ./... 2>&1; echo '---all tests---'; go test -race ./... 2>&1 | tail -12; echo '---config tests verbose---'; go test -race -v . -run LoadConfig 2>&1 | tail -20

Build and run full test suite plus LoadConfig tests verbose


---all tests---
ok  	televibe	2.127s
?   	televibe/ai	[no test files]
?   	televibe/cmd/submit	[no test files]
?   	televibe/cmd/televibe	[no test files]
?   	televibe/jira	[no test files]
ok  	televibe/reputation	1.944s
ok  	televibe/risk	(cached)
?   	televibe/telemetry	[no test files]
---config tests verbose---
=== RUN   TestLoadConfig_ExplicitZeroDisablesDefaultBackedField
--- PASS: TestLoadConfig_ExplicitZeroDisablesDefaultBackedField (0.00s)
=== RUN   TestLoadConfig_AbsentKeysGetDefaults
--- PASS: TestLoadConfig_AbsentKeysGetDefaults (0.00s)
=== RUN   TestLoadConfig_EnvBeatsTOMLZero
--- PASS: TestLoadConfig_EnvBeatsTOMLZero (0.00s)
PASS
ok  	televibe	1.793s


All green. Bugbot's issue confirmed as real, fixed at the source.

What changed:

**`config.go`** — one-line semantic fix: `cfg.applyDefaults()` now runs *before* `toml.Unmarshal`, not after. Inline comment explains why (the `== 0` footgun conflating "unset" with "disabled"). This fixes the bug bugbot flagged in `reputation` and also its structural twins in `risk.ApplyDefaults` (`ticket_threshold`, `escalation_threshold`), `telemetry.ApplyDefaults` (`export_period`), and `jira.ApplyDefaults` — all of which had the same shape but bugbot only called out one.

**New `config_test.go`** — three integration tests exercising the full `LoadConfig` pipeline:
1. Explicit zeros (`penalty_missing_custx = 0`, `ticket_threshold = 0`, etc.) survive and stay zero.
2. Keys absent from TOML still receive defaults — the "I didn't set this, give me sane values" case isn't regressed.
3. Env vars beat TOML zeros (`TV_RISK_TICKET_THRESHOLD=42` + `ticket_threshold = 0` → 42), proving the precedence ladder env > TOML > defaults holds end-to-end.

**`reputation_test.go`** — removed the two-line "ApplyDefaults would resurrect it, so rebuild" comment, which was documentation for the bug rather than the fix.

A nice side effect: `risk.ticket_threshold = 0` is now a valid way to say "auto-approve everything, never touch Jira" — a config-disable-the-Jira-path switch you couldn't actually set before without patching code.


bugbot also found this issue can you verify and identify an appropriate fix? Rehydration regex can't match bold-wrapped description field

High Severity

The reqIDRE regex (Teleport Request ID:\s*([A-Za-z0-9-]+)) expects optional whitespace after the colon, but buildDescription writes the line as * *Teleport Request ID:* %s using Jira wiki bold markup. In the raw stored text, a literal * (closing bold marker) and a space sit between ID: and the actual request ID. Since * is neither whitespace nor in [A-Za-z0-9-], the regex never matches, so ListOpenTeleportTickets always returns an empty map and restart rehydration silently fails — leading to duplicate ITSM tickets after any restart.
Additional Locations (1)

    apps/televibe/jira/itsm.go#L159-L160


Bugbot's right. Let me confirm by reading the exact strings and then fix it.


60	)
61
62	func (s Status) String() string {
63		switch s {
64		case StatusApproved:
65			return "approved"
66		case StatusRejected:
67			return "rejected"
68		}
69		return "open"
70	}
71
72	type TicketInput struct {
73		RequestID   string
74		User        string
75		Roles       []string
76		Reason      string
77		Score       int
78		Tier        string
79		Components  map[string]int
80		AIReasoning string
81		Escalation  bool
82	}
83
84	type Client struct {
85		cfg     Config
86		jira    *gojira.Client
87		reqIDRE *regexp.Regexp
88	}
89
90	func New(cfg Config) (*Client, error) {
91		tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
92		jc, err := gojira.NewClient(cfg.URL, tp.Client())
93		if err != nil {
94			return nil, err
95		}
96		return &Client{
97			cfg:     cfg,
98			jira:    jc,
99			reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),
100		}, nil
101	}
102
103	func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
104		if c.cfg.Project == "" {
105			return "", fmt.Errorf("jira project key is empty")
106		}
107		if c.cfg.IssueType == "" {
108			return "", fmt.Errorf("jira issue type is empty")
109		}

)

func (s Status) String() string {
	switch s {
	case StatusApproved:
		return "approved"
	case StatusRejected:
		return "rejected"
	}
	return "open"
}

type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

type Client struct {
	cfg     Config
	jira    *gojira.Client
	reqIDRE *regexp.Regexp
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{
		cfg:     cfg,
		jira:    jc,
		reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),
	}, nil
}

func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
	if c.cfg.Project == "" {
		return "", fmt.Errorf("jira project key is empty")
	}
	if c.cfg.IssueType == "" {
		return "", fmt.Errorf("jira issue type is empty")
	}


Confirmed. Line 99's regex: `Teleport Request ID:\s*([A-Za-z0-9-]+)`. Line 197's output: `* *Teleport Request ID:* <id>`. Between `ID:` and the ID sits a `*` (closing Jira bold), which is neither whitespace nor an ID character — so `\s*` matches nothing, then `[A-Za-z0-9-]+` hits `*` and fails. Rehydration silently returns `{}` on every restart.

Fix: relax the character class between `ID:` and the capture to any non-ID-character, so it tolerates the bold marker (and any future formatting). While I'm there, promote the regex to a package-level `var` so tests can exercise it without constructing a client.


Edit

type Client struct {
	cfg     Config
	jira    *gojira.Client
	reqIDRE *regexp.Regexp
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{
		cfg:     cfg,
		jira:    jc,
		reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),
	}, nil
}

// reqIDRE extracts a Teleport access-request ID from a Jira ticket
// description we created via buildDescription. The character class between
// the colon and the capture tolerates Jira wiki bold markers (e.g. the
// "*" that closes "*Teleport Request ID:*") as well as plain whitespace,
// so rehydration survives formatting tweaks to the description template.
var reqIDRE = regexp.MustCompile(`Teleport Request ID:[^A-Za-z0-9-]*([A-Za-z0-9-]+)`)

type Client struct {
	cfg  Config
	jira *gojira.Client
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{cfg: cfg, jira: jc}, nil
}


bugbot also found this issue can you verify and identify an appropriate fix? Rehydration regex can't match bold-wrapped description field  High Severity  The reqIDRE regex (Teleport Request ID:\s*([A-…


The file /Users/steve.briggs/projects/operations/apps/televibe/jira/itsm.go has been updated successfully.

// reqIDRE extracts a Teleport access-request ID from a Jira ticket
// description we created via buildDescription. The character class between
// the colon and the capture tolerates Jira wiki bold markers (e.g. the
// "*" that closes "*Teleport Request ID:*") as well as plain whitespace,
// so rehydration survives formatting tweaks to the description template.
var reqIDRE = regexp.MustCompile(`Teleport Request ID:[^A-Za-z0-9-]*([A-Za-z0-9-]+)`)

type Client struct {
	cfg  Config
	jira *gojira.Client
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{cfg: cfg, jira: jc}, nil
}

package jira

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	gojira "github.com/andygrunwald/go-jira/v2/cloud"
)

type Config struct {
	URL            string        `toml:"url"`
	Username       string        `toml:"username"`
	APIToken       string        `toml:"api_token"`
	Project        string        `toml:"project"`
	IssueType      string        `toml:"issue_type"`
	PollInterval   time.Duration `toml:"poll_interval"`
	ApprovedStatus string        `toml:"approved_status"`
	RejectedStatus string        `toml:"rejected_status"`
}

func (c *Config) ApplyDefaults() {
	if c.IssueType == "" {
		c.IssueType = "Task"
	}
	if c.PollInterval == 0 {
		c.PollInterval = 30 * time.Second
	}
	if c.ApprovedStatus == "" {
		c.ApprovedStatus = "Approved"
	}
	if c.RejectedStatus == "" {
		c.RejectedStatus = "Rejected"
	}
}

func (c *Config) Validate() error {
	if c.URL == "" {
		return fmt.Errorf("url is required")
	}
	if c.Project == "" {
		return fmt.Errorf("project is required")
	}
	if c.Username == "" || c.APIToken == "" {
		return fmt.Errorf("username and api_token are required")
	}
	return nil
}

type Status int

const (
	StatusOpen Status = iota
	StatusApproved
	StatusRejected
)

func (s Status) String() string {
	switch s {
	case StatusApproved:
		return "approved"
	case StatusRejected:
		return "rejected"
	}
	return "open"
}

type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

type Client struct {
	cfg     Config
	jira    *gojira.Client
	reqIDRE *regexp.Regexp
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{
		cfg:     cfg,
		jira:    jc,
		reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),
	}, nil
}

func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
	if c.cfg.Project == "" {
		return "", fmt.Errorf("jira project key is empty")
	}
	if c.cfg.IssueType == "" {
		return "", fmt.Errorf("jira issue type is empty")
	}
	if in.RequestID == "" {
		return "", fmt.Errorf("request id is empty")
	}

	labels := []string{"teleport", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "teleport-escalation")
	}

	summary := fmt.Sprintf("[teleport] Access request %s - %s", in.RequestID, in.Tier)
	if in.Escalation {
		summary = fmt.Sprintf("[teleport][escalation] Session escalation %s - %s", in.RequestID, in.Tier)
	}

	issue := &gojira.Issue{
		Fields: &gojira.IssueFields{
			Project:     gojira.Project{Key: c.cfg.Project},
			Type:        gojira.IssueType{Name: c.cfg.IssueType},
			Summary:     summary,
			Description: buildDescription(in),
			Labels:      labels,
		},
	}

	reqBody, err := json.MarshalIndent(issue, "", "  ")
	if err != nil {
		return "", fmt.Errorf("marshal jira issue: %w", err)
	}
	fmt.Println("Jira request body:")
	fmt.Println(string(reqBody))

	created, resp, err := c.jira.Issue.Create(ctx, issue)
	if err != nil {
		if resp != nil && resp.Body != nil {
			body, _ := io.ReadAll(resp.Body)
			fmt.Println("Jira error response:")
			fmt.Println(string(body))
		}
		return "", fmt.Errorf("create jira issue: %w", err)
	}

	if created == nil {
		return "", fmt.Errorf("jira returned nil issue without error")
	}

	return created.Key, nil
}

func (c *Client) TicketStatus(ctx context.Context, key string) (Status, error) {
	issue, _, err := c.jira.Issue.Get(ctx, key, nil)
	if err != nil {
		return StatusOpen, err
	}
	if issue.Fields == nil || issue.Fields.Status == nil {
		return StatusOpen, nil
	}
	switch issue.Fields.Status.Name {
	case c.cfg.ApprovedStatus:
		return StatusApproved, nil
	case c.cfg.RejectedStatus:
		return StatusRejected, nil
	}
	return StatusOpen, nil
}

func (c *Client) ListOpenTeleportTickets(ctx context.Context) (map[string]string, error) {
	jql := fmt.Sprintf(`project = %q AND labels = teleport AND status not in (%q, %q)`,
		c.cfg.Project, c.cfg.ApprovedStatus, c.cfg.RejectedStatus)
	issues, _, err := c.jira.Issue.Search(ctx, jql, nil)
	if err != nil {
		return nil, err
	}
	out := map[string]string{}
	for _, iss := range issues {
		if iss.Fields == nil {
			continue
		}
		m := c.reqIDRE.FindStringSubmatch(iss.Fields.Description)
		if len(m) < 2 {
			continue
		}
		out[m[1]] = iss.Key
	}
	return out, nil
}

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	if in.Escalation {
		sb.WriteString("h2. Teleport Session Escalation Review\n\n")
		sb.WriteString("{panel:title=Session escalation}This request is a mid-session privilege bump — please prioritise review.{panel}\n\n")
	} else {
		sb.WriteString("h2. Teleport Access Request Review\n\n")
	}
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s\n\n", in.Tier)

	sb.WriteString("h3. Request reason\n")
	sb.WriteString("{code}\n")
	sb.WriteString(in.Reason)
	sb.WriteString("\n{code}\n\n")

	if len(in.Components) > 0 {
		sb.WriteString("h3. Risk components\n")
		for k, v := range in.Components {
			fmt.Fprintf(&sb, "* %s: +%d\n", k, v)
		}
		sb.WriteString("\n")
	}

	if in.AIReasoning != "" {
		sb.WriteString("h3. AI reasoning\n")
		sb.WriteString("{quote}\n")
		sb.WriteString(in.AIReasoning)
		sb.WriteString("\n{quote}\n\n")
	}

	sb.WriteString("h3. SRE action required\n")
	sb.WriteString("Transition this ticket to the configured approved or rejected status to resolve the Teleport access request.\n")

	return sb.String()
}

+// reqIDRE extracts a Teleport access-request ID from a Jira ticket

+// description we created via buildDescription. The character class between

+// the colon and the capture tolerates Jira wiki bold markers (e.g. the

+// "*" that closes "*Teleport Request ID:*") as well as plain whitespace,

+// so rehydration survives formatting tweaks to the description template.

+var reqIDRE = regexp.MustCompile(`Teleport Request ID:[^A-Za-z0-9-]*([A-Za-z0-9-]+)`)

-    reqIDRE: regexp.MustCompile(`Teleport Request ID:\s*([A-Za-z0-9-]+)`),

+  return &Client{cfg: cfg, jira: jc}, nil


Bash

grep -n "reqIDRE\|c\.reqIDRE" /Users/steve.briggs/projects/operations/apps/televibe/jira/itsm.go

Find remaining references to update


84:// reqIDRE extracts a Teleport access-request ID from a Jira ticket
89:var reqIDRE = regexp.MustCompile(`Teleport Request ID:[^A-Za-z0-9-]*([A-Za-z0-9-]+)`)
192:		m := c.reqIDRE.FindStringSubmatch(iss.Fields.Description)


package jira

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"
	"time"

	gojira "github.com/andygrunwald/go-jira/v2/cloud"
)

type Config struct {
	URL            string        `toml:"url"`
	Username       string        `toml:"username"`
	APIToken       string        `toml:"api_token"`
	Project        string        `toml:"project"`
	IssueType      string        `toml:"issue_type"`
	PollInterval   time.Duration `toml:"poll_interval"`
	ApprovedStatus string        `toml:"approved_status"`
	RejectedStatus string        `toml:"rejected_status"`
}

func (c *Config) ApplyDefaults() {
	if c.IssueType == "" {
		c.IssueType = "Task"
	}
	if c.PollInterval == 0 {
		c.PollInterval = 30 * time.Second
	}
	if c.ApprovedStatus == "" {
		c.ApprovedStatus = "Approved"
	}
	if c.RejectedStatus == "" {
		c.RejectedStatus = "Rejected"
	}
}

func (c *Config) Validate() error {
	if c.URL == "" {
		return fmt.Errorf("url is required")
	}
	if c.Project == "" {
		return fmt.Errorf("project is required")
	}
	if c.Username == "" || c.APIToken == "" {
		return fmt.Errorf("username and api_token are required")
	}
	return nil
}

type Status int

const (
	StatusOpen Status = iota
	StatusApproved
	StatusRejected
)

func (s Status) String() string {
	switch s {
	case StatusApproved:
		return "approved"
	case StatusRejected:
		return "rejected"
	}
	return "open"
}

type TicketInput struct {
	RequestID   string
	User        string
	Roles       []string
	Reason      string
	Score       int
	Tier        string
	Components  map[string]int
	AIReasoning string
	Escalation  bool
}

// reqIDRE extracts a Teleport access-request ID from a Jira ticket
// description we created via buildDescription. The character class between
// the colon and the capture tolerates Jira wiki bold markers (e.g. the
// "*" that closes "*Teleport Request ID:*") as well as plain whitespace,
// so rehydration survives formatting tweaks to the description template.
var reqIDRE = regexp.MustCompile(`Teleport Request ID:[^A-Za-z0-9-]*([A-Za-z0-9-]+)`)

type Client struct {
	cfg  Config
	jira *gojira.Client
}

func New(cfg Config) (*Client, error) {
	tp := gojira.BasicAuthTransport{Username: cfg.Username, APIToken: cfg.APIToken}
	jc, err := gojira.NewClient(cfg.URL, tp.Client())
	if err != nil {
		return nil, err
	}
	return &Client{cfg: cfg, jira: jc}, nil
}

func (c *Client) CreateITSMTicket(ctx context.Context, in TicketInput) (string, error) {
	if c.cfg.Project == "" {
		return "", fmt.Errorf("jira project key is empty")
	}
	if c.cfg.IssueType == "" {
		return "", fmt.Errorf("jira issue type is empty")
	}
	if in.RequestID == "" {
		return "", fmt.Errorf("request id is empty")
	}

	labels := []string{"teleport", "teleport-access"}
	if in.Tier != "" {
		labels = append(labels, "tier-"+strings.ToLower(in.Tier))
	}
	if in.Escalation {
		labels = append(labels, "teleport-escalation")
	}

	summary := fmt.Sprintf("[teleport] Access request %s - %s", in.RequestID, in.Tier)
	if in.Escalation {
		summary = fmt.Sprintf("[teleport][escalation] Session escalation %s - %s", in.RequestID, in.Tier)
	}

	issue := &gojira.Issue{
		Fields: &gojira.IssueFields{
			Project:     gojira.Project{Key: c.cfg.Project},
			Type:        gojira.IssueType{Name: c.cfg.IssueType},
			Summary:     summary,
			Description: buildDescription(in),
			Labels:      labels,
		},
	}

	reqBody, err := json.MarshalIndent(issue, "", "  ")
	if err != nil {
		return "", fmt.Errorf("marshal jira issue: %w", err)
	}
	fmt.Println("Jira request body:")
	fmt.Println(string(reqBody))

	created, resp, err := c.jira.Issue.Create(ctx, issue)
	if err != nil {
		if resp != nil && resp.Body != nil {
			body, _ := io.ReadAll(resp.Body)
			fmt.Println("Jira error response:")
			fmt.Println(string(body))
		}
		return "", fmt.Errorf("create jira issue: %w", err)
	}

	if created == nil {
		return "", fmt.Errorf("jira returned nil issue without error")
	}

	return created.Key, nil
}

func (c *Client) TicketStatus(ctx context.Context, key string) (Status, error) {
	issue, _, err := c.jira.Issue.Get(ctx, key, nil)
	if err != nil {
		return StatusOpen, err
	}
	if issue.Fields == nil || issue.Fields.Status == nil {
		return StatusOpen, nil
	}
	switch issue.Fields.Status.Name {
	case c.cfg.ApprovedStatus:
		return StatusApproved, nil
	case c.cfg.RejectedStatus:
		return StatusRejected, nil
	}
	return StatusOpen, nil
}

func (c *Client) ListOpenTeleportTickets(ctx context.Context) (map[string]string, error) {
	jql := fmt.Sprintf(`project = %q AND labels = teleport AND status not in (%q, %q)`,
		c.cfg.Project, c.cfg.ApprovedStatus, c.cfg.RejectedStatus)
	issues, _, err := c.jira.Issue.Search(ctx, jql, nil)
	if err != nil {
		return nil, err
	}
	out := map[string]string{}
	for _, iss := range issues {
		if iss.Fields == nil {
			continue
		}
		m := c.reqIDRE.FindStringSubmatch(iss.Fields.Description)
		if len(m) < 2 {
			continue
		}
		out[m[1]] = iss.Key
	}
	return out, nil
}

func buildDescription(in TicketInput) string {
	var sb strings.Builder
	if in.Escalation {
		sb.WriteString("h2. Teleport Session Escalation Review\n\n")
		sb.WriteString("{panel:title=Session escalation}This request is a mid-session privilege bump — please prioritise review.{panel}\n\n")
	} else {
		sb.WriteString("h2. Teleport Access Request Review\n\n")
	}
	fmt.Fprintf(&sb, "* *Teleport Request ID:* %s\n", in.RequestID)
	fmt.Fprintf(&sb, "* *Requester:* %s\n", in.User)
	fmt.Fprintf(&sb, "* *Roles requested:* %s\n", strings.Join(in.Roles, ", "))
	fmt.Fprintf(&sb, "* *Risk score:* %d\n", in.Score)
	fmt.Fprintf(&sb, "* *Risk tier:* %s\n\n", in.Tier)

	sb.WriteString("h3. Request reason\n")
	sb.WriteString("{code}\n")
	sb.WriteString(in.Reason)
	sb.WriteString("\n{code}\n\n")

	if len(in.Components) > 0 {
		sb.WriteString("h3. Risk components\n")
		for k, v := range in.Components {
			fmt.Fprintf(&sb, "* %s: +%d\n", k, v)
		}
		sb.WriteString("\n")
	}

	if in.AIReasoning != "" {
		sb.WriteString("h3. AI reasoning\n")
		sb.WriteString("{quote}\n")
		sb.WriteString(in.AIReasoning)
		sb.WriteString("\n{quote}\n\n")
	}

	sb.WriteString("h3. SRE action required\n")
	sb.WriteString("Transition this ticket to the configured approved or rejected status to resolve the Teleport access request.\n")

	return sb.String()
}

-    m := c.reqIDRE.FindStringSubmatch(iss.Fields.Description)

+    m := reqIDRE.FindStringSubmatch(iss.Fields.Description)


Now tests that prove the fix — including the exact output `buildDescription` produces today.


Write

package jira

import (
	"strings"
	"testing"
)

// This is the regression test for the rehydration bug: the regex has to
// match request IDs inside the exact wiki markup buildDescription emits,
// including the "*" that closes the Jira bold on "*Teleport Request ID:*".
func TestReqIDRE_ExtractsFromBuildDescription(t *testing.T) {
	cases := []struct {
		name string
		in   TicketInput
		want string
	}{
		{
			name: "uuid-style id, plain ticket",
			in:   TicketInput{RequestID: "abc12345-1234-1234-1234-abcdef012345", Tier: "low"},
			want: "abc12345-1234-1234-1234-abcdef012345",
		},
		{
			name: "uuid-style id, escalation ticket",
			in:   TicketInput{RequestID: "abc12345-1234-1234-1234-abcdef012345", Tier: "high", Escalation: true},
			want: "abc12345-1234-1234-1234-abcdef012345",
		},
		{
			name: "short alphanumeric id",
			in:   TicketInput{RequestID: "req-987", Tier: "medium"},
			want: "req-987",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			desc := buildDescription(tc.in)
			m := reqIDRE.FindStringSubmatch(desc)
			if len(m) < 2 {
				t.Fatalf("regex did not match. description was:\n%s", desc)
			}
			if m[1] != tc.want {
				t.Errorf("captured %q, want %q", m[1], tc.want)
			}
		})
	}
}

// Defensive: ensure unstyled formatting still matches, so manual edits to
// the template (removing bold, adding whitespace) don't silently break
// rehydration again.
func TestReqIDRE_ExtractsFromAlternativeFormats(t *testing.T) {
	cases := map[string]string{
		"plain colon-space":    "Teleport Request ID: abc-123",
		"no space after colon": "Teleport Request ID:abc-123",
		"bold wiki markup":     "* *Teleport Request ID:* abc-123",
		"extra surrounding":    "blah\n* *Teleport Request ID:* abc-123 more text\nfoo",
	}
	for name, in := range cases {
		t.Run(name, func(t *testing.T) {
			m := reqIDRE.FindStringSubmatch(in)
			if len(m) < 2 || m[1] != "abc-123" {
				t.Errorf("got %v, want capture of %q in input %q", m, "abc-123", in)
			}
		})
	}
}

func TestReqIDRE_NoMatchWhenMarkerAbsent(t *testing.T) {
	cases := []string{
		"",
		"just some random text",
		"Teleport Access Request:\nabc-123", // similar but different label
	}
	for _, in := range cases {
		if m := reqIDRE.FindStringSubmatch(in); m != nil {
			t.Errorf("unexpected match on %q: %v", in, m)
		}
	}
}

// Sanity check that the emitted description still contains the exact
// marker string the regex anchors on, so drift in buildDescription trips
// this test before the field deployment.
func TestBuildDescription_ContainsMarker(t *testing.T) {
	desc := buildDescription(TicketInput{RequestID: "abc-123"})
	if !strings.Contains(desc, "Teleport Request ID:") {
		t.Errorf("buildDescription output missing the 'Teleport Request ID:' marker")
	}
}


File created successfully at: /Users/steve.briggs/projects/operations/apps/televibe/jira/itsm_test.go

package jira

import (
	"strings"
	"testing"
)

// This is the regression test for the rehydration bug: the regex has to
// match request IDs inside the exact wiki markup buildDescription emits,
// including the "*" that closes the Jira bold on "*Teleport Request ID:*".
func TestReqIDRE_ExtractsFromBuildDescription(t *testing.T) {
	cases := []struct {
		name string
		in   TicketInput
		want string
	}{
		{
			name: "uuid-style id, plain ticket",
			in:   TicketInput{RequestID: "abc12345-1234-1234-1234-abcdef012345", Tier: "low"},
			want: "abc12345-1234-1234-1234-abcdef012345",
		},
		{
			name: "uuid-style id, escalation ticket",
			in:   TicketInput{RequestID: "abc12345-1234-1234-1234-abcdef012345", Tier: "high", Escalation: true},
			want: "abc12345-1234-1234-1234-abcdef012345",
		},
		{
			name: "short alphanumeric id",
			in:   TicketInput{RequestID: "req-987", Tier: "medium"},
			want: "req-987",
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			desc := buildDescription(tc.in)
			m := reqIDRE.FindStringSubmatch(desc)
			if len(m) < 2 {
				t.Fatalf("regex did not match. description was:\n%s", desc)
			}
			if m[1] != tc.want {
				t.Errorf("captured %q, want %q", m[1], tc.want)
			}
		})
	}
}

// Defensive: ensure unstyled formatting still matches, so manual edits to
// the template (removing bold, adding whitespace) don't silently break
// rehydration again.
func TestReqIDRE_ExtractsFromAlternativeFormats(t *testing.T) {
	cases := map[string]string{
		"plain colon-space":    "Teleport Request ID: abc-123",
		"no space after colon": "Teleport Request ID:abc-123",
		"bold wiki markup":     "* *Teleport Request ID:* abc-123",
		"extra surrounding":    "blah\n* *Teleport Request ID:* abc-123 more text\nfoo",
	}
	for name, in := range cases {
		t.Run(name, func(t *testing.T) {
			m := reqIDRE.FindStringSubmatch(in)
			if len(m) < 2 || m[1] != "abc-123" {
				t.Errorf("got %v, want capture of %q in input %q", m, "abc-123", in)
			}
		})
	}
}

func TestReqIDRE_NoMatchWhenMarkerAbsent(t *testing.T) {
	cases := []string{
		"",
		"just some random text",
		"Teleport Access Request:\nabc-123", // similar but different label
	}
	for _, in := range cases {
		if m := reqIDRE.FindStringSubmatch(in); m != nil {
			t.Errorf("unexpected match on %q: %v", in, m)
		}
	}
}

// Sanity check that the emitted description still contains the exact
// marker string the regex anchors on, so drift in buildDescription trips
// this test before the field deployment.
func TestBuildDescription_ContainsMarker(t *testing.T) {
	desc := buildDescription(TicketInput{RequestID: "abc-123"})
	if !strings.Contains(desc, "Teleport Request ID:") {
		t.Errorf("buildDescription output missing the 'Teleport Request ID:' marker")
	}
}


Bash

go build ./... 2>&1; echo '---all tests---'; go test -race ./... 2>&1 | tail -10; echo '---jira tests verbose---'; go test -race -v ./jira/... 2>&1 | tail -30

Build and run all tests including the new jira tests verbose


---all tests---
ok  	televibe	2.239s
?   	televibe/ai	[no test files]
?   	televibe/cmd/submit	[no test files]
?   	televibe/cmd/televibe	[no test files]
ok  	televibe/jira	1.969s
ok  	televibe/reputation	(cached)
ok  	televibe/risk	(cached)
?   	televibe/telemetry	[no test files]
---jira tests verbose---
=== RUN   TestReqIDRE_ExtractsFromBuildDescription
=== RUN   TestReqIDRE_ExtractsFromBuildDescription/uuid-style_id,_plain_ticket
=== RUN   TestReqIDRE_ExtractsFromBuildDescription/uuid-style_id,_escalation_ticket
=== RUN   TestReqIDRE_ExtractsFromBuildDescription/short_alphanumeric_id
--- PASS: TestReqIDRE_ExtractsFromBuildDescription (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromBuildDescription/uuid-style_id,_plain_ticket (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromBuildDescription/uuid-style_id,_escalation_ticket (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromBuildDescription/short_alphanumeric_id (0.00s)
=== RUN   TestReqIDRE_ExtractsFromAlternativeFormats
=== RUN   TestReqIDRE_ExtractsFromAlternativeFormats/bold_wiki_markup
=== RUN   TestReqIDRE_ExtractsFromAlternativeFormats/extra_surrounding
=== RUN   TestReqIDRE_ExtractsFromAlternativeFormats/plain_colon-space
=== RUN   TestReqIDRE_ExtractsFromAlternativeFormats/no_space_after_colon
--- PASS: TestReqIDRE_ExtractsFromAlternativeFormats (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromAlternativeFormats/bold_wiki_markup (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromAlternativeFormats/extra_surrounding (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromAlternativeFormats/plain_colon-space (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromAlternativeFormats/no_space_after_colon (0.00s)
=== RUN   TestReqIDRE_NoMatchWhenMarkerAbsent
--- PASS: TestReqIDRE_NoMatchWhenMarkerAbsent (0.00s)
=== RUN   TestBuildDescription_ContainsMarker
--- PASS: TestBuildDescription_ContainsMarker (0.00s)
PASS
ok  	televibe/jira	1.274s


All green. Bugbot was dead right — the regex could not possibly match `buildDescription`'s output.

What changed:

**`jira/itsm.go`**
- Regex promoted to a package-level `var reqIDRE` (was a per-client field) with a doc comment explaining why the class is loose.
- New pattern: `Teleport Request ID:[^A-Za-z0-9-]*([A-Za-z0-9-]+)` — the character class between `ID:` and the capture now matches any run of characters that *aren't* valid ID characters, so it consumes the `*` closing-bold-marker, the space, or any future formatting tweak without needing a regex change.
- Removed the `reqIDRE` field from `Client` since it's no longer instance-state; `New()` simplified accordingly.
- `ListOpenTeleportTickets` now uses the package-level `reqIDRE`.

**New `jira/itsm_test.go`**
- `TestReqIDRE_ExtractsFromBuildDescription` is the actual regression test: it runs the *real* `buildDescription` function (the one that writes the wiki-bold `*Teleport Request ID:*` line) on several representative `TicketInput` fixtures (UUID id, short id, escalation flag on/off) and asserts the regex extracts the id correctly. This is exactly the scenario bugbot flagged — if someone tweaks the template and breaks extraction again, this test fires.
- `TestReqIDRE_ExtractsFromAlternativeFormats` covers plain colon-space, no-space-after-colon, the current bold markup, and the marker embedded in surrounding noise. Defends against future drift in either direction.
- `TestReqIDRE_NoMatchWhenMarkerAbsent` — empty string, unrelated text, and a near-miss ("Teleport Access Request:" instead of the right marker) all correctly return no match, so we don't accidentally start matching unrelated tickets.
- `TestBuildDescription_ContainsMarker` — cheap canary that fails if someone changes the template to use a different label.

Operational note, Sir Charles: any ITSM tickets that have been filed against the *currently deployed* buggy code won't be caught by rehydration on the next restart after this fix is shipped — the map-building logic works, but those tickets are still orphans from televibe's perspective. If you have live pending tickets, either resolve them before the deploy or plan on a one-time manual reconciliation. For the tickets filed *after* the fix ships, rehydration will work correctly.


bugbot also found this can you verify and fix the issue? Rehydration regex can't match bold-wrapped description field

High Severity

The reqIDRE regex (Teleport Request ID:\s*([A-Za-z0-9-]+)) expects optional whitespace after the colon, but buildDescription writes the line as * *Teleport Request ID:* %s using Jira wiki bold markup. In the raw stored text, a literal * (closing bold marker) and a space sit between ID: and the actual request ID. Since * is neither whitespace nor in [A-Za-z0-9-], the regex never matches, so ListOpenTeleportTickets always returns an empty map and restart rehydration silently fails — leading to duplicate ITSM tickets after any restart.
Additional Locations (1)

    apps/televibe/jira/itsm.go#L159-L160


Sir Charles — that's the same issue I addressed last turn. Let me quickly verify the fix is actually on disk and the tests are passing, just so neither of us is fooled by cached state.


grep -n "reqIDRE\|regexp.MustCompile" /Users/steve.briggs/projects/operations/apps/televibe/jira/itsm.go 2>&1; echo '---tests---'; go test -race -v ./jira/... 2>&1 | tail -20

Confirm fix is in place and tests pass


84:// reqIDRE extracts a Teleport access-request ID from a Jira ticket
89:var reqIDRE = regexp.MustCompile(`Teleport Request ID:[^A-Za-z0-9-]*([A-Za-z0-9-]+)`)
192:		m := reqIDRE.FindStringSubmatch(iss.Fields.Description)
---tests---
--- PASS: TestReqIDRE_ExtractsFromBuildDescription (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromBuildDescription/uuid-style_id,_plain_ticket (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromBuildDescription/uuid-style_id,_escalation_ticket (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromBuildDescription/short_alphanumeric_id (0.00s)
=== RUN   TestReqIDRE_ExtractsFromAlternativeFormats
=== RUN   TestReqIDRE_ExtractsFromAlternativeFormats/bold_wiki_markup
=== RUN   TestReqIDRE_ExtractsFromAlternativeFormats/extra_surrounding
=== RUN   TestReqIDRE_ExtractsFromAlternativeFormats/plain_colon-space
=== RUN   TestReqIDRE_ExtractsFromAlternativeFormats/no_space_after_colon
--- PASS: TestReqIDRE_ExtractsFromAlternativeFormats (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromAlternativeFormats/bold_wiki_markup (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromAlternativeFormats/extra_surrounding (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromAlternativeFormats/plain_colon-space (0.00s)
    --- PASS: TestReqIDRE_ExtractsFromAlternativeFormats/no_space_after_colon (0.00s)
=== RUN   TestReqIDRE_NoMatchWhenMarkerAbsent
--- PASS: TestReqIDRE_NoMatchWhenMarkerAbsent (0.00s)
=== RUN   TestBuildDescription_ContainsMarker
--- PASS: TestBuildDescription_ContainsMarker (0.00s)
PASS
ok  	televibe/jira	(cached)


Already fixed in the previous turn. Recap so you can see bugbot's concern is addressed:

- Old regex: `Teleport Request ID:\s*([A-Za-z0-9-]+)` — couldn't match because the `*` closing Jira bold was neither whitespace nor an ID char.
- **New regex** (itsm.go:89): `Teleport Request ID:[^A-Za-z0-9-]*([A-Za-z0-9-]+)` — the character class between `ID:` and the capture now swallows any run of non-ID characters, so it consumes the `*`, the space, or both.
- **Regression test** `TestReqIDRE_ExtractsFromBuildDescription` runs the real `buildDescription` and asserts the regex extracts the id back out — this is the exact pipeline bugbot said was broken.
- **Defensive tests** cover plain `ID: x`, `ID:x` (no space), bold-wrapped `*ID:* x`, and the marker embedded in surrounding noise, so future template tweaks can't silently break rehydration again.

If bugbot re-flagged this, it may be re-scanning an older commit. The fix is committed to the working tree; make sure the commit is pushed to the PR branch before kicking bugbot again.


sorry, i meant this finding; Rehydration regex can't match bold-wrapped description field

High Severity

The reqIDRE regex (Teleport Request ID:\s*([A-Za-z0-9-]+)) expects optional whitespace after the colon, but buildDescription writes the line as * *Teleport Request ID:* %s using Jira wiki bold markup. In the raw stored text, a literal * (closing bold marker) and a space sit between ID: and the actual request ID. Since * is neither whitespace nor in [A-Za-z0-9-], the regex never matches, so ListOpenTeleportTickets always returns an empty map and restart rehydration silently fails — leading to duplicate ITSM tickets after any restart.
Additional Locations (1)

    apps/televibe/jira/itsm.go#L159-L160


150				fmt.Println("Jira error response:")
151				fmt.Println(string(body))
152			}
153			return "", fmt.Errorf("create jira issue: %w", err)
154		}
155
156		if created == nil {
157			return "", fmt.Errorf("jira returned nil issue without error")
158		}
159
160		return created.Key, nil
161	}
162
163	func (c *Client) TicketStatus(ctx context.Context, key string) (Status, error) {
164		issue, _, err := c.jira.Issue.Get(ctx, key, nil)
165		if err != nil {
166			return StatusOpen, err
167		}
168		if issue.Fields == nil || issue.Fields.Status == nil {
169			return StatusOpen, nil
170		}
171		switch issue.Fields.Status.Name {
172		case c.cfg.ApprovedStatus:
173			return StatusApproved, nil
174		case c.cfg.RejectedStatus:
175			return StatusRejected, nil
176		}
177		return StatusOpen, nil
178	}
179

			fmt.Println("Jira error response:")
			fmt.Println(string(body))
		}
		return "", fmt.Errorf("create jira issue: %w", err)
	}

	if created == nil {
		return "", fmt.Errorf("jira returned nil issue without error")
	}

	return created.Key, nil
}

func (c *Client) TicketStatus(ctx context.Context, key string) (Status, error) {
	issue, _, err := c.jira.Issue.Get(ctx, key, nil)
	if err != nil {
		return StatusOpen, err
	}
	if issue.Fields == nil || issue.Fields.Status == nil {
		return StatusOpen, nil
	}
	switch issue.Fields.Status.Name {
	case c.cfg.ApprovedStatus:
		return StatusApproved, nil
	case c.cfg.RejectedStatus:
		return StatusRejected, nil
	}
	return StatusOpen, nil
}


Sir Charles — the text of the finding is byte-for-byte identical to the one we just fixed, so either bugbot re-flagged the same thing or you meant to paste a different one. Let me peek at the cited `itsm.go#L159-L160` just in case there's a second site I missed.


sorry, i meant this finding; Rehydration regex can't match bold-wrapped description field  High Severity  The reqIDRE regex (Teleport Request ID:\s*([A-Za-z0-9-]+)) expects optional whitespace after t…


No second site — lines 159-160 today are just `return created.Key, nil` after the issue-create call. Bugbot's "Additional Locations" link was pointing at pre-fix line numbers (the regex was at line 99 before I promoted it to a package var, which shifted everything).

So this is the same finding I fixed last turn. To confirm, the fix is live on disk:

- **itsm.go:89** — regex updated to `Teleport Request ID:[^A-Za-z0-9-]*([A-Za-z0-9-]+)`, tolerating the `*` that closes Jira bold.
- **itsm_test.go** — `TestReqIDRE_ExtractsFromBuildDescription` runs the real `buildDescription` output through the regex and asserts the ID extracts, so this specific regression cannot silently return.
- All jira tests pass under `-race`.

If bugbot is still flagging it, the likely culprit is it's scanning a commit that predates the fix. Two things worth checking before kicking it again:

1. `git status` / `git log` — confirm the jira changes are committed (not just in the working tree).
2. `git push` — make sure the commit made it to the PR branch bugbot is reading from.

Happy to stage + commit it for you if you want, just say the word and I'll do a single commit for all four bugbot fixes (race dedup, zero-value defaults, regex mismatch, plus the earlier escalation/bedrock/env/random work that's been piling up). Otherwise, if the real second finding is in a different bugbot comment, paste that and I'll dig in.


Concurrent persist calls can overwrite newer data with stale snapshot

Medium Severity

apply releases the write lock before calling persist, and persist snapshots the map under a read lock then performs file I/O with no lock held. Two concurrent apply calls can each snapshot and write: if the first caller (holding an older snapshot missing the second caller's changes) completes its os.Rename last, it silently overwrites the newer file. On next restart, the second caller's reputation changes are lost.
Additional Locations (1)

    apps/televibe/reputation/reputation.go#L163-L198


100	// the updated assessment. A component map with no bad signals is a no-op.
101	func (s *Store) Assess(ctx context.Context, user string, components map[string]int) (Assessment, error) {
102		return s.apply(ctx, user, s.penaltiesFor(components))
103	}
104
105	func (s *Store) penaltiesFor(components map[string]int) []Penalty {
106		penalties := make([]Penalty, 0, 2)
107		if _, ok := components["missing_custx_ticket"]; ok && s.cfg.PenaltyMissingCUSTX > 0 {
108			penalties = append(penalties, Penalty{Reason: "missing_custx_ticket", Amount: s.cfg.PenaltyMissingCUSTX})
109		}
110		if _, ok := components["thin_description"]; ok && s.cfg.PenaltyThinDescription > 0 {
111			penalties = append(penalties, Penalty{Reason: "thin_description", Amount: s.cfg.PenaltyThinDescription})
112		}
113		return penalties
114	}
115
116	func (s *Store) apply(ctx context.Context, user string, penalties []Penalty) (Assessment, error) {
117		s.mu.Lock()
118		prev, ok := s.rep[user]
119		if !ok {
120			prev = s.cfg.StartingScore
121		}
122		score := prev
123		for _, p := range penalties {
124			score -= p.Amount
125		}
126		if score < s.cfg.FloorScore {
127			score = s.cfg.FloorScore
128		}
129		if score > s.cfg.CeilingScore {
130			score = s.cfg.CeilingScore
131		}
132		s.rep[user] = score
133		s.mu.Unlock()
134
135		assessment := Assessment{Score: score, Previous: prev, Penalties: penalties}
136		if s.cfg.PersistPath == "" || len(penalties) == 0 {
137			return assessment, nil
138		}
139		if err := s.persist(ctx); err != nil {
140			return assessment, fmt.Errorf("persist reputation: %w", err)
141		}
142		return assessment, nil
143	}
144
145	func (s *Store) load() error {
146		data, err := os.ReadFile(s.cfg.PersistPath)
147		if err != nil {
148			if errors.Is(err, fs.ErrNotExist) {
149				return nil
150			}
151			return fmt.Errorf("read reputation state: %w", err)
152		}
153		if len(data) == 0 {
154			return nil
155		}
156		var rep map[string]int
157		if err := json.Unmarshal(data, &rep); err != nil {
158			return fmt.Errorf("parse reputation state: %w", err)
159		}
160		s.rep = rep
161		return nil
162	}
163
164	func (s *Store) persist(_ context.Context) error {
165		s.mu.RLock()
166		data, err := json.Marshal(s.rep)
167		s.mu.RUnlock()
168		if err != nil {
169			return err
170		}
171
172		dir := filepath.Dir(s.cfg.PersistPath)
173		if err := os.MkdirAll(dir, 0o755); err != nil {
174			return err
175		}
176		tmp, err := os.CreateTemp(dir, ".reputation.*.tmp")
177		if err != nil {
178			return err
179		}
180		tmpPath := tmp.Name()
181		defer os.Remove(tmpPath)
182
183		if _, err := tmp.Write(data); err != nil {
184			tmp.Close()
185			return err
186		}
187		if err := tmp.Chmod(0o600); err != nil {
188			tmp.Close()
189			return err
190		}
191		if err := tmp.Sync(); err != nil {
192			tmp.Close()
193			return err
194		}
195		if err := tmp.Close(); err != nil {
196			return err
197		}
198		return os.Rename(tmpPath, s.cfg.PersistPath)
199	}

// the updated assessment. A component map with no bad signals is a no-op.
func (s *Store) Assess(ctx context.Context, user string, components map[string]int) (Assessment, error) {
	return s.apply(ctx, user, s.penaltiesFor(components))
}

func (s *Store) penaltiesFor(components map[string]int) []Penalty {
	penalties := make([]Penalty, 0, 2)
	if _, ok := components["missing_custx_ticket"]; ok && s.cfg.PenaltyMissingCUSTX > 0 {
		penalties = append(penalties, Penalty{Reason: "missing_custx_ticket", Amount: s.cfg.PenaltyMissingCUSTX})
	}
	if _, ok := components["thin_description"]; ok && s.cfg.PenaltyThinDescription > 0 {
		penalties = append(penalties, Penalty{Reason: "thin_description", Amount: s.cfg.PenaltyThinDescription})
	}
	return penalties
}

func (s *Store) apply(ctx context.Context, user string, penalties []Penalty) (Assessment, error) {
	s.mu.Lock()
	prev, ok := s.rep[user]
	if !ok {
		prev = s.cfg.StartingScore
	}
	score := prev
	for _, p := range penalties {
		score -= p.Amount
	}
	if score < s.cfg.FloorScore {
		score = s.cfg.FloorScore
	}
	if score > s.cfg.CeilingScore {
		score = s.cfg.CeilingScore
	}
	s.rep[user] = score
	s.mu.Unlock()

	assessment := Assessment{Score: score, Previous: prev, Penalties: penalties}
	if s.cfg.PersistPath == "" || len(penalties) == 0 {
		return assessment, nil
	}
	if err := s.persist(ctx); err != nil {
		return assessment, fmt.Errorf("persist reputation: %w", err)
	}
	return assessment, nil
}

func (s *Store) load() error {
	data, err := os.ReadFile(s.cfg.PersistPath)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return nil
		}
		return fmt.Errorf("read reputation state: %w", err)
	}
	if len(data) == 0 {
		return nil
	}
	var rep map[string]int
	if err := json.Unmarshal(data, &rep); err != nil {
		return fmt.Errorf("parse reputation state: %w", err)
	}
	s.rep = rep
	return nil
}

func (s *Store) persist(_ context.Context) error {
	s.mu.RLock()
	data, err := json.Marshal(s.rep)
	s.mu.RUnlock()
	if err != nil {
		return err
	}

	dir := filepath.Dir(s.cfg.PersistPath)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	tmp, err := os.CreateTemp(dir, ".reputation.*.tmp")
	if err != nil {
		return err
	}
	tmpPath := tmp.Name()
	defer os.Remove(tmpPath)

	if _, err := tmp.Write(data); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Chmod(0o600); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Sync(); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	return os.Rename(tmpPath, s.cfg.PersistPath)
}


1	package reputation
2
3	import (
4		"context"
5		"os"
6		"path/filepath"
7		"testing"
8	)
9
10	func baseCfg(path string) Config {
11		c := Config{PersistPath: path, FloorScore: 0}
12		c.ApplyDefaults()
13		return c
14	}
15
16	func TestScore_DefaultsToStartingScore(t *testing.T) {
17		s, err := NewStore(baseCfg(""))
18		if err != nil {
19			t.Fatalf("NewStore: %v", err)
20		}
21		if got := s.Score("alice"); got != 100 {
22			t.Errorf("Score(alice) = %d, want 100", got)
23		}
24	}
25
26	func TestAssess_AppliesConfiguredPenalties(t *testing.T) {
27		cfg := baseCfg("")
28		cfg.PenaltyMissingCUSTX = 30
29		cfg.PenaltyThinDescription = 15
30		s, _ := NewStore(cfg)

package reputation

import (
	"context"
	"os"
	"path/filepath"
	"testing"
)

func baseCfg(path string) Config {
	c := Config{PersistPath: path, FloorScore: 0}
	c.ApplyDefaults()
	return c
}

func TestScore_DefaultsToStartingScore(t *testing.T) {
	s, err := NewStore(baseCfg(""))
	if err != nil {
		t.Fatalf("NewStore: %v", err)
	}
	if got := s.Score("alice"); got != 100 {
		t.Errorf("Score(alice) = %d, want 100", got)
	}
}

func TestAssess_AppliesConfiguredPenalties(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 30
	cfg.PenaltyThinDescription = 15
	s, _ := NewStore(cfg)


Concurrent persist calls can overwrite newer data with stale snapshot  Medium Severity  apply releases the write lock before calling persist, and persist snapshots the map under a read lock then perfo…


Bugbot's correct. Walking it through:

1. Goroutine A: `apply(alice)` acquires `mu.Lock`, writes `rep[alice]=90`, releases. Calls `persist()`.
2. A's persist: `mu.RLock`, marshals `{alice:90}`, `mu.RUnlock`. Starts writing tmp file.
3. Goroutine B: `apply(bob)` acquires `mu.Lock`, writes `rep[bob]=95`, releases. Calls `persist()`.
4. B's persist: `mu.RLock`, marshals `{alice:90, bob:95}`, `mu.RUnlock`. Starts writing tmp file.
5. If B's `os.Rename` completes first and A's completes second, disk ends up at `{alice:90}` — bob's update is lost even though the in-memory map is correct.

The race detector won't catch this — it's a filesystem ordering race, not a memory race. Only visible as silent data loss after a restart.

Fix: hold `mu` (write lock) across the whole persist so persists serialize. A dedicated `persistMu` would let concurrent reads proceed during I/O, but the throughput cost of doing it with the main lock is tiny (persist is <10ms on a small JSON file, `Assess` isn't a hot path), and one lock is easier to reason about than two.


Edit

func (s *Store) apply(ctx context.Context, user string, penalties []Penalty) (Assessment, error) {
	s.mu.Lock()
	prev, ok := s.rep[user]
	if !ok {
		prev = s.cfg.StartingScore
	}
	score := prev
	for _, p := range penalties {
		score -= p.Amount
	}
	if score < s.cfg.FloorScore {
		score = s.cfg.FloorScore
	}
	if score > s.cfg.CeilingScore {
		score = s.cfg.CeilingScore
	}
	s.rep[user] = score
	s.mu.Unlock()

	assessment := Assessment{Score: score, Previous: prev, Penalties: penalties}
	if s.cfg.PersistPath == "" || len(penalties) == 0 {
		return assessment, nil
	}
	if err := s.persist(ctx); err != nil {
		return assessment, fmt.Errorf("persist reputation: %w", err)
	}
	return assessment, nil
}

func (s *Store) apply(ctx context.Context, user string, penalties []Penalty) (Assessment, error) {
	// Holding the write lock across the persist serializes concurrent
	// apply calls. Without it, two in-flight persists can each snapshot
	// independently and the slower os.Rename can land last, silently
	// overwriting the newer on-disk state with a stale view.
	s.mu.Lock()
	defer s.mu.Unlock()

	prev, ok := s.rep[user]
	if !ok {
		prev = s.cfg.StartingScore
	}
	score := prev
	for _, p := range penalties {
		score -= p.Amount
	}
	if score < s.cfg.FloorScore {
		score = s.cfg.FloorScore
	}
	if score > s.cfg.CeilingScore {
		score = s.cfg.CeilingScore
	}
	s.rep[user] = score

	assessment := Assessment{Score: score, Previous: prev, Penalties: penalties}
	if s.cfg.PersistPath == "" || len(penalties) == 0 {
		return assessment, nil
	}
	if err := s.persistLocked(ctx); err != nil {
		return assessment, fmt.Errorf("persist reputation: %w", err)
	}
	return assessment, nil
}


The file /Users/steve.briggs/projects/operations/apps/televibe/reputation/reputation.go has been updated successfully.

func (s *Store) apply(ctx context.Context, user string, penalties []Penalty) (Assessment, error) {
	// Holding the write lock across the persist serializes concurrent
	// apply calls. Without it, two in-flight persists can each snapshot
	// independently and the slower os.Rename can land last, silently
	// overwriting the newer on-disk state with a stale view.
	s.mu.Lock()
	defer s.mu.Unlock()

	prev, ok := s.rep[user]
	if !ok {
		prev = s.cfg.StartingScore
	}
	score := prev
	for _, p := range penalties {
		score -= p.Amount
	}
	if score < s.cfg.FloorScore {
		score = s.cfg.FloorScore
	}
	if score > s.cfg.CeilingScore {
		score = s.cfg.CeilingScore
	}
	s.rep[user] = score

	assessment := Assessment{Score: score, Previous: prev, Penalties: penalties}
	if s.cfg.PersistPath == "" || len(penalties) == 0 {
		return assessment, nil
	}
	if err := s.persistLocked(ctx); err != nil {
		return assessment, fmt.Errorf("persist reputation: %w", err)
	}
	return assessment, nil
}

// Package reputation tracks per-user reputation scores derived from the
// quality of Teleport access requests. State is persisted to a local JSON
// file so no external database is required.
package reputation

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"sync"
)

type Config struct {
	PersistPath            string `toml:"persist_path"`
	StartingScore          int    `toml:"starting_score"`
	FloorScore             int    `toml:"floor_score"`
	CeilingScore           int    `toml:"ceiling_score"`
	PenaltyMissingCUSTX    int    `toml:"penalty_missing_custx"`
	PenaltyThinDescription int    `toml:"penalty_thin_description"`
}

func (c *Config) ApplyDefaults() {
	if c.StartingScore == 0 {
		c.StartingScore = 100
	}
	if c.CeilingScore == 0 {
		c.CeilingScore = 100
	}
	if c.PenaltyMissingCUSTX == 0 {
		c.PenaltyMissingCUSTX = 10
	}
	if c.PenaltyThinDescription == 0 {
		c.PenaltyThinDescription = 5
	}
}

func (c *Config) Validate() error {
	if c.FloorScore > c.CeilingScore {
		return fmt.Errorf("floor_score (%d) must be <= ceiling_score (%d)", c.FloorScore, c.CeilingScore)
	}
	if c.StartingScore < c.FloorScore || c.StartingScore > c.CeilingScore {
		return fmt.Errorf("starting_score (%d) must be within [%d, %d]", c.StartingScore, c.FloorScore, c.CeilingScore)
	}
	return nil
}

type Penalty struct {
	Reason string
	Amount int
}

type Assessment struct {
	Score     int
	Previous  int
	Penalties []Penalty
}

type Store struct {
	cfg Config
	mu  sync.RWMutex
	rep map[string]int
}

func NewStore(cfg Config) (*Store, error) {
	s := &Store{cfg: cfg, rep: make(map[string]int, 64)}
	if cfg.PersistPath != "" {
		if err := s.load(); err != nil {
			return nil, err
		}
	}
	return s, nil
}

// Score returns the current reputation for user, or StartingScore if unseen.
func (s *Store) Score(user string) int {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if v, ok := s.rep[user]; ok {
		return v
	}
	return s.cfg.StartingScore
}

// Range invokes fn for every tracked user under a read lock. Do not call
// back into the store from fn or you will deadlock.
func (s *Store) Range(fn func(user string, score int)) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for u, sc := range s.rep {
		fn(u, sc)
	}
}

// Assess translates risk components into configured penalties, docks the
// user's score, clamps to [floor, ceiling], persists to disk, and returns
// the updated assessment. A component map with no bad signals is a no-op.
func (s *Store) Assess(ctx context.Context, user string, components map[string]int) (Assessment, error) {
	return s.apply(ctx, user, s.penaltiesFor(components))
}

func (s *Store) penaltiesFor(components map[string]int) []Penalty {
	penalties := make([]Penalty, 0, 2)
	if _, ok := components["missing_custx_ticket"]; ok && s.cfg.PenaltyMissingCUSTX > 0 {
		penalties = append(penalties, Penalty{Reason: "missing_custx_ticket", Amount: s.cfg.PenaltyMissingCUSTX})
	}
	if _, ok := components["thin_description"]; ok && s.cfg.PenaltyThinDescription > 0 {
		penalties = append(penalties, Penalty{Reason: "thin_description", Amount: s.cfg.PenaltyThinDescription})
	}
	return penalties
}

func (s *Store) apply(ctx context.Context, user string, penalties []Penalty) (Assessment, error) {
	s.mu.Lock()
	prev, ok := s.rep[user]
	if !ok {
		prev = s.cfg.StartingScore
	}
	score := prev
	for _, p := range penalties {
		score -= p.Amount
	}
	if score < s.cfg.FloorScore {
		score = s.cfg.FloorScore
	}
	if score > s.cfg.CeilingScore {
		score = s.cfg.CeilingScore
	}
	s.rep[user] = score
	s.mu.Unlock()

	assessment := Assessment{Score: score, Previous: prev, Penalties: penalties}
	if s.cfg.PersistPath == "" || len(penalties) == 0 {
		return assessment, nil
	}
	if err := s.persist(ctx); err != nil {
		return assessment, fmt.Errorf("persist reputation: %w", err)
	}
	return assessment, nil
}

func (s *Store) load() error {
	data, err := os.ReadFile(s.cfg.PersistPath)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return nil
		}
		return fmt.Errorf("read reputation state: %w", err)
	}
	if len(data) == 0 {
		return nil
	}
	var rep map[string]int
	if err := json.Unmarshal(data, &rep); err != nil {
		return fmt.Errorf("parse reputation state: %w", err)
	}
	s.rep = rep
	return nil
}

func (s *Store) persist(_ context.Context) error {
	s.mu.RLock()
	data, err := json.Marshal(s.rep)
	s.mu.RUnlock()
	if err != nil {
		return err
	}

	dir := filepath.Dir(s.cfg.PersistPath)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	tmp, err := os.CreateTemp(dir, ".reputation.*.tmp")
	if err != nil {
		return err
	}
	tmpPath := tmp.Name()
	defer os.Remove(tmpPath)

	if _, err := tmp.Write(data); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Chmod(0o600); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Sync(); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	return os.Rename(tmpPath, s.cfg.PersistPath)
}

+  // Holding the write lock across the persist serializes concurrent

+  // apply calls. Without it, two in-flight persists can each snapshot

+  // independently and the slower os.Rename can land last, silently

+  // overwriting the newer on-disk state with a stale view.

-  if err := s.persist(ctx); err != nil {

+  if err := s.persistLocked(ctx); err != nil {


func (s *Store) persist(_ context.Context) error {
	s.mu.RLock()
	data, err := json.Marshal(s.rep)
	s.mu.RUnlock()
	if err != nil {
		return err
	}

// persistLocked writes the in-memory reputation map to disk via a
// tmp-file + atomic rename. The caller MUST hold s.mu (write lock);
// that's how we keep concurrent persists from racing on the rename.
func (s *Store) persistLocked(_ context.Context) error {
	data, err := json.Marshal(s.rep)
	if err != nil {
		return err
	}


// persistLocked writes the in-memory reputation map to disk via a
// tmp-file + atomic rename. The caller MUST hold s.mu (write lock);
// that's how we keep concurrent persists from racing on the rename.
func (s *Store) persistLocked(_ context.Context) error {
	data, err := json.Marshal(s.rep)
	if err != nil {
		return err
	}

// Package reputation tracks per-user reputation scores derived from the
// quality of Teleport access requests. State is persisted to a local JSON
// file so no external database is required.
package reputation

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"sync"
)

type Config struct {
	PersistPath            string `toml:"persist_path"`
	StartingScore          int    `toml:"starting_score"`
	FloorScore             int    `toml:"floor_score"`
	CeilingScore           int    `toml:"ceiling_score"`
	PenaltyMissingCUSTX    int    `toml:"penalty_missing_custx"`
	PenaltyThinDescription int    `toml:"penalty_thin_description"`
}

func (c *Config) ApplyDefaults() {
	if c.StartingScore == 0 {
		c.StartingScore = 100
	}
	if c.CeilingScore == 0 {
		c.CeilingScore = 100
	}
	if c.PenaltyMissingCUSTX == 0 {
		c.PenaltyMissingCUSTX = 10
	}
	if c.PenaltyThinDescription == 0 {
		c.PenaltyThinDescription = 5
	}
}

func (c *Config) Validate() error {
	if c.FloorScore > c.CeilingScore {
		return fmt.Errorf("floor_score (%d) must be <= ceiling_score (%d)", c.FloorScore, c.CeilingScore)
	}
	if c.StartingScore < c.FloorScore || c.StartingScore > c.CeilingScore {
		return fmt.Errorf("starting_score (%d) must be within [%d, %d]", c.StartingScore, c.FloorScore, c.CeilingScore)
	}
	return nil
}

type Penalty struct {
	Reason string
	Amount int
}

type Assessment struct {
	Score     int
	Previous  int
	Penalties []Penalty
}

type Store struct {
	cfg Config
	mu  sync.RWMutex
	rep map[string]int
}

func NewStore(cfg Config) (*Store, error) {
	s := &Store{cfg: cfg, rep: make(map[string]int, 64)}
	if cfg.PersistPath != "" {
		if err := s.load(); err != nil {
			return nil, err
		}
	}
	return s, nil
}

// Score returns the current reputation for user, or StartingScore if unseen.
func (s *Store) Score(user string) int {
	s.mu.RLock()
	defer s.mu.RUnlock()
	if v, ok := s.rep[user]; ok {
		return v
	}
	return s.cfg.StartingScore
}

// Range invokes fn for every tracked user under a read lock. Do not call
// back into the store from fn or you will deadlock.
func (s *Store) Range(fn func(user string, score int)) {
	s.mu.RLock()
	defer s.mu.RUnlock()
	for u, sc := range s.rep {
		fn(u, sc)
	}
}

// Assess translates risk components into configured penalties, docks the
// user's score, clamps to [floor, ceiling], persists to disk, and returns
// the updated assessment. A component map with no bad signals is a no-op.
func (s *Store) Assess(ctx context.Context, user string, components map[string]int) (Assessment, error) {
	return s.apply(ctx, user, s.penaltiesFor(components))
}

func (s *Store) penaltiesFor(components map[string]int) []Penalty {
	penalties := make([]Penalty, 0, 2)
	if _, ok := components["missing_custx_ticket"]; ok && s.cfg.PenaltyMissingCUSTX > 0 {
		penalties = append(penalties, Penalty{Reason: "missing_custx_ticket", Amount: s.cfg.PenaltyMissingCUSTX})
	}
	if _, ok := components["thin_description"]; ok && s.cfg.PenaltyThinDescription > 0 {
		penalties = append(penalties, Penalty{Reason: "thin_description", Amount: s.cfg.PenaltyThinDescription})
	}
	return penalties
}

func (s *Store) apply(ctx context.Context, user string, penalties []Penalty) (Assessment, error) {
	// Holding the write lock across the persist serializes concurrent
	// apply calls. Without it, two in-flight persists can each snapshot
	// independently and the slower os.Rename can land last, silently
	// overwriting the newer on-disk state with a stale view.
	s.mu.Lock()
	defer s.mu.Unlock()

	prev, ok := s.rep[user]
	if !ok {
		prev = s.cfg.StartingScore
	}
	score := prev
	for _, p := range penalties {
		score -= p.Amount
	}
	if score < s.cfg.FloorScore {
		score = s.cfg.FloorScore
	}
	if score > s.cfg.CeilingScore {
		score = s.cfg.CeilingScore
	}
	s.rep[user] = score

	assessment := Assessment{Score: score, Previous: prev, Penalties: penalties}
	if s.cfg.PersistPath == "" || len(penalties) == 0 {
		return assessment, nil
	}
	if err := s.persistLocked(ctx); err != nil {
		return assessment, fmt.Errorf("persist reputation: %w", err)
	}
	return assessment, nil
}

func (s *Store) load() error {
	data, err := os.ReadFile(s.cfg.PersistPath)
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return nil
		}
		return fmt.Errorf("read reputation state: %w", err)
	}
	if len(data) == 0 {
		return nil
	}
	var rep map[string]int
	if err := json.Unmarshal(data, &rep); err != nil {
		return fmt.Errorf("parse reputation state: %w", err)
	}
	s.rep = rep
	return nil
}

func (s *Store) persist(_ context.Context) error {
	s.mu.RLock()
	data, err := json.Marshal(s.rep)
	s.mu.RUnlock()
	if err != nil {
		return err
	}

	dir := filepath.Dir(s.cfg.PersistPath)
	if err := os.MkdirAll(dir, 0o755); err != nil {
		return err
	}
	tmp, err := os.CreateTemp(dir, ".reputation.*.tmp")
	if err != nil {
		return err
	}
	tmpPath := tmp.Name()
	defer os.Remove(tmpPath)

	if _, err := tmp.Write(data); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Chmod(0o600); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Sync(); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	return os.Rename(tmpPath, s.cfg.PersistPath)
}

-func (s *Store) persist(_ context.Context) error {

+// persistLocked writes the in-memory reputation map to disk via a

+// tmp-file + atomic rename. The caller MUST hold s.mu (write lock);

+// that's how we keep concurrent persists from racing on the rename.

+func (s *Store) persistLocked(_ context.Context) error {


Now the regression test — fire N concurrent `Assess` calls for distinct users, re-read the persisted file, assert all N updates are on disk.


155		cfg.StartingScore = 100
156		s, _ := NewStore(cfg)
157
158		a, _ := s.Assess(context.Background(), "alice", map[string]int{
159			"missing_custx_ticket": 1,
160			"thin_description":     1,
161		})
162		if a.Score != 95 {
163			t.Errorf("Score = %d, want 95 (only thin_description applied)", a.Score)
164		}
165		for _, p := range a.Penalties {
166			if p.Reason == "missing_custx_ticket" {
167				t.Errorf("missing_custx_ticket penalty should be skipped when amount = 0")
168			}
169		}
170	}
171

	cfg.StartingScore = 100
	s, _ := NewStore(cfg)

	a, _ := s.Assess(context.Background(), "alice", map[string]int{
		"missing_custx_ticket": 1,
		"thin_description":     1,
	})
	if a.Score != 95 {
		t.Errorf("Score = %d, want 95 (only thin_description applied)", a.Score)
	}
	for _, p := range a.Penalties {
		if p.Reason == "missing_custx_ticket" {
			t.Errorf("missing_custx_ticket penalty should be skipped when amount = 0")
		}
	}
}


package reputation

import (
	"context"
	"os"
	"path/filepath"
	"testing"
)

package reputation

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"testing"
)


The file /Users/steve.briggs/projects/operations/apps/televibe/reputation/reputation_test.go has been updated successfully.

package reputation

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"testing"
)

package reputation

import (
	"context"
	"os"
	"path/filepath"
	"testing"
)

func baseCfg(path string) Config {
	c := Config{PersistPath: path, FloorScore: 0}
	c.ApplyDefaults()
	return c
}

func TestScore_DefaultsToStartingScore(t *testing.T) {
	s, err := NewStore(baseCfg(""))
	if err != nil {
		t.Fatalf("NewStore: %v", err)
	}
	if got := s.Score("alice"); got != 100 {
		t.Errorf("Score(alice) = %d, want 100", got)
	}
}

func TestAssess_AppliesConfiguredPenalties(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 30
	cfg.PenaltyThinDescription = 15
	s, _ := NewStore(cfg)

	a, err := s.Assess(context.Background(), "alice", map[string]int{
		"missing_custx_ticket": 30,
		"thin_description":     15,
		"privilege_concern":    10, // not a reputation signal, ignored
	})
	if err != nil {
		t.Fatalf("Assess: %v", err)
	}
	if a.Previous != 100 {
		t.Errorf("Previous = %d, want 100", a.Previous)
	}
	if a.Score != 55 {
		t.Errorf("Score = %d, want 55", a.Score)
	}
	if len(a.Penalties) != 2 {
		t.Errorf("len(Penalties) = %d, want 2", len(a.Penalties))
	}
}

func TestAssess_ClampsToFloor(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 40
	s, _ := NewStore(cfg)
	ctx := context.Background()

	for i := 0; i < 5; i++ {
		if _, err := s.Assess(ctx, "alice", map[string]int{"missing_custx_ticket": 1}); err != nil {
			t.Fatalf("Assess: %v", err)
		}
	}
	if got := s.Score("alice"); got != 0 {
		t.Errorf("floored score = %d, want 0", got)
	}
}

func TestAssess_NoSignalsIsNoOp(t *testing.T) {
	s, _ := NewStore(baseCfg(""))
	a, err := s.Assess(context.Background(), "bob", map[string]int{"description_quality": 3})
	if err != nil {
		t.Fatalf("Assess: %v", err)
	}
	if a.Score != 100 || len(a.Penalties) != 0 {
		t.Errorf("unexpected assessment: %+v", a)
	}
}

func TestPersistence_RoundTrip(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "sub", "rep.json") // exercise MkdirAll

	cfg := baseCfg(path)
	cfg.PenaltyMissingCUSTX = 10
	s, err := NewStore(cfg)
	if err != nil {
		t.Fatalf("NewStore: %v", err)
	}
	if _, err := s.Assess(context.Background(), "alice", map[string]int{"missing_custx_ticket": 1}); err != nil {
		t.Fatalf("Assess: %v", err)
	}

	s2, err := NewStore(cfg)
	if err != nil {
		t.Fatalf("NewStore (reload): %v", err)
	}
	if got := s2.Score("alice"); got != 90 {
		t.Errorf("after reload, Score(alice) = %d, want 90", got)
	}
	if got := s2.Score("bob"); got != 100 {
		t.Errorf("unseen user Score(bob) = %d, want 100", got)
	}

	st, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat: %v", err)
	}
	if got := st.Mode().Perm(); got != 0o600 {
		t.Errorf("mode = %o, want 0600", got)
	}
}

func TestValidate(t *testing.T) {
	cases := []struct {
		name string
		cfg  Config
		ok   bool
	}{
		{"floor_above_ceiling", Config{FloorScore: 50, CeilingScore: 10, StartingScore: 20}, false},
		{"starting_out_of_range", Config{FloorScore: 0, CeilingScore: 100, StartingScore: 150}, false},
		{"ok", Config{FloorScore: 0, CeilingScore: 100, StartingScore: 50}, true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := tc.cfg.Validate()
			if (err == nil) != tc.ok {
				t.Errorf("Validate() err = %v, ok = %v", err, tc.ok)
			}
		})
	}
}

func TestRange_IteratesAllUsers(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 10
	cfg.PenaltyThinDescription = 5
	s, _ := NewStore(cfg)
	ctx := context.Background()
	_, _ = s.Assess(ctx, "alice", map[string]int{"missing_custx_ticket": 1})
	_, _ = s.Assess(ctx, "bob", map[string]int{"thin_description": 1})

	seen := map[string]int{}
	s.Range(func(u string, sc int) { seen[u] = sc })
	if len(seen) != 2 {
		t.Errorf("len(seen) = %d, want 2", len(seen))
	}
	if seen["alice"] != 90 || seen["bob"] != 95 {
		t.Errorf("unexpected seen: %+v", seen)
	}
}

func TestAssess_DisabledPenaltyIsSkipped(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 0 // explicit disable
	cfg.PenaltyThinDescription = 5
	cfg.StartingScore = 100
	s, _ := NewStore(cfg)

	a, _ := s.Assess(context.Background(), "alice", map[string]int{
		"missing_custx_ticket": 1,
		"thin_description":     1,
	})
	if a.Score != 95 {
		t.Errorf("Score = %d, want 95 (only thin_description applied)", a.Score)
	}
	for _, p := range a.Penalties {
		if p.Reason == "missing_custx_ticket" {
			t.Errorf("missing_custx_ticket penalty should be skipped when amount = 0")
		}
	}
}

import (
"context"
+  "encoding/json"
+  "fmt"
"os"
"path/filepath"
+  "sync"
"testing"
)


	for _, p := range a.Penalties {
		if p.Reason == "missing_custx_ticket" {
			t.Errorf("missing_custx_ticket penalty should be skipped when amount = 0")
		}
	}
}

	for _, p := range a.Penalties {
		if p.Reason == "missing_custx_ticket" {
			t.Errorf("missing_custx_ticket penalty should be skipped when amount = 0")
		}
	}
}

// Regression for the persist race: two in-flight persist calls could
// each snapshot independently and the slower os.Rename would land last,
// silently overwriting the newer state. This test fires N concurrent
// Assess calls for distinct users and asserts all N updates land on
// disk. Under the buggy implementation this fails intermittently
// because the final file is missing some users.
func TestAssess_ConcurrentPersistsDoNotDropUpdates(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "reputation.json")
	s, err := NewStore(baseCfg(path))
	if err != nil {
		t.Fatalf("NewStore: %v", err)
	}

	const N = 64
	var wg sync.WaitGroup
	wg.Add(N)
	for i := 0; i < N; i++ {
		go func(i int) {
			defer wg.Done()
			user := fmt.Sprintf("user-%d", i)
			if _, err := s.Assess(context.Background(), user, map[string]int{
				"missing_custx_ticket": 1,
			}); err != nil {
				t.Errorf("Assess(%s): %v", user, err)
			}
		}(i)
	}
	wg.Wait()

	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read persist file: %v", err)
	}
	var rep map[string]int
	if err := json.Unmarshal(data, &rep); err != nil {
		t.Fatalf("parse persisted state: %v", err)
	}
	if got := len(rep); got != N {
		t.Fatalf("disk state has %d users, want %d — concurrent persists dropped writes", got, N)
	}
	for i := 0; i < N; i++ {
		user := fmt.Sprintf("user-%d", i)
		if _, ok := rep[user]; !ok {
			t.Errorf("user %s missing from persisted state", user)
		}
	}
}


	for _, p := range a.Penalties {
		if p.Reason == "missing_custx_ticket" {
			t.Errorf("missing_custx_ticket penalty should be skipped when amount = 0")
		}
	}
}

// Regression for the persist race: two in-flight persist calls could
// each snapshot independently and the slower os.Rename would land last,
// silently overwriting the newer state. This test fires N concurrent
// Assess calls for distinct users and asserts all N updates land on
// disk. Under the buggy implementation this fails intermittently
// because the final file is missing some users.
func TestAssess_ConcurrentPersistsDoNotDropUpdates(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "reputation.json")
	s, err := NewStore(baseCfg(path))
	if err != nil {
		t.Fatalf("NewStore: %v", err)
	}

	const N = 64
	var wg sync.WaitGroup
	wg.Add(N)
	for i := 0; i < N; i++ {
		go func(i int) {
			defer wg.Done()
			user := fmt.Sprintf("user-%d", i)
			if _, err := s.Assess(context.Background(), user, map[string]int{
				"missing_custx_ticket": 1,
			}); err != nil {
				t.Errorf("Assess(%s): %v", user, err)
			}
		}(i)
	}
	wg.Wait()

	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read persist file: %v", err)
	}
	var rep map[string]int
	if err := json.Unmarshal(data, &rep); err != nil {
		t.Fatalf("parse persisted state: %v", err)
	}
	if got := len(rep); got != N {
		t.Fatalf("disk state has %d users, want %d — concurrent persists dropped writes", got, N)
	}
	for i := 0; i < N; i++ {
		user := fmt.Sprintf("user-%d", i)
		if _, ok := rep[user]; !ok {
			t.Errorf("user %s missing from persisted state", user)
		}
	}
}

package reputation

import (
	"context"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"sync"
	"testing"
)

func baseCfg(path string) Config {
	c := Config{PersistPath: path, FloorScore: 0}
	c.ApplyDefaults()
	return c
}

func TestScore_DefaultsToStartingScore(t *testing.T) {
	s, err := NewStore(baseCfg(""))
	if err != nil {
		t.Fatalf("NewStore: %v", err)
	}
	if got := s.Score("alice"); got != 100 {
		t.Errorf("Score(alice) = %d, want 100", got)
	}
}

func TestAssess_AppliesConfiguredPenalties(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 30
	cfg.PenaltyThinDescription = 15
	s, _ := NewStore(cfg)

	a, err := s.Assess(context.Background(), "alice", map[string]int{
		"missing_custx_ticket": 30,
		"thin_description":     15,
		"privilege_concern":    10, // not a reputation signal, ignored
	})
	if err != nil {
		t.Fatalf("Assess: %v", err)
	}
	if a.Previous != 100 {
		t.Errorf("Previous = %d, want 100", a.Previous)
	}
	if a.Score != 55 {
		t.Errorf("Score = %d, want 55", a.Score)
	}
	if len(a.Penalties) != 2 {
		t.Errorf("len(Penalties) = %d, want 2", len(a.Penalties))
	}
}

func TestAssess_ClampsToFloor(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 40
	s, _ := NewStore(cfg)
	ctx := context.Background()

	for i := 0; i < 5; i++ {
		if _, err := s.Assess(ctx, "alice", map[string]int{"missing_custx_ticket": 1}); err != nil {
			t.Fatalf("Assess: %v", err)
		}
	}
	if got := s.Score("alice"); got != 0 {
		t.Errorf("floored score = %d, want 0", got)
	}
}

func TestAssess_NoSignalsIsNoOp(t *testing.T) {
	s, _ := NewStore(baseCfg(""))
	a, err := s.Assess(context.Background(), "bob", map[string]int{"description_quality": 3})
	if err != nil {
		t.Fatalf("Assess: %v", err)
	}
	if a.Score != 100 || len(a.Penalties) != 0 {
		t.Errorf("unexpected assessment: %+v", a)
	}
}

func TestPersistence_RoundTrip(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "sub", "rep.json") // exercise MkdirAll

	cfg := baseCfg(path)
	cfg.PenaltyMissingCUSTX = 10
	s, err := NewStore(cfg)
	if err != nil {
		t.Fatalf("NewStore: %v", err)
	}
	if _, err := s.Assess(context.Background(), "alice", map[string]int{"missing_custx_ticket": 1}); err != nil {
		t.Fatalf("Assess: %v", err)
	}

	s2, err := NewStore(cfg)
	if err != nil {
		t.Fatalf("NewStore (reload): %v", err)
	}
	if got := s2.Score("alice"); got != 90 {
		t.Errorf("after reload, Score(alice) = %d, want 90", got)
	}
	if got := s2.Score("bob"); got != 100 {
		t.Errorf("unseen user Score(bob) = %d, want 100", got)
	}

	st, err := os.Stat(path)
	if err != nil {
		t.Fatalf("stat: %v", err)
	}
	if got := st.Mode().Perm(); got != 0o600 {
		t.Errorf("mode = %o, want 0600", got)
	}
}

func TestValidate(t *testing.T) {
	cases := []struct {
		name string
		cfg  Config
		ok   bool
	}{
		{"floor_above_ceiling", Config{FloorScore: 50, CeilingScore: 10, StartingScore: 20}, false},
		{"starting_out_of_range", Config{FloorScore: 0, CeilingScore: 100, StartingScore: 150}, false},
		{"ok", Config{FloorScore: 0, CeilingScore: 100, StartingScore: 50}, true},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			err := tc.cfg.Validate()
			if (err == nil) != tc.ok {
				t.Errorf("Validate() err = %v, ok = %v", err, tc.ok)
			}
		})
	}
}

func TestRange_IteratesAllUsers(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 10
	cfg.PenaltyThinDescription = 5
	s, _ := NewStore(cfg)
	ctx := context.Background()
	_, _ = s.Assess(ctx, "alice", map[string]int{"missing_custx_ticket": 1})
	_, _ = s.Assess(ctx, "bob", map[string]int{"thin_description": 1})

	seen := map[string]int{}
	s.Range(func(u string, sc int) { seen[u] = sc })
	if len(seen) != 2 {
		t.Errorf("len(seen) = %d, want 2", len(seen))
	}
	if seen["alice"] != 90 || seen["bob"] != 95 {
		t.Errorf("unexpected seen: %+v", seen)
	}
}

func TestAssess_DisabledPenaltyIsSkipped(t *testing.T) {
	cfg := baseCfg("")
	cfg.PenaltyMissingCUSTX = 0 // explicit disable
	cfg.PenaltyThinDescription = 5
	cfg.StartingScore = 100
	s, _ := NewStore(cfg)

	a, _ := s.Assess(context.Background(), "alice", map[string]int{
		"missing_custx_ticket": 1,
		"thin_description":     1,
	})
	if a.Score != 95 {
		t.Errorf("Score = %d, want 95 (only thin_description applied)", a.Score)
	}
	for _, p := range a.Penalties {
		if p.Reason == "missing_custx_ticket" {
			t.Errorf("missing_custx_ticket penalty should be skipped when amount = 0")
		}
	}
}

+// Regression for the persist race: two in-flight persist calls could

+// each snapshot independently and the slower os.Rename would land last,

+// silently overwriting the newer state. This test fires N concurrent

+// Assess calls for distinct users and asserts all N updates land on

+// disk. Under the buggy implementation this fails intermittently

+// because the final file is missing some users.

+func TestAssess_ConcurrentPersistsDoNotDropUpdates(t *testing.T) {

+  path := filepath.Join(dir, "reputation.json")

+      user := fmt.Sprintf("user-%d", i)

+      if _, err := s.Assess(context.Background(), user, map[string]int{

+        t.Errorf("Assess(%s): %v", user, err)

+    t.Fatalf("read persist file: %v", err)

+  if err := json.Unmarshal(data, &rep); err != nil {

+    t.Fatalf("parse persisted state: %v", err)

+    t.Fatalf("disk state has %d users, want %d — concurrent persists dropped writes", got, N)

+      t.Errorf("user %s missing from persisted state", user)


Bash

go build ./... 2>&1; echo '---all tests---'; go test -race ./... 2>&1 | tail -10; echo '---new test verbose---'; go test -race -v -count=5 ./reputation/... -run ConcurrentPersistsDoNotDropUpdates 2>&1 | tail -20

Build, run all tests, and run the new concurrency test 5x to check flakiness