package sessionread import ( "context" "database/sql" "io" "log/slog" "strconv" "testing" "time" "git.yeij.top/AskaEth/Cyrene_v2/internal/db" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" ) // testLogger returns a logger that writes nowhere, keeping test output clean. func testLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, nil)) } // testLocation pins the rendering timezone so a timestamp assertion is stable // regardless of the machine's TZ. var testLocation = time.UTC // at builds a millisecond timestamp from the readable wall clock the assertions // use, so an expected line is written the same way a reader thinks about it. func at(hour, minute int) int64 { return time.Date(2026, 10, 3, hour, minute, 0, 0, testLocation).UnixMilli() } // itoa renders a millisecond cursor the way the material does. func itoa(ms int64) string { return strconv.FormatInt(ms, 10) } // newTestStore opens an isolated in-memory history store. The *sql.DB comes back // too so a read-only assertion can inspect the tables directly. func newTestStore(t *testing.T) (*history.Store, *sql.DB) { t.Helper() ctx := context.Background() conn, err := db.Open(ctx, ":memory:", testLogger()) if err != nil { t.Fatalf("open memory db: %v", err) } t.Cleanup(func() { _ = conn.Close() }) st, err := history.NewStore(ctx, conn, testLogger()) if err != nil { t.Fatalf("new store: %v", err) } return st, conn } // testPersonas is the role-name double: it knows two roles and one default. func testPersonas() PersonaNamer { return fakePersonas{ names: map[string]string{"yuki": "雪", "cyrene": "昔涟"}, def: "cyrene", } } // fakePersonas is a read-only PersonaNamer with a configurable map. type fakePersonas struct { names map[string]string def string } // PersonaName implements PersonaNamer. func (f fakePersonas) PersonaName(id string) (string, bool) { name, ok := f.names[id] return name, ok } // DefaultPersonaID implements PersonaNamer. func (f fakePersonas) DefaultPersonaID() string { return f.def } // fakeHistory is the read-only test double: it implements exactly the two read // calls the package depends on and nothing else. There is no append, no delete and // no read-cursor method for the code under test to reach, which is how the // read-only guarantee of design/22 §8.1 约束 1 becomes a type-level property // instead of a promise (see TestReaderExposesNoWriteMethod). type fakeHistory struct { sessions []history.SessionMeta msgs map[string][]history.Message // newest first, like the store listErr error sessionsErr error // observed arguments of the last List call, for the pagination assertions. listBefore int64 listLimit int listCalls int } // List implements HistoryReader with the store's semantics: newest first, strictly // older than before, at most limit rows. func (f *fakeHistory) List(_ context.Context, sessionID string, before int64, limit int, _ string) ([]history.Message, error) { f.listBefore, f.listLimit = before, limit f.listCalls++ if f.listErr != nil { return nil, f.listErr } out := make([]history.Message, 0, limit) for _, m := range f.msgs[sessionID] { if before > 0 && m.CreatedAt >= before { continue } if len(out) == limit { break } out = append(out, m) } return out, nil } // ListSessions implements HistoryReader. func (f *fakeHistory) ListSessions(_ context.Context) ([]history.SessionMeta, error) { if f.sessionsErr != nil { return nil, f.sessionsErr } return f.sessions, nil } // Compile-time assertions: the real store must satisfy the read-only view this // package holds (the composition root injects it), and the read-only double must // satisfy it too. var ( _ HistoryReader = (*history.Store)(nil) _ HistoryReader = (*fakeHistory)(nil) ) // newReader builds a Reader over h with the test doubles the assertions expect. func newReader(h HistoryReader, p PersonaNamer) *Tool { return New(WithHistory(h), WithPersonas(p), WithLocation(testLocation), WithLogger(testLogger())) }