Skip to content

Commit bc5f6b8

Browse files
committed
feat: make & linter
1 parent 482e8ad commit bc5f6b8

25 files changed

Lines changed: 765 additions & 196 deletions

.github/workflows/ci.yml

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,55 @@ name: CI
22

33
on:
44
push:
5+
branches: [ "main", "master" ]
56
pull_request:
7+
branches: [ "main", "master" ]
68

79
jobs:
8-
test:
10+
prepare-matrix:
911
runs-on: ubuntu-latest
12+
outputs:
13+
modules: ${{ steps.set-matrix.outputs.modules }}
1014
steps:
11-
- uses: actions/checkout@v4
12-
13-
- uses: actions/setup-go@v5
14-
with:
15-
go-version-file: go.mod
16-
17-
- name: Run tests
18-
run: make test
15+
- uses: actions/checkout@v6
16+
- id: set-matrix
17+
run: |
18+
MODULES=$(find . -type d \( -name ".*" -not -name "." -o -name "vendor" \) -prune -o -type f -name "go.mod" -exec dirname {} \; | jq -R -s -c 'split("\n") | map(select(length > 0))')
19+
echo "modules=$MODULES" >> $GITHUB_OUTPUT
1920
2021
lint:
22+
needs: prepare-matrix
2123
runs-on: ubuntu-latest
24+
strategy:
25+
fail-fast: false
26+
matrix:
27+
module: ${{ fromJson(needs.prepare-matrix.outputs.modules) }}
2228
steps:
23-
- uses: actions/checkout@v4
24-
25-
- uses: actions/setup-go@v5
29+
- uses: actions/checkout@v6
30+
- uses: actions/setup-go@v6
2631
with:
27-
go-version-file: go.mod
28-
29-
- name: Install golangci-lint
30-
run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.64.8
31-
32-
- name: Add Go bin to PATH
33-
run: echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
32+
go-version-file: ${{ matrix.module }}/go.mod
33+
- name: Run golangci-lint
34+
uses: golangci/golangci-lint-action@v9
35+
with:
36+
version: v2.11.3
37+
working-directory: ${{ matrix.module }}
38+
args: --timeout=5m
3439

35-
- name: Run lint
36-
run: make lint
40+
test:
41+
name: Test
42+
needs: [prepare-matrix, lint]
43+
runs-on: ubuntu-latest
44+
strategy:
45+
fail-fast: false
46+
matrix:
47+
module: ${{ fromJson(needs.prepare-matrix.outputs.modules) }}
48+
steps:
49+
- uses: actions/checkout@v6
50+
- uses: actions/setup-go@v6
51+
with:
52+
cache: true
53+
go-version-file: ${{ matrix.module }}/go.mod
54+
- name: Run tests
55+
working-directory: ${{ matrix.module }}
56+
run: go test -v -race ./...

.golangci.yml

Lines changed: 480 additions & 54 deletions
Large diffs are not rendered by default.

Makefile

Lines changed: 33 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,48 @@
1-
.PHONY: test lint fix tidy bench fuzz cover
2-
3-
# Import path patterns for every module in go.work. A bare ./... from the repo root only matches the main module.
4-
MODULE_PKGS := $(shell go list -m -f '{{.Path}}/...')
5-
6-
7-
test:
8-
@go test -race -count=1 $(MODULE_PKGS)
1+
GO := go
2+
MODULES := $(shell find . -type d \( -name ".*" -not -name "." -o -name "vendor" \) -prune -o -type f -name "go.mod" -exec dirname {} \;)
93

4+
.PHONY: lint fix test bench fuzz cover
105

116
lint:
12-
@go list -m -f '{{.Dir}}' | while IFS= read -r dir; do \
13-
echo "$$dir"; \
7+
@for dir in $(MODULES); do \
8+
echo "golangci-lint - $$dir"; \
149
(cd "$$dir" && golangci-lint run ./...) || exit 1; \
1510
done
1611

