diff options
| author | Paul Buetow <paul@buetow.org> | 2025-09-19 22:56:10 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2025-09-19 22:56:10 +0300 |
| commit | 1065d7a991d3b5c103bcc986a84867db28cb4720 (patch) | |
| tree | 00824c2b7b7b3016f235205bed9483b638340de6 /internal | |
| parent | 6feda08653a80a7609df2e8b80e98ede15f86a61 (diff) | |
morev0.11.6
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/appconfig/config_alias_test.go | 36 | ||||
| -rw-r--r-- | internal/appconfig/config_env_model_test.go | 37 | ||||
| -rw-r--r-- | internal/hexaicli/run_model_override_test.go | 39 | ||||
| -rw-r--r-- | internal/lsp/transport_concurrency_test.go | 92 | ||||
| -rw-r--r-- | internal/stats/lock_posix.go | 23 | ||||
| -rw-r--r-- | internal/stats/lock_windows.go | 24 |
6 files changed, 251 insertions, 0 deletions
diff --git a/internal/appconfig/config_alias_test.go b/internal/appconfig/config_alias_test.go new file mode 100644 index 0000000..6cc5bda --- /dev/null +++ b/internal/appconfig/config_alias_test.go @@ -0,0 +1,36 @@ +package appconfig + +import ( + "log" + "os" + "path/filepath" + "testing" +) + +func TestOpenAIPresets_AliasResolution(t *testing.T) { + dir := t.TempDir() + t.Setenv("XDG_CONFIG_HOME", dir) + cfgDir := filepath.Join(dir, "hexai") + if err := os.MkdirAll(cfgDir, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + toml := ` +[provider] +name = "openai" + +[openai] +model = "codex" + +[openai.presets] +codex = "gpt-5-codex" +` + path := filepath.Join(cfgDir, "config.toml") + if err := os.WriteFile(path, []byte(toml), 0o644); err != nil { + t.Fatalf("write: %v", err) + } + cfg := Load(log.New(os.Stderr, "test ", 0)) + if cfg.OpenAIModel != "gpt-5-codex" { + t.Fatalf("expected alias to resolve to gpt-5-codex, got %q", cfg.OpenAIModel) + } +} + diff --git a/internal/appconfig/config_env_model_test.go b/internal/appconfig/config_env_model_test.go new file mode 100644 index 0000000..2db2bb5 --- /dev/null +++ b/internal/appconfig/config_env_model_test.go @@ -0,0 +1,37 @@ +package appconfig + +import ( + "log" + "os" + "testing" +) + +// Test that HEXAI_MODEL applies to provider model fields and that +// provider-specific envs take precedence when both are set. +func TestEnv_GenericModelOverrideAndPrecedence(t *testing.T) { + t.Setenv("HEXAI_MODEL", "gpt-5-codex") + t.Setenv("HEXAI_PROVIDER", "openai") + // No provider-specific env set yet: HEXAI_MODEL should flow into OpenAIModel + cfg := Load(log.New(os.Stderr, "test ", 0)) + if cfg.OpenAIModel != "gpt-5-codex" { + t.Fatalf("expected OpenAIModel=gpt-5-codex via HEXAI_MODEL, got %q", cfg.OpenAIModel) + } + + // Now set a provider-specific model; it should win over HEXAI_MODEL + t.Setenv("HEXAI_OPENAI_MODEL", "gpt-5-thinking") + cfg2 := Load(log.New(os.Stderr, "test ", 0)) + if cfg2.OpenAIModel != "gpt-5-thinking" { + t.Fatalf("expected OpenAIModel from HEXAI_OPENAI_MODEL to win, got %q", cfg2.OpenAIModel) + } +} + +// Test that HEXAI_MODEL_FORCE overrides provider-specific envs (used by CLI --model). +func TestEnv_ModelForce_OverridesProviderSpecific(t *testing.T) { + t.Setenv("HEXAI_OPENAI_MODEL", "gpt-5-main") + t.Setenv("HEXAI_MODEL_FORCE", "gpt-5-codex") + t.Setenv("HEXAI_PROVIDER", "openai") + cfg := Load(log.New(os.Stderr, "test ", 0)) + if cfg.OpenAIModel != "gpt-5-codex" { + t.Fatalf("expected OpenAIModel forced to gpt-5-codex, got %q", cfg.OpenAIModel) + } +} diff --git a/internal/hexaicli/run_model_override_test.go b/internal/hexaicli/run_model_override_test.go new file mode 100644 index 0000000..6394bd1 --- /dev/null +++ b/internal/hexaicli/run_model_override_test.go @@ -0,0 +1,39 @@ +package hexaicli + +import ( + "bytes" + "context" + "strings" + "testing" + + "codeberg.org/snonux/hexai/internal/appconfig" + "codeberg.org/snonux/hexai/internal/llm" +) + +type fakeClientModelEnv struct{ name, model string } +func (f fakeClientModelEnv) Chat(_ context.Context, _ []llm.Message, _ ...llm.RequestOption) (string, error) { return "ok", nil } +func (f fakeClientModelEnv) Name() string { return f.name } +func (f fakeClientModelEnv) DefaultModel() string { return f.model } + +// Ensure that HEXAI_MODEL overrides config for CLI runs. +func TestRun_ModelEnvOverride_FlowsIntoClient(t *testing.T) { + t.Setenv("HEXAI_MODEL", "gpt-5-codex") + t.Setenv("HEXAI_PROVIDER", "openai") + // Replace client constructor to assert model was overridden + oldNew := newClientFromApp + defer func() { newClientFromApp = oldNew }() + newClientFromApp = func(cfg appconfig.App) (llm.Client, error) { + if strings.TrimSpace(cfg.OpenAIModel) != "gpt-5-codex" { + t.Fatalf("expected cfg.OpenAIModel=gpt-5-codex, got %q", cfg.OpenAIModel) + } + return fakeClientModelEnv{name: "openai", model: cfg.OpenAIModel}, nil + } + + var out, errb bytes.Buffer + if err := Run(context.Background(), []string{"hello"}, strings.NewReader(""), &out, &errb); err != nil { + t.Fatalf("run error: %v", err) + } + if !strings.Contains(errb.String(), "model=gpt-5-codex") { + t.Fatalf("stderr should print effective model, got: %s", errb.String()) + } +} diff --git a/internal/lsp/transport_concurrency_test.go b/internal/lsp/transport_concurrency_test.go new file mode 100644 index 0000000..f2390c7 --- /dev/null +++ b/internal/lsp/transport_concurrency_test.go @@ -0,0 +1,92 @@ +package lsp + +import ( + "bytes" + "encoding/json" + "io" + "log" + "runtime" + "strconv" + "strings" + "sync" + "sync/atomic" + "testing" +) + +type raceBuffer struct { + buf bytes.Buffer + bufMu sync.Mutex + inWrite atomic.Int32 + raced atomic.Bool +} + +func (b *raceBuffer) Write(p []byte) (int, error) { + if b.inWrite.Swap(1) != 0 { + b.raced.Store(true) + } + runtime.Gosched() + b.bufMu.Lock() + n, err := b.buf.Write(p) + b.bufMu.Unlock() + b.inWrite.Store(0) + return n, err +} + +func (b *raceBuffer) Bytes() []byte { + b.bufMu.Lock() + defer b.bufMu.Unlock() + return append([]byte(nil), b.buf.Bytes()...) +} + +func (b *raceBuffer) Raced() bool { + return b.raced.Load() +} + +func TestServerReplySerializesWrites(t *testing.T) { + t.Parallel() + + writer := &raceBuffer{} + srv := NewServer(bytes.NewReader([]byte{}), writer, log.New(io.Discard, "", 0), ServerOptions{}) + + const goroutines = 16 + start := make(chan struct{}) + var wg sync.WaitGroup + for i := 0; i < goroutines; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + <-start + id := json.RawMessage(strconv.Itoa(i + 1)) + srv.reply(id, map[string]int{"index": i}, nil) + }(i) + } + close(start) + wg.Wait() + + if writer.Raced() { + t.Fatalf("detected overlapping writes to server output") + } + + data := writer.Bytes() + for len(data) > 0 { + headerEnd := bytes.Index(data, []byte("\r\n\r\n")) + if headerEnd < 0 { + t.Fatalf("missing header delimiter in %q", string(data)) + } + header := string(data[:headerEnd]) + if !strings.HasPrefix(header, "Content-Length: ") { + t.Fatalf("unexpected header %q", header) + } + lengthStr := strings.TrimSpace(header[len("Content-Length: "):]) + length, err := strconv.Atoi(lengthStr) + if err != nil { + t.Fatalf("invalid content length %q: %v", lengthStr, err) + } + payloadStart := headerEnd + 4 + payloadEnd := payloadStart + length + if payloadEnd > len(data) { + t.Fatalf("payload truncated: need %d bytes, have %d", length, len(data)-payloadStart) + } + data = data[payloadEnd:] + } +} diff --git a/internal/stats/lock_posix.go b/internal/stats/lock_posix.go new file mode 100644 index 0000000..2c41d31 --- /dev/null +++ b/internal/stats/lock_posix.go @@ -0,0 +1,23 @@ +//go:build !windows + +package stats + +import ( + "errors" + + "golang.org/x/sys/unix" +) + +func tryLockFile(fd uintptr) error { + if err := unix.Flock(int(fd), unix.LOCK_EX|unix.LOCK_NB); err != nil { + if errors.Is(err, unix.EWOULDBLOCK) { + return errLockWouldBlock + } + return err + } + return nil +} + +func unlockFile(fd uintptr) error { + return unix.Flock(int(fd), unix.LOCK_UN) +} diff --git a/internal/stats/lock_windows.go b/internal/stats/lock_windows.go new file mode 100644 index 0000000..2ec5e90 --- /dev/null +++ b/internal/stats/lock_windows.go @@ -0,0 +1,24 @@ +//go:build windows + +package stats + +import ( + "golang.org/x/sys/windows" +) + +func tryLockFile(fd uintptr) error { + var ol windows.Overlapped + err := windows.LockFileEx(windows.Handle(fd), windows.LOCKFILE_EXCLUSIVE_LOCK|windows.LOCKFILE_FAIL_IMMEDIATELY, 0, 1, 0, &ol) + if err == nil { + return nil + } + if err == windows.ERROR_LOCK_VIOLATION { + return errLockWouldBlock + } + return err +} + +func unlockFile(fd uintptr) error { + var ol windows.Overlapped + return windows.UnlockFileEx(windows.Handle(fd), 0, 1, 0, &ol) +} |
