diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-18 07:45:37 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-18 07:45:37 +0300 |
| commit | 4ffb22e7f69f1c9c79b095d4e60bad3d97aac55b (patch) | |
| tree | 2dc708cbb95975d34084eb5afd376871caa13e27 /internal/hexaicli/cache.go | |
| parent | ece7dcfd232b780f5650326c8ac2379ca70387d4 (diff) | |
ik0 replace test seams with dependency injection
Diffstat (limited to 'internal/hexaicli/cache.go')
| -rw-r--r-- | internal/hexaicli/cache.go | 57 |
1 files changed, 52 insertions, 5 deletions
diff --git a/internal/hexaicli/cache.go b/internal/hexaicli/cache.go index 544eab0..742ffce 100644 --- a/internal/hexaicli/cache.go +++ b/internal/hexaicli/cache.go @@ -1,6 +1,7 @@ package hexaicli import ( + "context" "crypto/sha256" "encoding/hex" "encoding/json" @@ -14,7 +15,38 @@ import ( const cliResponseCacheTTL = 24 * time.Hour -var nowCLIResponseCache = time.Now +// responseCache carries the injectable dependencies for the on-disk CLI +// response cache. The only dependency is the clock used to stamp entries and +// decide expiry. Production code uses defaultResponseCache (backed by +// time.Now); tests construct a responseCache with a fake clock to exercise TTL +// expiry without sleeping. +type responseCache struct { + now func() time.Time +} + +// defaultResponseCache is the production cache used by the package-level +// lookup/store wrappers. It reads the real wall clock. +var defaultResponseCache = responseCache{now: time.Now} + +// cacheNowContextKey carries an injected clock through the request context so +// the cache TTL logic can be driven deterministically (e.g. in tests) without +// mutating package state. +type cacheNowContextKey struct{} + +// withCLIResponseCacheNow returns a context carrying now as the clock the CLI +// response cache should use for stamping and expiring entries. +func withCLIResponseCacheNow(ctx context.Context, now func() time.Time) context.Context { + return context.WithValue(ctx, cacheNowContextKey{}, now) +} + +// responseCacheFromContext builds a responseCache using the clock injected via +// withCLIResponseCacheNow, falling back to the real wall clock. +func responseCacheFromContext(ctx context.Context) responseCache { + if now, ok := ctx.Value(cacheNowContextKey{}).(func() time.Time); ok && now != nil { + return responseCache{now: now} + } + return defaultResponseCache +} type cliResponseCacheKey struct { Provider string `json:"provider"` @@ -39,7 +71,21 @@ func newCLIResponseCacheKey(provider, model string, req requestArgs, msgs []llm. } } -func lookupCLIResponseCache(key cliResponseCacheKey) (string, time.Duration, bool) { +// lookupCLIResponseCache reads a cached response using the clock injected into +// ctx (defaulting to the real wall clock). +func lookupCLIResponseCache(ctx context.Context, key cliResponseCacheKey) (string, time.Duration, bool) { + return responseCacheFromContext(ctx).lookup(key) +} + +// storeCLIResponseCache writes a cached response using the clock injected into +// ctx (defaulting to the real wall clock). +func storeCLIResponseCache(ctx context.Context, key cliResponseCacheKey, output string) { + responseCacheFromContext(ctx).store(key, output) +} + +// lookup returns the cached output for key, its age, and whether it is a valid +// (non-expired) hit. Expired entries are removed. +func (c responseCache) lookup(key cliResponseCacheKey) (string, time.Duration, bool) { path, ok := cliResponseCachePath(key) if !ok { return "", 0, false @@ -48,7 +94,7 @@ func lookupCLIResponseCache(key cliResponseCacheKey) (string, time.Duration, boo if !ok { return "", 0, false } - age := nowCLIResponseCache().Sub(entry.CreatedAt) + age := c.now().Sub(entry.CreatedAt) if age > cliResponseCacheTTL { _ = os.Remove(path) return "", 0, false @@ -56,7 +102,8 @@ func lookupCLIResponseCache(key cliResponseCacheKey) (string, time.Duration, boo return entry.Output, age, true } -func storeCLIResponseCache(key cliResponseCacheKey, output string) { +// store persists output for key, stamping it with the injected clock. +func (c responseCache) store(key cliResponseCacheKey, output string) { path, ok := cliResponseCachePath(key) if !ok { return @@ -64,7 +111,7 @@ func storeCLIResponseCache(key cliResponseCacheKey, output string) { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return } - entry := cliResponseCacheEntry{CreatedAt: nowCLIResponseCache().UTC(), Output: output} + entry := cliResponseCacheEntry{CreatedAt: c.now().UTC(), Output: output} data, err := json.Marshal(entry) if err != nil { return |