17-
1812
fix:
19-
@go fix $(MODULE_PKGS)
20-
@go list -m -f '{{.Dir}}' | while IFS= read -r dir; do \
21-
(cd "$$dir" && go mod tidy) || exit 1; \
22-
done
23-
@go work sync
24-
@go list -m -f '{{.Dir}}' | while IFS= read -r dir; do \
25-
(cd "$$dir" && golangci-lint fmt ./... && golangci-lint run --fix ./...) || exit 1; \
13+
@if [ -f "go.work" ]; then $(GO) work sync; fi
14+
@for dir in $(MODULES); do \
15+
echo "fix & tidy - $$dir"; \
16+
(cd "$$dir" && $(GO) fix ./... && $(GO) mod tidy) || exit 1; \
17+
(cd "$$dir" && golangci-lint run --fix ./...) || exit 1; \
2618
done
2719

20+
test:
21+
@for dir in $(MODULES); do \
22+
echo "test - $$dir"; \
23+
(cd "$$dir" && $(GO) test -v -race ./...) || exit 1; \
24+
done
2825

2926
bench:
30-
@go test -bench=. -benchmem $(MODULE_PKGS)
31-
32-
cover:
33-
@go test -coverprofile=coverage.out -covermode=atomic $(MODULE_PKGS)
34-
@go tool cover -func=coverage.out
27+
@for dir in $(MODULES); do \
28+
echo "bench - $$dir"; \
29+
(cd "$$dir" && $(GO) test -bench=. -run=^$$ ./...) || exit 1; \
30+
done
3531

3632
fuzz:
37-
@go list -m -f '{{.Dir}}' | while IFS= read -r dir; do \
38-
if ! grep -r --include='*_test.go' -l 'func Fuzz' "$$dir" >/dev/null 2>&1; then \
39-
continue; \
40-
fi; \
41-
echo "$$dir"; \
42-
( cd "$$dir" && \
43-
for pkg in $$(go list ./...); do \
44-
if go test -list . "$$pkg" 2>/dev/null | grep -q '^Fuzz'; then \
45-
echo " $$pkg"; \
46-
go test -fuzz=. -fuzztime=30s "$$pkg" || exit 1; \
33+
@for dir in $(MODULES); do \
34+
echo "fuzz - $$dir"; \
35+
(cd "$$dir" && \
36+
for pkg in $$($(GO) list -tags=fuzz ./...); do \
37+
if $(GO) test -tags=fuzz -list . "$$pkg" 2>/dev/null | grep -q '^Fuzz'; then \
38+
$(GO) test -tags=fuzz -fuzz=. -fuzztime=30s "$$pkg" || exit 1; \
4739
fi; \
48-
done ) || exit 1; \
40+
done \
41+
) || exit 1; \
42+
done
43+
44+
cover:
45+
@for dir in $(MODULES); do \
46+
echo "cover - $$dir"; \
47+
(cd "$$dir" && $(GO) test -coverprofile=coverage.out ./... && $(GO) tool cover -func=coverage.out) || exit 1; \
4948
done

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ make fix
2626

2727
`make test-all`, `make lint-all`, and `make fix-all` are aliases for `make test`, `make lint`, and `make fix`. Use a local clone with `go.work` so adapters resolve the **local** `contexty` tree (no unpublished version tags required for day-to-day work).
2828

29-
`make bench` runs benchmarks across the workspace. `make cover` writes a single `coverage.out` at the repo root and prints `go tool cover -func`. `make fuzz` runs `go test -fuzz` per package in each module that contains `func Fuzz*` (modules without fuzz targets are skipped; Go does not support `-fuzz` with `./...` across multiple packages).
29+
`make bench` runs benchmarks across the workspace. `make cover` writes a single `coverage.out` at the repo root and prints `go tool cover -func`. Fuzz targets live in `*_test.go` files with `//go:build fuzz` so they are not compiled by plain `go test ./...`. `make fuzz` runs `go test -tags=fuzz -fuzz` **per package** in each module that has fuzz tests (Go does not allow `-fuzz` across multiple packages in one invocation).
3030

3131
## Fail-fast API (panics)
3232

