Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ on:
jobs:
build-and-test:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Codecov OIDC upload; no stored token needed
steps:
- uses: actions/checkout@v4

Expand All @@ -34,4 +37,12 @@ jobs:
# outside internal/httpx constructs a raw http.Client / http.NewRequest —
# so un-receipted egress can't merge.
- name: Test
run: go test ./...
run: go test -coverprofile=coverage.out -covermode=atomic ./...

- name: Upload coverage
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
uses: codecov/codecov-action@v5
with:
files: coverage.out
use_oidc: true
fail_ci_if_error: false
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ build/
dist/
security-roast-report.md
.claude/
coverage.out
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,21 @@ All notable changes to qmax-code. Versions follow [Semantic Versioning](https://

## [Unreleased]

### Changed
- Go module path is now `github.com/Quality-Max/qmax-code`, matching the
GitHub repository. The old lowercase path stopped the Go proxy and
pkg.go.dev from resolving the module. `go install
github.com/Quality-Max/qmax-code@latest` works from the next tagged release.

### Added
- Haiku 5.5 as an explicit Anthropic API and Claude Code model option, with
request settings and per-request cost estimates. The `haiku` shorthand and
automatic chat routing continue to use Haiku 4.5 until verified.
- Regression coverage for CLI commands, agent loops, the REPL, terminal UI,
authentication, sessions, setup, telemetry, and VNC frame handling.
- Package documentation for `pkg.go.dev` and a Go Reference badge.
- CI uploads test coverage to Codecov.

## [1.38.0] - 2026-10-14

### Changed
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
[![GitHub downloads](https://img.shields.io/github/downloads/Quality-Max/qmax-code/total?label=downloads)](https://github.com/Quality-Max/qmax-code/releases)
[![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
[![Made with Go](https://img.shields.io/badge/made%20with-Go%201.25.13+-00ADD8.svg)](https://go.dev/)
[![Go Reference](https://pkg.go.dev/badge/github.com/Quality-Max/qmax-code.svg)](https://pkg.go.dev/github.com/Quality-Max/qmax-code)
[![Announcement](https://img.shields.io/badge/announcement-Apache%202.0-7c6cf0.svg)](docs/releases/v1.38.0.md)

[![Buy Me a Coffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-yellow?logo=buymeacoffee)](https://buymeacoffee.com/qualitymax)
Expand Down Expand Up @@ -123,6 +124,12 @@ curl -sL https://qualitymax.io/static/install-qmax-code.txt | bash

To build from source instead:

```bash
go install github.com/Quality-Max/qmax-code@latest
```

Or from a checkout:

```bash
git clone https://github.com/Quality-Max/qmax-code.git
cd qmax-code
Expand Down Expand Up @@ -274,6 +281,13 @@ troubleshooting.
| OpenCode | `/opencode` or `/orch` | Per-provider key in OS keychain | CLI subprocess for opt-in Z.AI, Groq, and OpenRouter providers. |
| Ollama | `/ollama` or `/orch` | Configured Ollama endpoint | Self-hosted inference; configure the URL and model first. |

Haiku 5.5 is an explicit option in the Anthropic API and Claude Code pickers,
or with `/model claude-haiku-5-5`. The `haiku` shorthand and automatic chat
routing continue to use Haiku 4.5 until the upgrade is verified. Direct Haiku
5.5 requests disable thinking to reserve the response budget for text and
tools. Cost estimates preserve each request's prompt-size tier and reported
cache tokens; historical sessions without recorded costs remain estimates.

QualityMax authentication is independent of model-provider authentication. You
can run any backend with `--local` and no QualityMax login. Without `--local`,
qmax-code starts the QualityMax onboarding flow when no supported QualityMax
Expand Down
4 changes: 2 additions & 2 deletions claude_code_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import (
"runtime"
"strings"

"github.com/qualitymax/qmax-code/internal/api"
"github.com/qualitymax/qmax-code/internal/setup"
"github.com/Quality-Max/qmax-code/internal/api"
"github.com/Quality-Max/qmax-code/internal/setup"
)

const maxClaudeCodeAuthBytes = 64 * 1024
Expand Down
155 changes: 155 additions & 0 deletions claude_code_command_handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package main

import (
"context"
"os"
"path/filepath"
"strings"
"testing"

"github.com/Quality-Max/qmax-code/internal/api"
)

// stubCCSeams swaps the three connectClaudeCode seams and restores them at
// test end. Returns the login stub so a test can record calls.
func stubCCSeams(t *testing.T, load func() *api.AuthConfig, login func() (*api.AuthConfig, error), upload func(context.Context, *api.AuthConfig, string) (*api.ClaudeCodeConnection, error)) {
t.Helper()
oldLoad := loadQualityMaxAuthForCC
oldLogin := loginQualityMaxForCC
oldUpload := uploadClaudeCodeAuth
loadQualityMaxAuthForCC = load
loginQualityMaxForCC = login
uploadClaudeCodeAuth = upload
t.Cleanup(func() {
loadQualityMaxAuthForCC = oldLoad
loginQualityMaxForCC = oldLogin
uploadClaudeCodeAuth = oldUpload
})
}

// writeClaudeCredentials writes a credentials file under the temp HOME so
// readClaudeCodeAuth's file fallback finds it. Combine with
// isolatePathWithFailingSecurity to keep the macOS keychain out of the picture.
func writeClaudeCredentials(t *testing.T, content string) {
t.Helper()
home := withTempHome(t)
dir := filepath.Join(home, ".config", "claude")
if err := os.MkdirAll(dir, 0o700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "credentials.json"), []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}

func TestHandleCCCommandUsage(t *testing.T) {
cases := [][]string{
nil,
{},
{"login"},
{"connect", "extra"},
}
for _, args := range cases {
err := handleCCCommand(args)
if err == nil || !strings.Contains(err.Error(), "usage: qmax-code cc connect") {
t.Errorf("handleCCCommand(%v) = %v, want usage error", args, err)
}
}
}

func TestHandleCCCommandConnectUploadsSession(t *testing.T) {
isolatePathWithFailingSecurity(t)
authJSON := `{"tokens":{"access_token":"access"}}`
writeClaudeCredentials(t, authJSON)

uploadRan := false
stubCCSeams(t,
func() *api.AuthConfig { return &api.AuthConfig{APIKey: "qm-user"} },
func() (*api.AuthConfig, error) {
t.Fatal("existing QualityMax auth should be reused")
return nil, nil
},
func(ctx context.Context, auth *api.AuthConfig, gotJSON string) (*api.ClaudeCodeConnection, error) {
uploadRan = true
if gotJSON != authJSON {
t.Fatalf("uploaded auth JSON = %q, want file content", gotJSON)
}
return &api.ClaudeCodeConnection{Connected: true, AccountLabel: "user@example.com"}, nil
},
)
stdout, _ := captureOutput(t)

if err := handleCCCommand([]string{"connect"}); err != nil {
t.Fatalf("handleCCCommand(connect): %v", err)
}
if !uploadRan {
t.Fatal("upload was never invoked")
}
if out := stdout(); !strings.Contains(out, "connected to QualityMax as user@example.com") {
t.Fatalf("stdout = %q, want account-label confirmation", out)
}
}

func TestConnectClaudeCodeRejectsNonJSONCredentials(t *testing.T) {
isolatePathWithFailingSecurity(t)
writeClaudeCredentials(t, "definitely not json")

stubCCSeams(t,
func() *api.AuthConfig { return &api.AuthConfig{APIKey: "qm-user"} },
nil,
func(context.Context, *api.AuthConfig, string) (*api.ClaudeCodeConnection, error) {
t.Fatal("upload must not run for invalid credentials")
return nil, nil
},
)

err := connectClaudeCode(context.Background())
if err == nil || !strings.Contains(err.Error(), "not a valid JSON object") {
t.Fatalf("connectClaudeCode error = %v, want JSON validation error", err)
}
}

func TestConnectClaudeCodeErrorPropagatesThroughHandler(t *testing.T) {
isolatePathWithFailingSecurity(t)
writeClaudeCredentials(t, "definitely not json")

stubCCSeams(t, func() *api.AuthConfig { return &api.AuthConfig{APIKey: "qm-user"} }, nil, nil)

if err := handleCCCommand([]string{"connect"}); err == nil {
t.Fatal("handleCCCommand should surface the credential error")
}
}

func TestConnectClaudeCodeLogsInWhenUnauthenticated(t *testing.T) {
isolatePathWithFailingSecurity(t)
writeClaudeCredentials(t, `{"tokens":{"access_token":"access"}}`)

loginCalls := 0
stubCCSeams(t,
func() *api.AuthConfig { return nil }, // no stored QualityMax auth
func() (*api.AuthConfig, error) {
loginCalls++
return &api.AuthConfig{APIKey: "fresh-login"}, nil
},
func(ctx context.Context, auth *api.AuthConfig, authJSON string) (*api.ClaudeCodeConnection, error) {
if auth.APIKey != "fresh-login" {
t.Fatalf("upload used APIKey %q, want the freshly logged-in one", auth.APIKey)
}
return &api.ClaudeCodeConnection{Connected: true}, nil
},
)
stdout, _ := captureOutput(t)

if err := connectClaudeCode(context.Background()); err != nil {
t.Fatalf("connectClaudeCode: %v", err)
}
if loginCalls != 1 {
t.Fatalf("login calls = %d, want 1", loginCalls)
}
if out := stdout(); !strings.Contains(out, "Connect this computer to QualityMax") {
t.Fatalf("stdout = %q, want login prompt message", out)
}
if out := stdout(); !strings.Contains(out, "Claude Code connected to QualityMax.") {
t.Fatalf("stdout = %q, want unlabelled confirmation", out)
}
}
2 changes: 1 addition & 1 deletion claude_code_command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"path/filepath"
"testing"

"github.com/qualitymax/qmax-code/internal/api"
"github.com/Quality-Max/qmax-code/internal/api"
)

func TestReadClaudeCodeAuthFile(t *testing.T) {
Expand Down
Loading
Loading