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
52 changes: 40 additions & 12 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,45 @@
version: "2"
linters:
enable:
- govet
- errcheck
- staticcheck
- unused
- ineffassign
- misspell
settings:
errcheck:
check-type-assertions: true
exclusions:
generated: lax
rules:
# errcheck's check-type-assertions is on so that an unchecked
# assertion in shipping code is a finding rather than a panic in
# front of a user. In a test a panicking assertion already fails the
# test with the offending type in the message, and rewriting forty
# inline assertions into two-statement checks would cost readability
# for no safety. This exempts type assertions only: errcheck reports
# those without a function name, so unchecked *errors* stay enforced
# in tests too.
- path: _test\.go
linters:
- errcheck
text: ^Error return value is not checked$
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports

linters-settings:
errcheck:
check-type-assertions: true

run:
timeout: 5m
settings:
goimports:
local-prefixes:
- github.com/packetcode/packetcode
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
9 changes: 4 additions & 5 deletions cmd/packetcode/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"crypto/sha256"
"flag"
"fmt"
"github.com/packetcode/packetcode/internal/provider"
"io"
"os"
"time"
Expand All @@ -26,6 +25,7 @@ import (
"github.com/packetcode/packetcode/internal/git"
"github.com/packetcode/packetcode/internal/jobs"
"github.com/packetcode/packetcode/internal/permissions"
"github.com/packetcode/packetcode/internal/provider"
"github.com/packetcode/packetcode/internal/tools"
"github.com/packetcode/packetcode/internal/ui/theme"
"github.com/packetcode/packetcode/internal/workflow"
Expand Down Expand Up @@ -215,7 +215,7 @@ func run(providerOverride, modelOverride, resumeID string, trust bool, permissio
var runtimeBackend computers.RuntimeBackend
var activeComputer *computers.Computer
if !cfg.PacketComputers.IsEnabled() && computerName != "" {
return fmt.Errorf("Packet Computers integration is disabled; enable [packet_computers].enabled or set PACKETCODE_PACKET_COMPUTERS_ENABLED=true")
return fmt.Errorf("the Packet Computers integration is disabled; enable [packet_computers].enabled or set PACKETCODE_PACKET_COMPUTERS_ENABLED=true")
}
if computerName != "" {
computersDir, dirErr := config.ComputersDir()
Expand Down Expand Up @@ -328,7 +328,7 @@ func run(providerOverride, modelOverride, resumeID string, trust bool, permissio
}
}
disabledComputersError := func() error {
return fmt.Errorf("Packet Computers integration is disabled; enable [packet_computers].enabled or set PACKETCODE_PACKET_COMPUTERS_ENABLED=true")
return fmt.Errorf("the Packet Computers integration is disabled; enable [packet_computers].enabled or set PACKETCODE_PACKET_COMPUTERS_ENABLED=true")
}
var resolveWorkspace jobs.WorkspaceResolver = func(string) (jobs.Workspace, error) {
return jobs.Workspace{}, disabledComputersError()
Expand Down Expand Up @@ -455,8 +455,7 @@ func run(providerOverride, modelOverride, resumeID string, trust bool, permissio
return tools.NewCollectAgentResultsTool(jobsMgr.AsToolsSpawner(), parentJobID, parentDepth)
})
runtime.AddCleanup(func() error {
jobsMgr.Shutdown(5 * time.Second)
return nil
return jobsMgr.Shutdown(5 * time.Second)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate the cleanup error past the deferred Close

When a job worker survives the five-second timeout or snapshot flushing fails, this cleanup now returns the error to packetRuntime.Close, but the TUI path invokes that method as defer runtime.Close() at line 290 and discards its result. Consequently, if the Bubble Tea run itself succeeds, run still returns nil and the process reports success—the shutdown failure this change intends to expose remains invisible.

Useful? React with 👍 / 👎.

})

toolReg.Register(tools.NewSpawnAgentTool(jobsMgr.AsToolsSpawner(), "", 0))
Expand Down
4 changes: 2 additions & 2 deletions cmd/packetcode/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"github.com/packetcode/packetcode/internal/toolout"
"io"
"sync"
"time"
Expand All @@ -20,6 +19,7 @@ import (
"github.com/packetcode/packetcode/internal/provider"
"github.com/packetcode/packetcode/internal/session"
"github.com/packetcode/packetcode/internal/skills"
"github.com/packetcode/packetcode/internal/toolout"
"github.com/packetcode/packetcode/internal/tools"
)

Expand Down Expand Up @@ -179,7 +179,7 @@ func buildPacketRuntime(ctx context.Context, opts packetRuntimeConfig) (_ *packe
activeModel = opts.ModelOverride
}
if activeProvider == "sugar" && !opts.Config.SugarIsEnabled() && !opts.Config.SugarUsesCustomProvider() {
return nil, fmt.Errorf("Sugar integration is disabled; enable [sugar].enabled or set PACKETCODE_SUGAR_ENABLED=true")
return nil, fmt.Errorf("the Sugar integration is disabled; enable [sugar].enabled or set PACKETCODE_SUGAR_ENABLED=true")
}
if activeProvider == "" {
return nil, fmt.Errorf("no default provider is configured; configure PacketCode before creating a session")
Expand Down
2 changes: 1 addition & 1 deletion cmd/packetcode/sugar_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ func validateSugarLoginURL(raw string) error {
if parsed.Scheme == "http" && (host == "localhost" || host == "127.0.0.1" || host == "::1") {
return nil
}
return fmt.Errorf("Sugar server must use HTTPS unless it is localhost")
return fmt.Errorf("the Sugar server must use HTTPS unless it is localhost")
}

func validateSugarVerificationURL(baseURL, verificationURL string) error {
Expand Down
5 changes: 3 additions & 2 deletions cmd/packetcode/sugar_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import (
"testing"
"time"

"github.com/packetcode/packetcode/internal/config"
"github.com/packetcode/packetcode/internal/provider/sugar"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/packetcode/packetcode/internal/config"
"github.com/packetcode/packetcode/internal/provider/sugar"
)

func TestSugarLoginDiscoversLiveModelsAndPersistsProvider(t *testing.T) {
Expand Down
7 changes: 0 additions & 7 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,6 @@ type App struct {
width int
height int
streaming bool
err string

// Coalesced live tool-output streaming. Chunks (EventToolOutputChunk)
// land in toolOutputPending keyed by the running call id; a single
Expand Down Expand Up @@ -1574,12 +1573,6 @@ func (a *App) startTurn(text string, emitUser bool) (tea.Model, tea.Cmd) {
return a.startTurnWith(turnOptions{display: text, text: text, emitUser: emitUser})
}

// startTurnDisplaying starts a turn whose transcript line is not the text the
// model receives, for text the user typed.
func (a *App) startTurnDisplaying(display, text string, emitUser bool) (tea.Model, tea.Cmd) {
return a.startTurnWith(turnOptions{display: display, text: text, emitUser: emitUser})
}

// turnOptions describes one turn's two texts and where its text came from.
type turnOptions struct {
// display is the transcript line; text is what the model receives.
Expand Down
29 changes: 0 additions & 29 deletions internal/app/app_cancel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,35 +87,6 @@ func (r *releaseProvider) ChatCompletion(ctx context.Context, _ provider.ChatReq
return ch, nil
}

// approvalProvider emits a write_file tool call on turn 0 — used to
// drive the App into a state where the approval modal is visible.
type approvalProvider struct {
turnIdx int32
}

func (approvalProvider) Name() string { return "appr" }
func (approvalProvider) Slug() string { return "appr" }
func (approvalProvider) BrandColor() lipgloss.Color { return lipgloss.Color("#000000") }
func (approvalProvider) ValidateKey(context.Context, string) error { return nil }
func (approvalProvider) ListModels(context.Context) ([]provider.Model, error) { return nil, nil }
func (approvalProvider) Pricing(string) (float64, float64) { return 0, 0 }
func (approvalProvider) ContextWindow(string) int { return 100_000 }
func (approvalProvider) SupportsTools(string) bool { return true }

func (a *approvalProvider) ChatCompletion(ctx context.Context, _ provider.ChatRequest) (<-chan provider.StreamEvent, error) {
atomic.AddInt32(&a.turnIdx, 1)
ch := make(chan provider.StreamEvent, 8)
go func() {
defer close(ch)
ch <- provider.StreamEvent{Type: provider.EventToolCallStart, ToolCall: &provider.ToolCallDelta{Index: 0, ID: "c1", Name: "fake_write"}}
ch <- provider.StreamEvent{Type: provider.EventToolCallDelta, ToolCall: &provider.ToolCallDelta{Index: 0, ArgumentsDelta: `{}`}}
ch <- provider.StreamEvent{Type: provider.EventToolCallEnd, ToolCall: &provider.ToolCallDelta{Index: 0}}
ch <- provider.StreamEvent{Type: provider.EventDone, Usage: &provider.Usage{InputTokens: 1, OutputTokens: 1}}
<-ctx.Done()
}()
return ch, nil
}

// fakeWriteTool requires approval so the agent's approver.Approve path
// fires. Execute blocks on ctx so the test can verify the approval
// modal's Hide on cancel without the tool ever running.
Expand Down
6 changes: 1 addition & 5 deletions internal/app/app_jobs_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func TestE2E_SpawnAgentToolViaSlashCommand(t *testing.T) {
if err != nil {
t.Fatalf("jobs.NewManager: %v", err)
}
defer mgr.Shutdown(2 * time.Second)
defer func() { _ = mgr.Shutdown(2 * time.Second) }()

// Build a minimal App by hand so we don't have to stand up every
// Deps field. The fields we need for this test are: deps.Jobs,
Expand Down Expand Up @@ -192,10 +192,6 @@ func TestE2E_SpawnAgentToolViaSlashCommand(t *testing.T) {
appMu.Lock()
_, _ = app.handleSlashCommand(cmd, args, "/spawn hi")
appMu.Unlock()
if false {
// silence unused-result lint; return values are meaningful to
// the Bubble Tea loop but not to us here
}

// (1) Conversation got the queued echo.
appMu.Lock()
Expand Down
6 changes: 3 additions & 3 deletions internal/app/app_lifecycle_parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func TestFirstVisibleProgressStopsThinkingSpinner(t *testing.T) {
func TestLeftArrowOpensAgentsOnlyFromEmptyIdleInput(t *testing.T) {
r := newTestApp(t)
mgr := wireJobsManagerForSlashTest(t, r)
t.Cleanup(func() { mgr.Shutdown(2 * time.Second) })
t.Cleanup(func() { _ = mgr.Shutdown(2 * time.Second) })
r.app.input.Reset()
r.app.handleKey(tea.KeyMsg{Type: tea.KeyLeft})
if !r.app.agentView.Visible() {
Expand All @@ -46,7 +46,7 @@ func TestLeftArrowOpensAgentsOnlyFromEmptyIdleInput(t *testing.T) {
func TestAgentWorkspaceTaskPromptCanClearReturnAndSpawn(t *testing.T) {
r := newTestApp(t)
mgr := wireJobsManagerForSlashTest(t, r)
t.Cleanup(func() { mgr.Shutdown(2 * time.Second) })
t.Cleanup(func() { _ = mgr.Shutdown(2 * time.Second) })

r.app.showAgentView()
r.app.handleKey(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("n")})
Expand Down Expand Up @@ -84,7 +84,7 @@ func TestAgentWorkspaceTaskPromptCanClearReturnAndSpawn(t *testing.T) {
func TestAgentWorkspaceListActionsAreNotSwallowedByTaskInput(t *testing.T) {
r := newTestApp(t)
mgr := wireJobsManagerForSlashTest(t, r)
t.Cleanup(func() { mgr.Shutdown(2 * time.Second) })
t.Cleanup(func() { _ = mgr.Shutdown(2 * time.Second) })

_, _ = r.app.handleSpawnCommand([]string{"inspect the renderer"})
r.app.showAgentView()
Expand Down
10 changes: 5 additions & 5 deletions internal/app/app_slashcmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1446,7 +1446,7 @@ func TestApp_Queue_ListDropAndClear(t *testing.T) {
func TestApp_Agents_ListUsesBackgroundJobs(t *testing.T) {
r := newTestApp(t)
mgr := wireJobsManagerForSlashTest(t, r)
defer mgr.Shutdown(2 * time.Second)
defer func() { _ = mgr.Shutdown(2 * time.Second) }()

snap, spawnErr := mgr.Spawn(jobs.SpawnRequest{
Prompt: "audit fixtures",
Expand All @@ -1470,7 +1470,7 @@ func TestApp_Agents_ListUsesBackgroundJobs(t *testing.T) {
func TestApp_Agents_DetailOpensJobsPanel(t *testing.T) {
r := newTestApp(t)
mgr := wireJobsManagerForSlashTest(t, r)
defer mgr.Shutdown(2 * time.Second)
defer func() { _ = mgr.Shutdown(2 * time.Second) }()

snap, spawnErr := mgr.Spawn(jobs.SpawnRequest{
Prompt: "inspect flaky test",
Expand All @@ -1490,7 +1490,7 @@ func TestApp_Agents_DetailOpensJobsPanel(t *testing.T) {
func TestApp_Agents_NotFoundUsesAgentLabel(t *testing.T) {
r := newTestApp(t)
mgr := wireJobsManagerForSlashTest(t, r)
defer mgr.Shutdown(2 * time.Second)
defer func() { _ = mgr.Shutdown(2 * time.Second) }()

r.app.handleSlashCommand("agents", []string{"missing"}, "/agents missing")
convContains(t, r.app, "[agent:missing not found]")
Expand All @@ -1499,7 +1499,7 @@ func TestApp_Agents_NotFoundUsesAgentLabel(t *testing.T) {
func TestApp_Agents_ViewDoesNotOverrideJobsPanelOverlay(t *testing.T) {
r := newTestApp(t)
mgr := wireJobsManagerForSlashTest(t, r)
defer mgr.Shutdown(2 * time.Second)
defer func() { _ = mgr.Shutdown(2 * time.Second) }()

snap, spawnErr := mgr.Spawn(jobs.SpawnRequest{
Prompt: "inspect overlay",
Expand Down Expand Up @@ -1633,7 +1633,7 @@ func TestApp_ResizeRecomposesBuiltInStatusLine(t *testing.T) {
func TestApp_HandleJobUpdate_IgnoresStaleSeqAndDedupesTerminal(t *testing.T) {
r := newTestApp(t)
mgr := wireJobsManagerForSlashTest(t, r)
defer mgr.Shutdown(2 * time.Second)
defer func() { _ = mgr.Shutdown(2 * time.Second) }()

snap, spawnErr := mgr.Spawn(jobs.SpawnRequest{
Prompt: "finish once",
Expand Down
22 changes: 12 additions & 10 deletions internal/app/mentions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import (
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

func TestExpandFileMentions(t *testing.T) {
root := t.TempDir()
os.WriteFile(filepath.Join(root, "main.go"), []byte("package main\nfunc main(){}"), 0o644)
os.MkdirAll(filepath.Join(root, "internal"), 0o755)
os.WriteFile(filepath.Join(root, "internal", "util.go"), []byte("package internal"), 0o644)
require.NoError(t, os.WriteFile(filepath.Join(root, "main.go"), []byte("package main\nfunc main(){}"), 0o644))
require.NoError(t, os.MkdirAll(filepath.Join(root, "internal"), 0o755))
require.NoError(t, os.WriteFile(filepath.Join(root, "internal", "util.go"), []byte("package internal"), 0o644))

prompt := "explain @main.go and @internal/util.go please"
expanded, attached := expandFileMentions(prompt, root)
Expand All @@ -32,7 +34,7 @@ func TestExpandFileMentions(t *testing.T) {

func TestExpandFileMentions_TrailingPunctuationAndDedup(t *testing.T) {
root := t.TempDir()
os.WriteFile(filepath.Join(root, "a.go"), []byte("AAA"), 0o644)
require.NoError(t, os.WriteFile(filepath.Join(root, "a.go"), []byte("AAA"), 0o644))
// Same file twice + trailing period.
_, attached := expandFileMentions("look at @a.go. and again @a.go", root)
if len(attached) != 1 || attached[0] != "a.go" {
Expand All @@ -42,7 +44,7 @@ func TestExpandFileMentions_TrailingPunctuationAndDedup(t *testing.T) {

func TestExpandFileMentions_IgnoresMissingAndEscapes(t *testing.T) {
root := t.TempDir()
os.WriteFile(filepath.Join(root, "real.go"), []byte("X"), 0o644)
require.NoError(t, os.WriteFile(filepath.Join(root, "real.go"), []byte("X"), 0o644))
prompt := "email me @someone and read @nope.go and @../../etc/passwd but also @real.go"
expanded, attached := expandFileMentions(prompt, root)
if len(attached) != 1 || attached[0] != "real.go" {
Expand All @@ -63,7 +65,7 @@ func TestExpandFileMentions_NoMentions(t *testing.T) {

func TestExpandFileMentions_SkipsBinary(t *testing.T) {
root := t.TempDir()
os.WriteFile(filepath.Join(root, "bin.dat"), []byte{1, 2, 0, 3, 4}, 0o644)
require.NoError(t, os.WriteFile(filepath.Join(root, "bin.dat"), []byte{1, 2, 0, 3, 4}, 0o644))
_, attached := expandFileMentions("check @bin.dat", root)
if len(attached) != 0 {
t.Fatalf("binary file should not be attached: %v", attached)
Expand All @@ -72,8 +74,8 @@ func TestExpandFileMentions_SkipsBinary(t *testing.T) {

func TestExpandFileMentions_RefusesDotEnv(t *testing.T) {
root := t.TempDir()
os.WriteFile(filepath.Join(root, ".env"), []byte("PACKETCODE_OPENAI_API_KEY=sk-secret"), 0o600)
os.WriteFile(filepath.Join(root, ".env.example"), []byte("PACKETCODE_OPENAI_API_KEY="), 0o600)
require.NoError(t, os.WriteFile(filepath.Join(root, ".env"), []byte("PACKETCODE_OPENAI_API_KEY=sk-secret"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(root, ".env.example"), []byte("PACKETCODE_OPENAI_API_KEY="), 0o600))
expanded, attached := expandFileMentions("keys in @.env and @.env.example", root)
if len(attached) != 1 || attached[0] != ".env.example" {
t.Fatalf("attached = %v, want only .env.example", attached)
Expand All @@ -87,7 +89,7 @@ func TestExpandFileMentions_RefusesSymlinkEscape(t *testing.T) {
outside := t.TempDir()
root := t.TempDir()
secret := filepath.Join(outside, "id_rsa")
os.WriteFile(secret, []byte("PRIVATE KEY MATERIAL"), 0o600)
require.NoError(t, os.WriteFile(secret, []byte("PRIVATE KEY MATERIAL"), 0o600))
link := filepath.Join(root, "notes.txt")
if err := os.Symlink(secret, link); err != nil {
t.Skipf("symlinks unavailable here: %v", err)
Expand All @@ -103,7 +105,7 @@ func TestExpandFileMentions_RefusesSymlinkEscape(t *testing.T) {

func TestExpandFileMentions_AllowsSymlinkInsideRoot(t *testing.T) {
root := t.TempDir()
os.WriteFile(filepath.Join(root, "real.go"), []byte("package real"), 0o644)
require.NoError(t, os.WriteFile(filepath.Join(root, "real.go"), []byte("package real"), 0o644))
if err := os.Symlink(filepath.Join(root, "real.go"), filepath.Join(root, "alias.go")); err != nil {
t.Skipf("symlinks unavailable here: %v", err)
}
Expand Down
5 changes: 3 additions & 2 deletions internal/app/picker_items.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,10 @@ func providerItems(regs []provider.Provider, cfg *config.Config, activeSlug stri
slug := p.Slug()
defModel := ""
keyStatus := "(no key)"
if slug == "ollama" {
switch slug {
case "ollama":
keyStatus = "local"
} else if slug == "codex" {
case "codex":
keyStatus = "ChatGPT login"
}
if cfg != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/app/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func TestPromptProvider_UsesCanonicalDisplayOrder(t *testing.T) {
openAI := strings.Index(text, "openai")
anthropic := strings.Index(text, "anthropic")
custom := strings.Index(text, "aa-custom")
if openAI < 0 || anthropic < 0 || custom < 0 || !(openAI < anthropic && anthropic < custom) {
if openAI < 0 || anthropic < 0 || custom < 0 || openAI >= anthropic || anthropic >= custom {
t.Fatalf("unexpected provider order:\n%s", text)
}
}
Expand Down
Loading
Loading