package msglib import ( "context" "encoding/json" "errors" "fmt" "os" "strings" "testing" "time" ) func TestSelfCheckSendsProtocolEnvelopeAndDecodesResponse(t *testing.T) { client := newFakeClient(t, "self_check_ok", Config{}) got, err := client.SelfCheck(context.Background()) if err != nil { t.Fatalf("SelfCheck returned error: %v", err) } if got.SidecarName != "MsgLibReadSidecar" { t.Fatalf("SidecarName = %q, want MsgLibReadSidecar", got.SidecarName) } if got.Protocol != Protocol { t.Fatalf("Protocol = %q, want %q", got.Protocol, Protocol) } if got.ProcessBits != 32 || !got.ReadOnly || got.MutatesDB { t.Fatalf("unexpected safety fields: bits=%d read_only=%v mutates_db=%v", got.ProcessBits, got.ReadOnly, got.MutatesDB) } } func TestDisplaySourcesPassesConfiguredPathsAndDecodesMetadata(t *testing.T) { cfg := Config{ SQLiteDLLPath: `C:\fixture\System.Data.SQLite.dll`, DBPath: `C:\fixture\MsgLib.db`, Password: "123", } client := newFakeClient(t, "display_sources_ok", cfg) got, err := client.DisplaySources(context.Background()) if err != nil { t.Fatalf("DisplaySources returned error: %v", err) } if len(got) != 2 { t.Fatalf("len(DisplaySources) = %d, want 2", len(got)) } if got[0].Source != "contacts_roster" || got[0].Table != "TD_Roster" || !got[0].Available { t.Fatalf("unexpected first display source: %+v", got[0]) } if strings.Join(got[0].RequiredColumns, ",") != "UserJID,Name" { t.Fatalf("required columns = %#v", got[0].RequiredColumns) } if got[1].Source != "work_group_auth" || got[1].Available { t.Fatalf("unexpected second display source: %+v", got[1]) } } func TestDisplayEntitiesPassesBoundedArgsAndDecodesMetadata(t *testing.T) { cfg := Config{ SQLiteDLLPath: `C:\fixture\System.Data.SQLite.dll`, DBPath: `C:\fixture\MsgLib.db`, Password: "123", } client := newFakeClient(t, "display_entities_ok", cfg) got, err := client.DisplayEntities(context.Background(), DisplayEntitiesOptions{EntityType: "contacts", Query: "alice", Limit: 2}) if err != nil { t.Fatalf("DisplayEntities returned error: %v", err) } if len(got) != 2 { t.Fatalf("len(DisplayEntities) = %d, want 2", len(got)) } if got[0].EntityType != "contacts" || got[0].SourceTable != "TD_Roster" || got[0].JID != "alice@example" || got[0].DisplayName != "Alice" { t.Fatalf("unexpected first entity: %+v", got[0]) } if got[0].Confidence <= 0 || strings.Join(got[0].MatchedColumns, ",") != "JId,Nick" { t.Fatalf("unexpected first entity metadata: %+v", got[0]) } if got[1].SourceTable != "tblRecent" || got[1].JID != "recent@example" || got[1].DisplayName != "Recent Alice" { t.Fatalf("unexpected second entity: %+v", got[1]) } } func TestDisplayEntitiesRejectsUnsupportedEntityType(t *testing.T) { client := newFakeClient(t, "display_entities_ok", Config{}) _, err := client.DisplayEntities(context.Background(), DisplayEntitiesOptions{EntityType: "messages", Limit: 5}) if err == nil { t.Fatal("DisplayEntities returned nil error, want unsupported entity_type error") } if !strings.Contains(err.Error(), "entity_type") { t.Fatalf("DisplayEntities error = %v, want entity_type context", err) } } func TestClientReturnsStructuredSidecarError(t *testing.T) { client := newFakeClient(t, "sidecar_error", Config{}) _, err := client.SelfCheck(context.Background()) if err == nil { t.Fatal("SelfCheck returned nil error, want sidecar error") } var sidecarErr *SidecarError if !errors.As(err, &sidecarErr) { t.Fatalf("error %T %[1]v does not wrap *SidecarError", err) } if sidecarErr.Code != "DB_OPEN_FAILED" || !strings.Contains(sidecarErr.Message, "read-only open failed") { t.Fatalf("unexpected sidecar error: %+v", sidecarErr) } } func TestClientTimeoutStopsSidecar(t *testing.T) { client := newFakeClient(t, "hang", Config{Timeout: 150 * time.Millisecond}) _, err := client.SelfCheck(context.Background()) if err == nil { t.Fatal("SelfCheck returned nil error, want timeout") } if !errors.Is(err, context.DeadlineExceeded) && !strings.Contains(strings.ToLower(err.Error()), "deadline") { t.Fatalf("SelfCheck error = %v, want deadline exceeded", err) } } func TestConfigFromEnvReadsSidecarShape(t *testing.T) { t.Setenv("ISPHERE_MSGLIB_SIDECAR_EXE", `C:\tools\MsgLibReadSidecar.exe`) t.Setenv("ISPHERE_MSGLIB_SQLITE_DLL", `C:\tools\System.Data.SQLite.dll`) t.Setenv("ISPHERE_MSGLIB_DB", `C:\copy\MsgLib.db`) cfg, err := ConfigFromEnv() if err != nil { t.Fatalf("ConfigFromEnv returned error: %v", err) } if cfg.SidecarPath != `C:\tools\MsgLibReadSidecar.exe` { t.Fatalf("SidecarPath = %q", cfg.SidecarPath) } if cfg.SQLiteDLLPath != `C:\tools\System.Data.SQLite.dll` || cfg.DBPath != `C:\copy\MsgLib.db` { t.Fatalf("unexpected DB config: %+v", cfg) } if cfg.Password != "123" { t.Fatalf("Password = %q, want default 123", cfg.Password) } } func newFakeClient(t *testing.T, mode string, overrides Config) *Client { t.Helper() exe, err := os.Executable() if err != nil { t.Fatalf("os.Executable: %v", err) } cfg := overrides cfg.SidecarPath = exe cfg.SidecarArgs = []string{"-test.run=TestFakeSidecarProcess", "--"} cfg.ExtraEnv = append(cfg.ExtraEnv, "ISPHERE_MSGLIB_FAKE_SIDECAR=1", "ISPHERE_MSGLIB_FAKE_MODE="+mode) if cfg.Timeout == 0 { cfg.Timeout = 2 * time.Second } return NewClient(cfg) } func TestFakeSidecarProcess(t *testing.T) { if os.Getenv("ISPHERE_MSGLIB_FAKE_SIDECAR") != "1" { return } os.Exit(fakeSidecarMain()) } func fakeSidecarMain() int { mode := os.Getenv("ISPHERE_MSGLIB_FAKE_MODE") if mode == "hang" { time.Sleep(5 * time.Second) return 0 } var req map[string]any if err := json.NewDecoder(os.Stdin).Decode(&req); err != nil { fmt.Fprintf(os.Stderr, "decode request: %v\n", err) return 2 } protocol, _ := req["protocol"].(string) op, _ := req["op"].(string) requestID, _ := req["request_id"].(string) if protocol != Protocol || op == "" || requestID == "" { fmt.Fprintf(os.Stderr, "bad envelope: protocol=%q op=%q request_id=%q\n", protocol, op, requestID) return 2 } if mode == "sidecar_error" { return writeFakeResponse(map[string]any{ "protocol": Protocol, "request_id": requestID, "op": op, "ok": false, "data": nil, "error": map[string]any{ "code": "DB_OPEN_FAILED", "message": "read-only open failed", }, "warnings": []any{}, }) } switch op { case "self_check": return writeFakeResponse(map[string]any{ "protocol": Protocol, "request_id": requestID, "op": op, "ok": true, "data": map[string]any{ "sidecar_name": "MsgLibReadSidecar", "sidecar_version": "0.1.0", "protocol": Protocol, "process_bits": 32, "read_only": true, "mutates_db": false, }, "error": nil, "warnings": []any{}, }) case "display_sources": args, _ := req["args"].(map[string]any) if args["sqlite_dll_path"] != `C:\fixture\System.Data.SQLite.dll` || args["db_path"] != `C:\fixture\MsgLib.db` || args["password"] != "123" { fmt.Fprintf(os.Stderr, "bad display_sources args: %#v\n", args) return 2 } return writeFakeResponse(map[string]any{ "protocol": Protocol, "request_id": requestID, "op": op, "ok": true, "data": map[string]any{ "metadata_only": true, "read_only": true, "message_body_values_returned": false, "display_sources": []any{ map[string]any{"source": "contacts_roster", "table": "TD_Roster", "available": true, "required_columns": []any{"UserJID", "Name"}, "present_columns": []any{"UserJID", "Name", "Mobile"}}, map[string]any{"source": "work_group_auth", "table": "TD_WorkGroupAuth", "available": false, "required_columns": []any{"GroupJID", "GroupName"}, "present_columns": []any{"GroupJID"}}, }, }, "error": nil, "warnings": []any{}, }) case "display_entities": args, _ := req["args"].(map[string]any) if args["sqlite_dll_path"] != `C:\fixture\System.Data.SQLite.dll` || args["db_path"] != `C:\fixture\MsgLib.db` || args["password"] != "123" || args["entity_type"] != "contacts" || args["query"] != "alice" || args["limit"] != float64(2) { fmt.Fprintf(os.Stderr, "bad display_entities args: %#v\n", args) return 2 } return writeFakeResponse(map[string]any{ "protocol": Protocol, "request_id": requestID, "op": op, "ok": true, "data": map[string]any{ "metadata_only": true, "read_only": true, "entity_type": "contacts", "limit": 2, "result_count": 2, "entities": []any{ map[string]any{"entity_type": "contacts", "source_table": "TD_Roster", "jid": "alice@example", "display_name": "Alice", "confidence": 0.95, "matched_columns": []any{"JId", "Nick"}}, map[string]any{"entity_type": "contacts", "source_table": "tblRecent", "jid": "recent@example", "display_name": "Recent Alice", "confidence": 0.70, "matched_columns": []any{"PersonId", "PersonName"}}, }, }, "error": nil, "warnings": []any{}, }) default: fmt.Fprintf(os.Stderr, "unexpected op: %s\n", op) return 2 } } func writeFakeResponse(resp map[string]any) int { if err := json.NewEncoder(os.Stdout).Encode(resp); err != nil { fmt.Fprintf(os.Stderr, "encode response: %v\n", err) return 2 } return 0 }