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
15 changes: 7 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ __v0_runtime_loader.js
.next/
.pytest_cache/
.ruff_cache/
.sin_memory.db
.sin-code/
.sin/
.snowflake/
.tdd-locks/
.v0-trash/
.vercel/
cmd/sin-code/tui/.sin-code/
cmd/sin-code/internal/.sin-code/
*.bak
*.bak-pre-v*
*.brain.db
Expand Down Expand Up @@ -56,11 +61,5 @@ SIN-Code-SCA-Tool/
SIN-Code-Security-Bundle/
sin-code.exe
sin-tui
# Runtime SQLite / index artifacts produced by the Go binary at runtime.
# These live under cmd/ (not under ~/.local/share/sin-code/) because the
# store open() uses a relative path joined against os.Getwd(). Follow-up
# issue #62 will move them to os.UserConfigDir(); until then we keep them
# out of the index.
cmd/sin-code/internal/.sin-code/
cmd/sin-code/tui/.sin-code/
.venv-forge-test/
*.cov.out
*.out
62 changes: 14 additions & 48 deletions .gitignore.doc.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,19 @@
# .gitignore — Repo-root ignore list for SIN-Code
# .gitignore

SPDX-License-Identifier: MIT
Ignores build artifacts, cache directories, local secrets, and per-run SQLite databases generated by the SIN-Code binary and its tests.

## What this file does
- One stop for ALL ignore rules for the host repo (`github.com/OpenSIN-Code/SIN-Code`).
- Companion conventions: see `AGENTS.md` §3 ("CoDocs Standard") and
`docs/repo-layout.md` if it exists.
- The Python companion at `src/sin_code_bundle/` has its own `.gitignore`
inside the `src/sin_code_bundle.egg-info/` block; we keep both in sync
manually on releases.
## Key rules