adapters/store/postgres/store.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func New(pool *pgxpool.Pool, opts ...Option) *Store {
4040
// Load returns all stored messages for threadID ordered by insertion ID and the current version.
4141
func (s *Store) Load(ctx context.Context, threadID string) (contexty.HistorySnapshot, error) {
4242
if s.pool == nil {
43-
return contexty.HistorySnapshot{}, fmt.Errorf("contexty/postgres: nil pool")
43+
return contexty.HistorySnapshot{}, errors.New("contexty/postgres: nil pool")
4444
}
4545

4646
var version int64
@@ -87,7 +87,7 @@ func (s *Store) Append(ctx context.Context, threadID string, expectedVersion int
8787
return nil
8888
}
8989
if s.pool == nil {
90-
return fmt.Errorf("contexty/postgres: nil pool")
90+
return errors.New("contexty/postgres: nil pool")
9191
}
9292

9393
tx, err := s.pool.Begin(ctx)
@@ -111,7 +111,7 @@ func (s *Store) Append(ctx context.Context, threadID string, expectedVersion int
111111
// Save replaces the full stored history when expectedVersion matches.
112112
func (s *Store) Save(ctx context.Context, threadID string, expectedVersion int64, msgs []contexty.Message) error {
113113
if s.pool == nil {
114-
return fmt.Errorf("contexty/postgres: nil pool")
114+
return errors.New("contexty/postgres: nil pool")
115115
}
116116

117117
tx, err := s.pool.Begin(ctx)
@@ -140,7 +140,7 @@ func (s *Store) Save(ctx context.Context, threadID string, expectedVersion int64
140140
// Clear removes all stored history when expectedVersion matches.
141141
func (s *Store) Clear(ctx context.Context, threadID string, expectedVersion int64) error {
142142
if s.pool == nil {
143-
return fmt.Errorf("contexty/postgres: nil pool")
143+
return errors.New("contexty/postgres: nil pool")
144144
}
145145

146146
tx, err := s.pool.Begin(ctx)
@@ -182,7 +182,14 @@ type queryExecutor interface {
182182
Exec(ctx context.Context, sql string, arguments ...any) (pgconn.CommandTag, error)
183183
}
184184

185-
func execAppend(ctx context.Context, serializer contexty.MessageSerializer, query string, exec queryExecutor, threadID string, msgs []contexty.Message) error {
185+
func execAppend(
186+
ctx context.Context,
187+
serializer contexty.MessageSerializer,
188+
query string,
189+
exec queryExecutor,
190+
threadID string,
191+
msgs []contexty.Message,
192+
) error {
186193
payloads := make([]string, len(msgs))
187194
for i, msg := range msgs {
188195
payload, err := serializer.Marshal(msg)

adapters/store/redis/store.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (s *Store) listKey(threadID string) string {
8282
// Load returns all stored messages for threadID in append order and the current version.
8383
func (s *Store) Load(ctx context.Context, threadID string) (contexty.HistorySnapshot, error) {
8484
if s.client == nil {
85-
return contexty.HistorySnapshot{}, fmt.Errorf("contexty/redis: nil client")
85+
return contexty.HistorySnapshot{}, errors.New("contexty/redis: nil client")
8686
}
8787

8888
vstr, err := s.client.Get(ctx, s.verKey(threadID)).Result()
@@ -123,7 +123,7 @@ func (s *Store) Append(ctx context.Context, threadID string, expectedVersion int
123123
return nil
124124
}
125125
if s.client == nil {
126-
return fmt.Errorf("contexty/redis: nil client")
126+
return errors.New("contexty/redis: nil client")
127127
}
128128

129129
payloads, err := s.serializeMessages(msgs)
@@ -147,7 +147,7 @@ func (s *Store) Append(ctx context.Context, threadID string, expectedVersion int
147147
// Save replaces the full stored history when expectedVersion matches.
148148
func (s *Store) Save(ctx context.Context, threadID string, expectedVersion int64, msgs []contexty.Message) error {
149149
if s.client == nil {
150-
return fmt.Errorf("contexty/redis: nil client")
150+
return errors.New("contexty/redis: nil client")
151151
}
152152

153153
var args []any
@@ -172,9 +172,14 @@ func (s *Store) Save(ctx context.Context, threadID string, expectedVersion int64
172172
// Clear removes all stored history when expectedVersion matches.
173173
func (s *Store) Clear(ctx context.Context, threadID string, expectedVersion int64) error {
174174
if s.client == nil {
175-
return fmt.Errorf("contexty/redis: nil client")
176-
}
177-
if err := s.evalConflict(ctx, luaClear, []string{s.verKey(threadID), s.listKey(threadID)}, expectedVersion); err != nil {
175+
return errors.New("contexty/redis: nil client")
176+
}
177+
if err := s.evalConflict(
178+
ctx,
179+
luaClear,
180+
[]string{s.verKey(threadID), s.listKey(threadID)},
181+
expectedVersion,
182+
); err != nil {
178183
return err
179184
}
180185
s.maybeExpire(ctx, threadID)

adapters/store/redis/store_integration_test.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,10 @@ func TestStoreIntegration(t *testing.T) {
136136
withTTL := New(client, WithTTL(24*time.Hour))
137137
s0, err := withTTL.Load(ctx, "thread-ttl")
138138
require.NoError(t, err)
139-
require.NoError(t, withTTL.Append(ctx, "thread-ttl", s0.Version, contexty.TextMessage(contexty.RoleUser, "ttl")))
139+
require.NoError(
140+
t,
141+
withTTL.Append(ctx, "thread-ttl", s0.Version, contexty.TextMessage(contexty.RoleUser, "ttl")),
142+
)
140143
ttlList, err := client.TTL(ctx, defaultKeyPrefix+"thread-ttl").Result()
141144
require.NoError(t, err)
142145
assert.Greater(t, ttlList, time.Duration(0))
@@ -147,7 +150,10 @@ func TestStoreIntegration(t *testing.T) {
147150
withoutTTL := New(client)
148151
s1, err := withoutTTL.Load(ctx, "thread-no-ttl")
149152
require.NoError(t, err)
150-
require.NoError(t, withoutTTL.Append(ctx, "thread-no-ttl", s1.Version, contexty.TextMessage(contexty.RoleUser, "no ttl")))
153+
require.NoError(
154+
t,
155+
withoutTTL.Append(ctx, "thread-no-ttl", s1.Version, contexty.TextMessage(contexty.RoleUser, "no ttl")),
156+
)
151157
ttl, err := client.TTL(ctx, defaultKeyPrefix+"thread-no-ttl").Result()
152158
require.NoError(t, err)
153159
assert.Equal(t, time.Duration(-1), ttl)

builder.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package contexty
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67
)
78

@@ -93,7 +94,7 @@ func (b *Builder) Clone() *Builder {
9394
// SetBlockMessages replaces the messages of a named block using a deep copy of msgs.
9495
func (b *Builder) SetBlockMessages(name string, msgs []Message) error {
9596
if b == nil {
96-
return fmt.Errorf("contexty: set block messages: nil builder")
97+
return errors.New("contexty: set block messages: nil builder")
9798
}
9899
for i := range b.blocks {
99100
if b.blocks[i].name == name {
@@ -118,6 +119,8 @@ func (b *Builder) Build(ctx context.Context) ([]Message, error) {
118119

119120
// BuildDetailed assembles the configured blocks and returns formatter output plus
120121
// raw post-eviction block snapshots for orchestration.
122+
//
123+
//nolint:gocognit // Sequential block loop: eviction, budget checks, and strategy branches belong in one flow for readability.
121124
func (b *Builder) BuildDetailed(ctx context.Context) (BuildResult, error) {
122125
if b.maxTokens <= 0 || b.counter == nil {
123126
return BuildResult{}, ErrInvalidConfig

builder_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ func TestBuild_AddBlockSnapshotsInput(t *testing.T) {
173173
Role: RoleSystem,
174174
Content: []ContentPart{
175175
{Type: ContentPartTypeText, Text: "original"},
176-
{Type: ContentPartTypeImageURL, ImageURL: &ImageURL{URL: "https://example.com/original.png", Detail: "low"}},
176+
{
177+
Type: ContentPartTypeImageURL,
178+
ImageURL: &ImageURL{URL: "https://example.com/original.png", Detail: "low"},
179+
},
177180
},
178181
Metadata: map[string]any{"scope": "original"},
179182
}},

errors.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package contexty
33
import "errors"
44

55
// Sentinel errors for typical contexty failure modes.
6-
// Use errors.Is to check for these in calling code.
6+
// Use [errors.Is] to check for these in calling code.
77
var (
88
// ErrBudgetExceeded is returned by StrictStrategy when a block does not fit
99
// within the remaining token budget.

0 commit comments

Comments
 (0)