-
Notifications
You must be signed in to change notification settings - Fork 0
cli-common state components: statedir resolver + hermetic test helper + tier-1 cache core #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
a5bd286
feat(statedir): credential-scope path resolver + hermetic test helper
rianjs ee5f8dc
feat(cache): directory-agnostic tier-1 envelope + freshness core
rianjs 4b42eac
docs: mark working-with-state §6 step 1 (commons buildout) delivered
rianjs 52b4737
fix(statedir): reject both path separators OS-independently
rianjs a517574
test: close TDD coverage gaps (CacheDirEnsured, ReadResource I/O path)
rianjs 83b0a1f
fix(cache): reject trailing-dot keys + envelope identity-mismatch-as-…
rianjs 3004e6c
docs(cache): ReadResource/ErrCacheMiss contract includes identity mis…
rianjs File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,261 @@ | ||
| package cache_test | ||
|
|
||
| import ( | ||
| "errors" | ||
| "os" | ||
| "path/filepath" | ||
| "runtime" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/open-cli-collective/cli-common/cache" | ||
| ) | ||
|
|
||
| type payload struct { | ||
| Name string `json:"name"` | ||
| N int `json:"n"` | ||
| } | ||
|
|
||
| func TestRoundTrip(t *testing.T) { | ||
| loc := cache.Locator{Root: t.TempDir(), InstanceKey: "monit.atlassian.net"} | ||
|
|
||
| in := payload{Name: "fields", N: 7} | ||
| if err := cache.WriteResource(loc, "fields", "1h", in); err != nil { | ||
| t.Fatalf("WriteResource: %v", err) | ||
| } | ||
|
|
||
| env, err := cache.ReadResource[payload](loc, "fields") | ||
| if err != nil { | ||
| t.Fatalf("ReadResource: %v", err) | ||
| } | ||
| if env.Data != in { | ||
| t.Fatalf("Data = %+v, want %+v", env.Data, in) | ||
| } | ||
| if env.Resource != "fields" || env.Instance != "monit.atlassian.net" { | ||
| t.Fatalf("Resource/Instance = %q/%q", env.Resource, env.Instance) | ||
| } | ||
| if env.Version != cache.Version || env.TTL != "1h" { | ||
| t.Fatalf("Version/TTL = %d/%q", env.Version, env.TTL) | ||
| } | ||
| if env.FetchedAt.IsZero() || env.FetchedAt.Location() != time.UTC { | ||
| t.Fatalf("FetchedAt = %v, want non-zero UTC", env.FetchedAt) | ||
| } | ||
| } | ||
|
|
||
| func TestReadMissing(t *testing.T) { | ||
| loc := cache.Locator{Root: t.TempDir(), InstanceKey: "i"} | ||
| if _, err := cache.ReadResource[payload](loc, "absent"); !errors.Is(err, cache.ErrCacheMiss) { | ||
| t.Fatalf("err = %v, want ErrCacheMiss", err) | ||
| } | ||
| } | ||
|
|
||
| func TestVersionMismatchIsMiss(t *testing.T) { | ||
| root := t.TempDir() | ||
| loc := cache.Locator{Root: root, InstanceKey: "i"} | ||
| dir := filepath.Join(root, "i") | ||
| if err := os.MkdirAll(dir, 0o700); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| // A different schema version on disk must read as a miss (self-healing). | ||
| raw := `{"resource":"r","instance":"i","fetched_at":"2026-01-01T00:00:00Z","ttl":"1h","version":999,"data":{"name":"x","n":1}}` | ||
| if err := os.WriteFile(filepath.Join(dir, "r.json"), []byte(raw), 0o600); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if _, err := cache.ReadResource[payload](loc, "r"); !errors.Is(err, cache.ErrCacheMiss) { | ||
| t.Fatalf("err = %v, want ErrCacheMiss", err) | ||
| } | ||
| } | ||
|
|
||
| func TestIdentityMismatchIsMiss(t *testing.T) { | ||
| root := t.TempDir() | ||
| loc := cache.Locator{Root: root, InstanceKey: "host-a"} | ||
| dir := filepath.Join(root, "host-a") | ||
| if err := os.MkdirAll(dir, 0o700); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| // Correct version + path, but the envelope's own metadata names a | ||
| // different resource/instance (misplaced or hand-edited file). | ||
| raw := `{"resource":"OTHER","instance":"host-b","fetched_at":"2026-01-01T00:00:00Z","ttl":"1h","version":1,"data":{"name":"x","n":1}}` | ||
| if err := os.WriteFile(filepath.Join(dir, "r.json"), []byte(raw), 0o600); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if _, err := cache.ReadResource[payload](loc, "r"); !errors.Is(err, cache.ErrCacheMiss) { | ||
| t.Fatalf("err = %v, want ErrCacheMiss on identity mismatch", err) | ||
| } | ||
| } | ||
|
|
||
| func TestMalformedJSONIsError(t *testing.T) { | ||
| root := t.TempDir() | ||
| loc := cache.Locator{Root: root, InstanceKey: "i"} | ||
| dir := filepath.Join(root, "i") | ||
| if err := os.MkdirAll(dir, 0o700); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if err := os.WriteFile(filepath.Join(dir, "r.json"), []byte("{not json"), 0o600); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| _, err := cache.ReadResource[payload](loc, "r") | ||
| if err == nil || errors.Is(err, cache.ErrCacheMiss) { | ||
| t.Fatalf("err = %v, want a non-miss decode error", err) | ||
| } | ||
| } | ||
|
|
||
| func TestReadResource_IOErrorIsNotMiss(t *testing.T) { | ||
| root := t.TempDir() | ||
| loc := cache.Locator{Root: root, InstanceKey: "i"} | ||
| // Make the resource path a directory: os.ReadFile then returns an error | ||
| // that is NOT os.IsNotExist, exercising the "I/O error, not a miss" path. | ||
| if err := os.MkdirAll(filepath.Join(root, "i", "r.json"), 0o700); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| _, err := cache.ReadResource[payload](loc, "r") | ||
| if err == nil || errors.Is(err, cache.ErrCacheMiss) { | ||
| t.Fatalf("err = %v, want a non-miss I/O error", err) | ||
| } | ||
| } | ||
|
|
||
| func TestAtomicWrite_NoTempLeak_Perms(t *testing.T) { | ||
| root := t.TempDir() | ||
| loc := cache.Locator{Root: root, InstanceKey: "inst"} | ||
| if err := cache.WriteResource(loc, "res", "1h", payload{Name: "a", N: 1}); err != nil { | ||
| t.Fatalf("WriteResource: %v", err) | ||
| } | ||
| dir := filepath.Join(root, "inst") | ||
| entries, err := os.ReadDir(dir) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| for _, e := range entries { | ||
| if strings.Contains(e.Name(), ".tmp") { | ||
| t.Fatalf("temp file leaked: %s", e.Name()) | ||
| } | ||
| } | ||
| if runtime.GOOS != "windows" { | ||
| fi, err := os.Stat(filepath.Join(dir, "res.json")) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if perm := fi.Mode().Perm(); perm != 0o600 { | ||
| t.Fatalf("envelope mode = %o, want 0600", perm) | ||
| } | ||
| di, err := os.Stat(dir) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if perm := di.Mode().Perm(); perm != 0o700 { | ||
| t.Fatalf("cache dir mode = %o, want 0700", perm) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestInvalidRoot_CreatesNothing(t *testing.T) { | ||
| // Run inside an empty temp cwd so we can prove a relative/empty root | ||
| // never writes relative to the process working directory. | ||
| cwd := t.TempDir() | ||
| t.Chdir(cwd) | ||
|
|
||
| for _, root := range []string{"", "foo/bar", "relative"} { | ||
| loc := cache.Locator{Root: root, InstanceKey: "i"} | ||
| if err := cache.WriteResource(loc, "r", "1h", payload{}); !errors.Is(err, cache.ErrInvalidRoot) { | ||
| t.Fatalf("root=%q WriteResource err = %v, want ErrInvalidRoot", root, err) | ||
| } | ||
| if _, err := cache.ReadResource[payload](loc, "r"); !errors.Is(err, cache.ErrInvalidRoot) { | ||
| t.Fatalf("root=%q ReadResource err = %v, want ErrInvalidRoot", root, err) | ||
| } | ||
| } | ||
|
|
||
| entries, err := os.ReadDir(cwd) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if len(entries) != 0 { | ||
| t.Fatalf("cwd not untouched, contains: %v", entries) | ||
| } | ||
| } | ||
|
|
||
| func TestUnsafeComponents(t *testing.T) { | ||
| root := t.TempDir() | ||
| bad := []string{"", ".", "..", "a..b", "a/b", `a\b`, "../escape", ".hidden..x", "trailingdot."} | ||
| for _, v := range bad { | ||
| t.Run("instance="+v, func(t *testing.T) { | ||
| loc := cache.Locator{Root: root, InstanceKey: v} | ||
| if err := cache.WriteResource(loc, "r", "1h", payload{}); !errors.Is(err, cache.ErrInvalidName) { | ||
| t.Fatalf("instanceKey=%q err = %v, want ErrInvalidName", v, err) | ||
| } | ||
| }) | ||
| t.Run("name="+v, func(t *testing.T) { | ||
| loc := cache.Locator{Root: root, InstanceKey: "ok"} | ||
| if err := cache.WriteResource(loc, v, "1h", payload{}); !errors.Is(err, cache.ErrInvalidName) { | ||
| t.Fatalf("name=%q err = %v, want ErrInvalidName", v, err) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestClassify_OnlyFreshStaleManual(t *testing.T) { | ||
| now := time.Date(2026, 5, 19, 12, 0, 0, 0, time.UTC) | ||
| tests := []struct { | ||
| name string | ||
| fetchedAt time.Time | ||
| ttl string | ||
| want cache.Status | ||
| }{ | ||
| {"fresh", now.Add(-30 * time.Minute), "1h", cache.StatusFresh}, | ||
| {"elapsed-stale", now.Add(-2 * time.Hour), "1h", cache.StatusStale}, | ||
| {"exact-boundary-stale", now.Add(-1 * time.Hour), "1h", cache.StatusStale}, | ||
| {"zero-time-stale", time.Time{}, "1h", cache.StatusStale}, | ||
| {"manual-sentinel", now, "manual", cache.StatusManual}, | ||
| {"unparseable-ttl-stale", now, "not-a-duration", cache.StatusStale}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := cache.Classify(tt.fetchedAt, tt.ttl, now) | ||
| if got != tt.want { | ||
| t.Fatalf("Classify = %v, want %v", got, tt.want) | ||
| } | ||
| if got == cache.StatusUninitialized || got == cache.StatusUnavailable { | ||
| t.Fatalf("Classify must never return %v", got) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestStatusString(t *testing.T) { | ||
| cases := map[cache.Status]string{ | ||
| cache.StatusUninitialized: "uninitialized", | ||
| cache.StatusFresh: "fresh", | ||
| cache.StatusStale: "stale", | ||
| cache.StatusManual: "manual", | ||
| cache.StatusUnavailable: "unavailable", | ||
| cache.Status(99): "unknown", | ||
| } | ||
| for s, want := range cases { | ||
| if got := s.String(); got != want { | ||
| t.Fatalf("Status(%d).String() = %q, want %q", int(s), got, want) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| func TestAge(t *testing.T) { | ||
| now := time.Date(2026, 5, 19, 12, 0, 0, 0, time.UTC) | ||
| tests := []struct { | ||
| name string | ||
| fetchedAt time.Time | ||
| want string | ||
| }{ | ||
| {"zero", time.Time{}, "-"}, | ||
| {"negative-clamped", now.Add(time.Hour), "0s"}, | ||
| {"seconds", now.Add(-45 * time.Second), "45s"}, | ||
| {"minutes", now.Add(-5 * time.Minute), "5m"}, | ||
| {"hours", now.Add(-3 * time.Hour), "3h"}, | ||
| {"days", now.Add(-50 * time.Hour), "2d"}, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| if got := cache.Age(tt.fetchedAt, now); got != tt.want { | ||
| t.Fatalf("Age = %q, want %q", got, tt.want) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔵 Low (harness-engineering:harness-knowledge-reviewer):
TestUnsafeComponentsdoes not include a name with an embedded..(e.g."a..b"). The implementation correctly rejects it via thestrings.Contains(s, "..")guard, but the test never exercises that branch. A future refactor consolidating the guard into the regex alone would go undetected. Adding"a..b"to the bad slice would pin this invariant.Reply to this thread when addressed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed:
a..badded toTestUnsafeComponentsso thestrings.Contains(s,"..")invariant is pinned independently of the regex.