## Block structure
The file is organized in 6 visual blocks (separated by blank lines):
- `cmd/sin-code/tui/.sin-code/` — runtime TUI session/lessons databases (issue #61)
- `cmd/sin-code/internal/.sin-code/` — runtime code index used during tests (issue #62)
- `*.out` — coverage and temp output files
- `*.cov.out` — Go coverage profiles
- `coverage.*.out`, `coverage.out` — coverage reports
- `sin-code`, `sin-tui`, `sin-code.exe` — built binaries
- `.env*.local`, `.sin_memory.db` — local secrets/state
- `.claude/`, `.next/`, `node_modules`, `dist/` — IDE/build artifacts

| Lines | Block | Owner | Last touched |
| ------ | ----------------------------- | -------------- | ------------ |
| 1 | Docs header (CoDocs pointer) | Delqhi | 2026-06-13 |
| 2–26 | Build artifacts (binaries, | Delqhi | 2026-06-12 |
| | .bak, .brain.db, .test) | | |
| 27–35 | Section comments | Delqhi | 2026-06-12 |
| 36–57 | Standalone subprojects + | Delqhi | 2026-06-13 |
| | root binary + local TUI bin | | issue #61 |
| 58–61 | Runtime SQLite / index | Delqhi | 2026-06-13 |
| | artifacts under cmd/ | | issue #61 |
## See also

## How to add a new rule
1. **Pick the right block.** Use the table above; if the new rule doesn't
fit, propose a 7th block in the PR.
2. **Prefer anchor-form** (full path from repo root, no leading `/`) so the
rule is also caught by IDEs that ignore the leading-`/` Git-extension.
3. **Co-locate the doc change.** When you add a rule, update the "Block
structure" table in this file AND the "Last touched" column.
4. **Add a regression test** if the rule covers a runtime artifact (see
`tests/test_gitignore_tui_sin_code.py` for the template).
5. **Never use `*` at the start of a pattern** unless the rule is for a
pure-extension filter (e.g. `*.bak`); full-directory rules are clearer
as anchor-form.

## Known runtime-DB locations (do NOT remove without a migration plan)
- `cmd/sin-code/internal/.sin-code/index.bin` — produced by
`sin-code index build`; 1.8 MB blob, regenerated on demand. c06cf18.
- `cmd/sin-code/tui/.sin-code/lessons.db` — produced by
`internal/lessons/store.go`; ~24 KB; relative-path bug. Issue #62.
- `cmd/sin-code/tui/.sin-code/sessions.db` — produced by
`internal/session/store.go`; ~53 KB; relative-path bug. Issue #62.

## Cross-references
- AGENTS.md §3 (CoDocs Standard)
- AGENTS.md §7 (user-level config paths)
- commit `c06cf18` (precedent for `cmd/sin-code/internal/.sin-code/`)
- issue #61 (this fix)
- issue #62 (proposed: move all .sin-code paths to os.UserConfigDir())
- `AGENTS.md` §11.1 for known runtime DB locations.
- `docs/HOOKS.md` for lifecycle hooks that write state.
15 changes: 9 additions & 6 deletions cmd/sin-code/internal/adw.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,12 @@ import (
)

var (
adwPath string
adwFormat string
adwStrict bool
adwPath string
adwFormat string
adwStrict bool
adwAbs = filepath.Abs // test hook for filepath.Abs errors
adwWalk = filepath.Walk // test hook for filepath.Walk errors
adwInitialScore = 100 // test hook for the score > 100 cap
)

var AdwCmd = &cobra.Command{
Expand Down Expand Up @@ -50,7 +53,7 @@ Examples:
if len(args) > 0 {
path = args[0]
}
absPath, err := filepath.Abs(path)
absPath, err := adwAbs(path)
if err != nil {
return fmt.Errorf("invalid path: %w", err)
}
Expand Down Expand Up @@ -106,7 +109,7 @@ func scanDebt(root string, strict bool) *adwResult {
reverseDeps := make(map[string][]string) // import -> list of files importing it

// First pass: collect all files and their imports
err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
err := adwWalk(root, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
if info != nil && info.IsDir() {
base := filepath.Base(path)
Expand Down Expand Up @@ -223,7 +226,7 @@ func scanDebt(root string, strict bool) *adwResult {
}
}

score := 100 - critical*20 - high*10 - medium*5 - low*2
score := adwInitialScore - critical*20 - high*10 - medium*5 - low*2
if score < 0 {
score = 0
}
Expand Down
25 changes: 25 additions & 0 deletions cmd/sin-code/internal/adw_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,31 @@ func TestCheckTODOs(t *testing.T) {
}
}

func TestCheckTODOs_SkipsAdwTestFile(t *testing.T) {
issues := checkTODOs("internal/adw_test.go", "// TODO: should be ignored\n")
if len(issues) != 0 {
t.Errorf("expected 0 issues for adw_test.go, got %d", len(issues))
}
}

func TestCheckTODOs_SkipsQuotedString(t *testing.T) {
content := `package main
var msg = "TODO: not a real todo"
`
issues := checkTODOs("main.go", content)
if len(issues) != 0 {
t.Errorf("expected 0 issues for quoted string, got %d", len(issues))
}
}

func TestCheckTODOs_SkipsRawString(t *testing.T) {
content := "package main\nvar hint = `TODO: inside\ncontinues\n"
issues := checkTODOs("main.go", content)
if len(issues) != 0 {
t.Errorf("expected 0 issues for raw string start, got %d", len(issues))
}
}

func TestFindCircularDeps(t *testing.T) {
imports := map[string][]string{
"a.go": {"b.go"},
Expand Down
150 changes: 150 additions & 0 deletions cmd/sin-code/internal/agent_cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// SPDX-License-Identifier: MIT
// Purpose: Unit tests for agent subcommand helpers. (st-cov1)
package internal

import (
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
)

func TestFetchModels_HappyPath(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"data":[{"id":"gpt-4"},{"id":"gpt-3.5"}]}`))
}))
defer srv.Close()

models, err := fetchModels(srv.URL, "")
if err != nil {
t.Fatalf("fetchModels failed: %v", err)
}
if len(models) != 2 || models[0] != "gpt-4" {
t.Errorf("fetchModels = %v, want [gpt-4 gpt-3.5]", models)
}
}

func TestFetchModels_ErrorStatus(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusUnauthorized)
}))
defer srv.Close()

if _, err := fetchModels(srv.URL, ""); err == nil {
t.Fatal("expected error for non-200 status")
}
}

func TestOpenAgentInEditor_SeedsAndOpens(t *testing.T) {
oldEditor := os.Getenv("EDITOR")
t.Setenv("EDITOR", "true")
defer os.Setenv("EDITOR", oldEditor)

dir := t.TempDir()
t.Setenv("SIN_CODE_CONFIG_DIR", dir)

if err := openAgentInEditor("test-agent"); err != nil {
t.Fatalf("openAgentInEditor failed: %v", err)
}

cfgPath := filepath.Join(dir, "sin-code", "agents", "test-agent", "agent.toml")
if _, err := os.Stat(cfgPath); err != nil {
t.Errorf("expected config file at %s: %v", cfgPath, err)
}
}

func TestOpenAgentInEditor_MissingEditorFails(t *testing.T) {
oldEditor := os.Getenv("EDITOR")
t.Setenv("EDITOR", "nonexistent-binary-for-test-xyz")
defer os.Setenv("EDITOR", oldEditor)

dir := t.TempDir()
t.Setenv("SIN_CODE_CONFIG_DIR", dir)

if err := openAgentInEditor("another-agent"); err == nil {
t.Fatal("expected error for missing editor binary")
}
}

func TestOpenAgentInEditor_DefaultEditor(t *testing.T) {
oldEditor := os.Getenv("EDITOR")
t.Setenv("EDITOR", "")
defer os.Setenv("EDITOR", oldEditor)

binDir := t.TempDir()
fakeVim := filepath.Join(binDir, "vim")
if err := os.WriteFile(fakeVim, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write fake vim: %v", err)
}
oldPath := os.Getenv("PATH")
t.Setenv("PATH", binDir)
defer os.Setenv("PATH", oldPath)

dir := t.TempDir()
t.Setenv("SIN_CODE_CONFIG_DIR", dir)

if err := openAgentInEditor("default-editor-agent"); err != nil {
t.Fatalf("openAgentInEditor with default editor failed: %v", err)
}
}

func TestOpenAgentInEditor_InvalidName(t *testing.T) {
if err := openAgentInEditor("invalid/name"); err == nil {
t.Fatal("expected error for invalid agent name")
}
}

func TestApplyAgentEdits_InvalidName(t *testing.T) {
if err := applyAgentEdits("invalid/name", []string{"model=gpt-4"}); err == nil {
t.Fatal("expected error for invalid agent name")
}
}

func TestOpenAgentInEditor_MkdirError(t *testing.T) {
oldEditor := os.Getenv("EDITOR")
t.Setenv("EDITOR", "true")
defer os.Setenv("EDITOR", oldEditor)

dir := t.TempDir()
readOnly := filepath.Join(dir, "readonly")
if err := os.Mkdir(readOnly, 0o555); err != nil {
t.Fatalf("mkdir readonly: %v", err)
}
t.Setenv("SIN_CODE_CONFIG_DIR", readOnly)

if err := openAgentInEditor("mkdir-error-agent"); err == nil {
t.Fatal("expected error when mkdir fails")
}
}

func TestApplyAgentEdits_MkdirError(t *testing.T) {
dir := t.TempDir()
readOnly := filepath.Join(dir, "readonly")
if err := os.Mkdir(readOnly, 0o555); err != nil {
t.Fatalf("mkdir readonly: %v", err)
}
t.Setenv("SIN_CODE_CONFIG_DIR", readOnly)

if err := applyAgentEdits("mkdir-error-agent", []string{"model=gpt-4"}); err == nil {
t.Fatal("expected error when mkdir fails")
}
}

func TestApplyAgentEdits_CreateError(t *testing.T) {
dir := t.TempDir()
t.Setenv("SIN_CODE_CONFIG_DIR", dir)
agentDirPath := filepath.Join(dir, "sin-code", "agents", "create-error-agent")
if err := os.MkdirAll(agentDirPath, 0o755); err != nil {
t.Fatalf("mkdir agent dir: %v", err)
}
// Create agent.toml as a directory so os.Create fails.
if err := os.Mkdir(filepath.Join(agentDirPath, "agent.toml"), 0o755); err != nil {
t.Fatalf("mkdir agent.toml: %v", err)
}

if err := applyAgentEdits("create-error-agent", []string{"model=gpt-4"}); err == nil {
t.Fatal("expected error when create fails")
}
}
Loading
Loading