refactor: chats method shebangs; drop chats index (D14). (#11)
Tests / Test (push) Skipped
Tests / Release (semver) (push) Skipped
Tests / Test (push) Skipped
Tests / Release (semver) (push) Skipped
Parsers and commands live in internal/chats. bin/chats/{sync,import,facts,apply}.go
are tagged shebang mains. Brain ingest is not a chats command.
This commit is contained in:
committed by
GitHub
co-authored by
GitHub
parent
46310f8773
commit
cec0161ff6
@@ -0,0 +1,215 @@
|
||||
package chats
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func readFixture(t *testing.T, name string) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(filepath.Join("testdata", name))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// TestParseLinkedInInbox verifies get_inbox parsing against the v4.22 wire
|
||||
// format (testdata/linkedin_inbox.json — synthetic Alice/Bob/Charlie).
|
||||
func TestParseLinkedInInbox(t *testing.T) {
|
||||
text := readFixture(t, "linkedin_inbox.json")
|
||||
items := parseLinkedInInbox(text)
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("expected 2 conversations (empty thread url skipped), got %d", len(items))
|
||||
}
|
||||
|
||||
first := items[0]
|
||||
if first.ThreadID == "" {
|
||||
t.Error("expected thread id extracted from reference url")
|
||||
}
|
||||
if first.Participants != "Alice Example" {
|
||||
t.Errorf("participants=%q, want Alice Example", first.Participants)
|
||||
}
|
||||
if !strings.HasPrefix(first.ThreadID, "2-") {
|
||||
t.Errorf("unexpected thread id format %q", first.ThreadID)
|
||||
}
|
||||
if items[1].Participants != "Charlie Example" {
|
||||
t.Errorf("second participant=%q, want Charlie Example", items[1].Participants)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseLinkedInInboxBadJSON verifies a non-JSON response yields no items
|
||||
// rather than a panic or error.
|
||||
func TestParseLinkedInInboxBadJSON(t *testing.T) {
|
||||
if got := parseLinkedInInbox("Session expired"); len(got) != 0 {
|
||||
t.Fatalf("expected no items for non-JSON, got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseLinkedInConversation verifies message extraction from the sections
|
||||
// blob (testdata/linkedin_conversation.json — synthetic Alice/Bob).
|
||||
func TestParseLinkedInConversation(t *testing.T) {
|
||||
text := readFixture(t, "linkedin_conversation.json")
|
||||
msgs := parseLinkedInConversation(text)
|
||||
if len(msgs) != 2 {
|
||||
t.Fatalf("expected 2 messages, got %d", len(msgs))
|
||||
}
|
||||
|
||||
if msgs[0].From != "Alice Example" {
|
||||
t.Errorf("from=%q, want Alice Example", msgs[0].From)
|
||||
}
|
||||
if !strings.Contains(msgs[0].Text, "Senior Software Engineer") {
|
||||
t.Errorf("alice text missing role, got %q", msgs[0].Text)
|
||||
}
|
||||
if msgs[0].Date == "" {
|
||||
t.Error("expected message date")
|
||||
}
|
||||
if msgs[1].From != "Bob Example" {
|
||||
t.Errorf("from=%q, want Bob Example", msgs[1].From)
|
||||
}
|
||||
}
|
||||
|
||||
// TestParseLinkedInConversationEmpty verifies empty/non-JSON blobs parse to
|
||||
// zero messages.
|
||||
func TestParseLinkedInConversationEmpty(t *testing.T) {
|
||||
if got := parseLinkedInConversation("no data here"); len(got) != 0 {
|
||||
t.Fatalf("expected 0 messages, got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
// TestLinkedInTimestamp verifies weekday+clock resolution to a recent UTC date.
|
||||
func TestLinkedInTimestamp(t *testing.T) {
|
||||
// The most recent Wednesday before/equal to "now".
|
||||
ts := linkedInTimestamp("WEDNESDAY", "10:02 AM")
|
||||
parsed, err := time.Parse(time.RFC3339, ts)
|
||||
if err != nil {
|
||||
t.Fatalf("unparseable timestamp %q: %v", ts, err)
|
||||
}
|
||||
if parsed.Weekday() != time.Wednesday {
|
||||
t.Errorf("expected Wednesday, got %s", parsed.Weekday())
|
||||
}
|
||||
if parsed.Hour() != 10 || parsed.Minute() != 2 {
|
||||
t.Errorf("expected 10:02, got %02d:%02d", parsed.Hour(), parsed.Minute())
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
diff := now.Sub(parsed)
|
||||
if diff < 0 || diff > 7*24*time.Hour {
|
||||
t.Errorf("timestamp %s is not within the last week of %s", parsed, now)
|
||||
}
|
||||
|
||||
if got := linkedInTimestamp("MONDAY", "garbage"); got != "" {
|
||||
t.Errorf("expected empty for bad clock, got %q", got)
|
||||
}
|
||||
if got := linkedInTimestamp("", "1:22 PM"); got != "13:22" {
|
||||
t.Errorf("expected bare 13:22 for missing weekday, got %q", got)
|
||||
}
|
||||
|
||||
// Relative day headers must resolve to full dates, not bare clocks.
|
||||
today := linkedInTimestamp("TODAY", "9:42 AM")
|
||||
yp, err := time.Parse(time.RFC3339, today)
|
||||
if err != nil {
|
||||
t.Fatalf("TODAY unparseable %q: %v", today, err)
|
||||
}
|
||||
if yp.Year() != now.Year() || yp.Month() != now.Month() || yp.Day() != now.Day() {
|
||||
t.Errorf("TODAY expected %v, got %v", now, yp)
|
||||
}
|
||||
yest := linkedInTimestamp("YESTERDAY", "3:00 PM")
|
||||
yp, err = time.Parse(time.RFC3339, yest)
|
||||
if err != nil {
|
||||
t.Fatalf("YESTERDAY unparseable %q: %v", yest, err)
|
||||
}
|
||||
if yp.Day() != now.AddDate(0, 0, -1).Day() {
|
||||
t.Errorf("YESTERDAY expected day %d, got %d", now.AddDate(0, 0, -1).Day(), yp.Day())
|
||||
}
|
||||
|
||||
// MON DD header (e.g. "JUN 25"): must resolve to a full date. The
|
||||
// timestamp should fall within the current year (falling back to the
|
||||
// prior year if the date would be in the future).
|
||||
md := linkedInTimestamp("JUN 25", "10:48 AM")
|
||||
mp, err := time.Parse(time.RFC3339, md)
|
||||
if err != nil {
|
||||
t.Fatalf("MON DD unparseable %q: %v", md, err)
|
||||
}
|
||||
if mp.Year() != now.Year() && mp.Year() != now.Year()-1 {
|
||||
t.Errorf("JUN 25 expected year %d or %d, got %d", now.Year(), now.Year()-1, mp.Year())
|
||||
}
|
||||
if mp.Month() != time.June || mp.Day() != 25 {
|
||||
t.Errorf("JUN 25 expected Jun 25, got %s %d", mp.Month(), mp.Day())
|
||||
}
|
||||
if mp.After(now) {
|
||||
t.Errorf("JUN 25 resolved to the future: %s > %s", mp, now)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTransientLinkedInError verifies rate-limit errors are retryable but
|
||||
// genuine failures are not.
|
||||
func TestTransientLinkedInError(t *testing.T) {
|
||||
retryable := []string{
|
||||
"get_conversation error: Error calling tool 'get_conversation'",
|
||||
"get_conversation error: Unexpected error in get_conversation: net::ERR_HTTP_RESPONSE_CODE_FAILURE",
|
||||
"rpc error 503: rate limited",
|
||||
"rpc error 429: too many requests",
|
||||
"get_conversation: get_conversation error: This server still has a browser open on the profile.",
|
||||
}
|
||||
for _, msg := range retryable {
|
||||
if !isTransientLinkedInError(msg) {
|
||||
t.Errorf("expected %q to be transient", msg)
|
||||
}
|
||||
}
|
||||
permanent := []string{
|
||||
"get_inbox error: bad credentials",
|
||||
"rpc error -32602: Invalid request parameters",
|
||||
"unmarshal: unexpected end of JSON input",
|
||||
}
|
||||
for _, msg := range permanent {
|
||||
if isTransientLinkedInError(msg) {
|
||||
t.Errorf("expected %q to be permanent", msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMsgLimitFor verifies the per-conversation message cap resolution.
|
||||
func TestMsgLimitFor(t *testing.T) {
|
||||
if got := msgLimitFor(0); got != 100 {
|
||||
t.Errorf("expected default 100, got %d", got)
|
||||
}
|
||||
if got := msgLimitFor(5); got != 5 {
|
||||
t.Errorf("expected 5, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWedged verifies the browser-open failure is recognized as a wedge.
|
||||
func TestWedged(t *testing.T) {
|
||||
if !wedged(errors.New("get_conversation error: This server still has a browser open on the profile")) {
|
||||
t.Error("expected wedged error to be recognized")
|
||||
}
|
||||
if wedged(errors.New("get_conversation error: bad thing")) {
|
||||
t.Error("unexpected wedge detection")
|
||||
}
|
||||
}
|
||||
|
||||
// TestThreadIDFromURL verifies thread id extraction.
|
||||
func TestThreadIDFromURL(t *testing.T) {
|
||||
cases := []struct {
|
||||
url, want string
|
||||
}{
|
||||
{"/messaging/thread/2-abc123/", "2-abc123"},
|
||||
{"/messaging/thread/2-abc123", "2-abc123"},
|
||||
{"", ""},
|
||||
{"/messaging/thread/", ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := threadIDFromURL(c.url)
|
||||
if c.want == "" && validThreadID(got) {
|
||||
t.Errorf("threadIDFromURL(%q) = %q, want empty", c.url, got)
|
||||
}
|
||||
if c.want != "" && got != c.want {
|
||||
t.Errorf("threadIDFromURL(%q) = %q, want %q", c.url, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user