package ai import ( "errors" "testing" ) func TestExtractJSON(t *testing.T) { cases := []struct { name, in, want string wantErr bool }{ {"bare", `{"a":1}`, `{"a":1}`, false}, {"prose around", "sure, here:\n{\"a\":1}\nhope that helps", `{"a":1}`, false}, {"code fence", "```json\n{\"a\":1}\n```", `{"a":1}`, false}, {"nested", `{"a":{"b":2},"c":3}`, `{"a":{"b":2},"c":3}`, false}, {"brace in string", `{"a":"}{"}`, `{"a":"}{"}`, false}, {"first of two", `{"a":1} then {"b":2}`, `{"a":1}`, false}, {"none", `no json here`, "", true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, err := extractJSON(tc.in) if tc.wantErr { if err == nil { t.Fatalf("want error, got %q", got) } return } if err != nil { t.Fatalf("unexpected err: %v", err) } if got != tc.want { t.Fatalf("got %q want %q", got, tc.want) } }) } } func TestParseProposal(t *testing.T) { good := `{"next_action":"Draft summary","success_condition":"3 paragraphs written","timebox_minutes":25}` p, err := parseProposal(good) if err != nil { t.Fatalf("good parse: %v", err) } if p.NextAction != "Draft summary" || p.SuccessCondition != "3 paragraphs written" { t.Fatalf("fields wrong: %+v", p) } if p.TimeboxSecs != 25*60 { t.Fatalf("timebox secs got %d want %d", p.TimeboxSecs, 25*60) } if _, err := parseProposal(""); !errors.Is(err, ErrEmptyResponse) { t.Fatalf("empty: want ErrEmptyResponse, got %v", err) } if _, err := parseProposal("no json"); !errors.Is(err, ErrNoJSON) { t.Fatalf("no json: want ErrNoJSON, got %v", err) } bad := []string{ `{"next_action":"","success_condition":"x","timebox_minutes":25}`, `{"next_action":"x","success_condition":" ","timebox_minutes":25}`, `{"next_action":"x","success_condition":"y","timebox_minutes":0}`, `{"next_action":"x","success_condition":"y","timebox_minutes":-5}`, `{"next_action":"x"`, } for _, in := range bad { if _, err := parseProposal(in); !errors.Is(err, ErrInvalidProposal) { t.Fatalf("bad %q: want ErrInvalidProposal, got %v", in, err) } } } func TestParseProposalReadsAllowedClasses(t *testing.T) { p, err := parseProposal(`{"next_action":"a","success_condition":"b","timebox_minutes":20,"allowed_window_classes":["code"," firefox ",""]}`) if err != nil { t.Fatalf("parse: %v", err) } if len(p.AllowedWindowClasses) != 2 || p.AllowedWindowClasses[0] != "code" || p.AllowedWindowClasses[1] != "firefox" { t.Fatalf("classes wrong (blanks should drop, trim applied): %#v", p.AllowedWindowClasses) } } func TestParseProposalAllowedClassesOptional(t *testing.T) { p, err := parseProposal(`{"next_action":"a","success_condition":"b","timebox_minutes":20}`) if err != nil { t.Fatalf("parse: %v", err) } if len(p.AllowedWindowClasses) != 0 { t.Fatalf("absent field should yield empty slice, got %#v", p.AllowedWindowClasses) } }