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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,11 @@ All notable changes to the SIN-Code unified binary will be documented in this fi
exercises symbols / hover / definition / references / format against this
repository. Added so the LSP client can be re-validated whenever `client.go`
changes.
- **Ecosystem cleanup (legacy Python bundle)** — removed the deprecated
`sin-code-bundle` package from `AllPythonPackages` in `cmd/sin-code/internal/update_phases.go`;
the `sin update` command no longer attempts to upgrade the superseded Python
companion. Fixed remaining `SIN-Code-Bundle` repo-name references in
`go.mod`, `self-update.doc.md`, and `harvest.doc.md` (M5 compliance).

### chore
- **#61** — `.gitignore`: ignore `cmd/sin-code/tui/.sin-code/` runtime
Expand Down
25 changes: 17 additions & 8 deletions cmd/sin-code/internal/attachments/attachments.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,17 @@
var (
ErrTooLarge = errors.New("attachment exceeds 50MB")
ErrNotFound = errors.New("attachment not found")

// Test hooks (overridden in tests).
osUserConfigDir = os.UserConfigDir
osMkdirAll = os.MkdirAll
osOpen = os.Open
osReadAllHook = func(r io.Reader) ([]byte, error) { return io.ReadAll(r) }
osWriteFileHook = os.WriteFile
osReadDir = os.ReadDir
)


Check failure on line 37 in cmd/sin-code/internal/attachments/attachments.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (gofmt)
type Attachment struct {
ID string `json:"id"`
Hash string `json:"hash"`
Expand All @@ -46,14 +55,14 @@
if err != nil {
return nil, err
}
if err := os.MkdirAll(dir, 0o755); err != nil {
if err := osMkdirAll(dir, 0o755); err != nil {
return nil, err
}
return &Store{baseDir: dir}, nil
}

func NewStoreAt(dir string) (*Store, error) {
if err := os.MkdirAll(dir, 0o755); err != nil {
if err := osMkdirAll(dir, 0o755); err != nil {
return nil, err
}
return &Store{baseDir: dir}, nil
Expand All @@ -67,7 +76,7 @@
if info.Size() > MaxSize {
return nil, ErrTooLarge
}
f, err := os.Open(srcPath)
f, err := osOpen(srcPath)
if err != nil {
return nil, err
}
Expand All @@ -80,7 +89,7 @@
return nil, ErrTooLarge
}
h := sha256.New()
buf, err := io.ReadAll(io.TeeReader(r, h))
buf, err := osReadAllHook(io.TeeReader(r, h))
if err != nil {
return nil, err
}
Expand All @@ -89,11 +98,11 @@
ext := extFor(mime, name)
relPath := hashHex[:2] + "/" + hashHex + ext
fullPath := filepath.Join(s.baseDir, relPath)
if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
if err := osMkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
return nil, err
}
if _, err := os.Stat(fullPath); errors.Is(err, os.ErrNotExist) {
if err := os.WriteFile(fullPath, buf, 0o644); err != nil {
if err := osWriteFileHook(fullPath, buf, 0o644); err != nil {
return nil, err
}
}
Expand All @@ -112,7 +121,7 @@

func (s *Store) Get(hash string) (*Attachment, error) {
dir := filepath.Join(s.baseDir, hash[:2])
entries, err := os.ReadDir(dir)
entries, err := osReadDir(dir)
if err != nil {
return nil, ErrNotFound
}
Expand Down Expand Up @@ -156,7 +165,7 @@
func (s *Store) BaseDir() string { return s.baseDir }

func defaultDir() (string, error) {
cfg, err := os.UserConfigDir()
cfg, err := osUserConfigDir()
if err != nil {
return "", err
}
Expand Down
223 changes: 223 additions & 0 deletions cmd/sin-code/internal/attachments/attachments_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@

import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"testing"
"time"
)

func TestDetectMIMEPNG(t *testing.T) {
Expand Down Expand Up @@ -244,3 +247,223 @@
t.Fatal(err)
}
}

func TestNewStoreDefaultDirError(t *testing.T) {
orig := osUserConfigDir
osUserConfigDir = func() (string, error) { return "", fmt.Errorf("no config dir") }
defer func() { osUserConfigDir = orig }()
if _, err := NewStore(); err == nil {
t.Fatal("expected error")
}
}

func TestNewStoreMkdirAllError(t *testing.T) {
orig := osMkdirAll
osMkdirAll = func(string, os.FileMode) error { return fmt.Errorf("mkdir failed") }
defer func() { osMkdirAll = orig }()
if _, err := NewStoreAt(t.TempDir()); err == nil {
t.Fatal("expected error")
}
}

func TestNewStoreMkdirAllError2(t *testing.T) {
origDir := osUserConfigDir
origMkdir := osMkdirAll
osUserConfigDir = func() (string, error) { return t.TempDir(), nil }
osMkdirAll = func(string, os.FileMode) error { return fmt.Errorf("mkdir failed") }
defer func() {
osUserConfigDir = origDir
osMkdirAll = origMkdir
}()
if _, err := NewStore(); err == nil {
t.Fatal("expected error")
}
}

func TestNewStoreSuccess(t *testing.T) {
origDir := osUserConfigDir
origMkdir := osMkdirAll
osUserConfigDir = func() (string, error) { return t.TempDir(), nil }
osMkdirAll = origMkdir
defer func() {
osUserConfigDir = origDir
osMkdirAll = origMkdir
}()
s, err := NewStore()
if err != nil {
t.Fatal(err)
}
if s.BaseDir() == "" {
t.Fatal("expected base dir")
}
}

func TestAttachTooLarge(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "big.txt")
_ = os.WriteFile(path, []byte("x"), 0o644)
s, _ := NewStoreAt(dir)
_, err := s.AttachReader(bytes.NewReader([]byte("x")), "big.txt", MaxSize+1)
if err != ErrTooLarge {
t.Fatalf("expected ErrTooLarge, got %v", err)
}
}

func TestAttachFileTooLarge(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "big.txt")
_ = os.WriteFile(path, bytes.Repeat([]byte("a"), MaxSize+1), 0o644)
s, _ := NewStoreAt(dir)
_, err := s.Attach(path)
if err != ErrTooLarge {
t.Fatalf("expected ErrTooLarge, got %v", err)
}
}

func TestAttachOpenError(t *testing.T) {
dir := t.TempDir()
s, _ := NewStoreAt(dir)
orig := osOpen
osOpen = func(string) (*os.File, error) { return nil, fmt.Errorf("open error") }
defer func() { osOpen = orig }()
path := filepath.Join(dir, "x.txt")
_ = os.WriteFile(path, []byte("x"), 0o644)
if _, err := s.Attach(path); err == nil {
t.Fatal("expected error")
}
}

func TestAttachReaderReadAllError(t *testing.T) {
dir := t.TempDir()
s, _ := NewStoreAt(dir)
orig := osReadAllHook
osReadAllHook = func(io.Reader) ([]byte, error) { return nil, fmt.Errorf("read error") }
defer func() { osReadAllHook = orig }()
if _, err := s.AttachReader(bytes.NewReader([]byte("x")), "x.txt", 1); err == nil {
t.Fatal("expected error")
}
}

func TestAttachReaderMkdirAllError(t *testing.T) {
dir := t.TempDir()
s, _ := NewStoreAt(dir)
orig := osMkdirAll
osMkdirAll = func(string, os.FileMode) error { return fmt.Errorf("mkdir error") }
defer func() { osMkdirAll = orig }()
if _, err := s.AttachReader(bytes.NewReader([]byte("x")), "x.txt", 1); err == nil {
t.Fatal("expected error")
}
}

func TestAttachReaderWriteFileError(t *testing.T) {
dir := t.TempDir()
s, _ := NewStoreAt(dir)
orig := osWriteFileHook
osWriteFileHook = func(string, []byte, os.FileMode) error { return fmt.Errorf("write error") }
defer func() { osWriteFileHook = orig }()
if _, err := s.AttachReader(bytes.NewReader([]byte("x")), "x.txt", 1); err == nil {
t.Fatal("expected error")
}
}

func TestGetNotFound(t *testing.T) {
dir := t.TempDir()
s, _ := NewStoreAt(dir)
if _, err := s.Get("aabbccdd"); err != ErrNotFound {
t.Fatalf("expected ErrNotFound, got %v", err)
}
}

func TestGetInfoError(t *testing.T) {
dir := t.TempDir()
s, _ := NewStoreAt(dir)
hash := "aa" + strings.Repeat("0", 62)
orig := osReadDir
osReadDir = func(string) ([]os.DirEntry, error) {
return []os.DirEntry{infoErrEntry(hash + "_link")}, nil
}
defer func() { osReadDir = orig }()
_, err := s.Get(hash)
if err != ErrNotFound {
t.Fatalf("expected ErrNotFound, got %v", err)
}
}

type infoErrEntry string

func (e infoErrEntry) Name() string { return string(e) }

Check failure on line 394 in cmd/sin-code/internal/attachments/attachments_test.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (gofmt)
func (e infoErrEntry) IsDir() bool { return false }
func (e infoErrEntry) Type() os.FileMode { return 0 }
func (e infoErrEntry) Info() (os.FileInfo, error) { return nil, fmt.Errorf("info error") }
func (e infoErrEntry) String() string { return string(e) }

func TestGetPrefixMismatch(t *testing.T) {
dir := t.TempDir()
s, _ := NewStoreAt(dir)
targetDir := filepath.Join(dir, "aa")
_ = os.MkdirAll(targetDir, 0o755)
_ = os.WriteFile(filepath.Join(targetDir, "bb_"), []byte("x"), 0o644)
if _, err := s.Get("aa"); err != ErrNotFound {
t.Fatalf("expected ErrNotFound, got %v", err)
}
}

func TestPruneExpired(t *testing.T) {
dir := t.TempDir()
s, _ := NewStoreAt(dir)
copyPath := filepath.Join(s.BaseDir(), "aa", "old.txt")
_ = os.MkdirAll(filepath.Dir(copyPath), 0o755)
_ = os.WriteFile(copyPath, []byte("old"), 0o644)
oldTime := time.Now().UTC().Add(-DefaultExpiry - time.Hour)
_ = os.Chtimes(copyPath, oldTime, oldTime)
n, err := s.Prune()
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("expected 1 pruned, got %d", n)
}
}

func TestDetectMIMEUnknownWithExt(t *testing.T) {
if got := detectMIME([]byte{0xFF, 0xFE, 0, 0}, "x.bin"); got != "application/octet-stream" {
t.Errorf("got %q", got)
}
}

func TestDetectMIMEUnknownNoExt(t *testing.T) {
if got := detectMIME([]byte{0xFF, 0xFE, 0, 0}, ""); got != "application/octet-stream" {
t.Errorf("got %q", got)
}
}

func TestExtForTextFromName(t *testing.T) {
if got := extFor("text/plain", "data.md"); got != ".md" {
t.Errorf("got %q", got)
}
}

func TestExtForUnknown(t *testing.T) {
if got := extFor("application/x-custom", "data.bin"); got != ".bin" {
t.Errorf("got %q", got)
}
}

func TestIsLikelyTextLong(t *testing.T) {
if !isLikelyText(bytes.Repeat([]byte("a"), 9000)) {
t.Error("long ascii should be text")
}
}

func TestIsLikelyTextControlChar(t *testing.T) {
if isLikelyText([]byte{0x01}) {
t.Error("control char should be binary")
}
}

func TestAttachmentMarkerDefault(t *testing.T) {
a := &Attachment{Name: "x", MIME: "application/x", Hash: "h", Size: 1}
if got := a.Marker(); !strings.Contains(got, "file:") {
t.Errorf("got %q", got)
}
}
2 changes: 1 addition & 1 deletion cmd/sin-code/internal/harvest.doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Fetches URLs with a local disk cache, structure extraction, change detection, an

```bash
# Fetch an API endpoint with JSON output
sin-code harvest --url https://api.github.com/repos/OpenSIN-Code/SIN-Code-Bundle --format json
sin-code harvest --url https://api.github.com/repos/OpenSIN-Code/SIN-Code --format json

# Check cache hit (run twice within 5 minutes)
sin-code harvest --url https://example.com
Expand Down
2 changes: 1 addition & 1 deletion cmd/sin-code/internal/self-update.doc.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Checks GitHub releases for a newer version of sin-code and installs it with auto

## What it does

- **Queries the GitHub Releases API** for the latest version of `OpenSIN-Code/SIN-Code-Bundle`.
- **Queries the GitHub Releases API** for the latest version of `OpenSIN-Code/SIN-Code`.
- **Auto-detects platform** (`runtime.GOOS` + `runtime.GOARCH`) to select the correct asset.
- **Downloads and extracts** the correct archive (`.tar.gz` for macOS/Linux, `.zip` for Windows).
- **Backups the current binary** before replacement and restores it if the update fails.
Expand Down
16 changes: 13 additions & 3 deletions cmd/sin-code/internal/trace/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@
// visible hang (mandate C6 hard-time-limit).
const defaultShutdownTimeout = 5 * time.Second

// Test hooks for error paths.
var (
stdouttraceNew = stdouttrace.New

Check failure on line 44 in cmd/sin-code/internal/trace/provider.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not properly formatted (gofmt)
otlptracehttpNew = otlptracehttp.New
resourceNew = resource.New
)

// ProviderConfig configures InitProvider.
type ProviderConfig struct {
ServiceName string // semconv service.name (required for prod)
Expand Down Expand Up @@ -78,10 +85,13 @@
cfg = &ProviderConfig{}
}
cfg.normalize()
if cfg.Exporter == "" {
cfg.Exporter = ExporterNoop
}

// Resource is mandatory even for noop — semconv attributes keep
// the trace dashboard readable when the exporter is OTLP.
res, err := resource.New(ctx,
res, err := resourceNew(ctx,
resource.WithAttributes(
semconv.ServiceName(cfg.ServiceName),
semconv.ServiceVersion(cfg.ServiceVersion),
Expand All @@ -94,7 +104,7 @@

switch cfg.Exporter {
case ExporterStdout:
exp, err := stdouttrace.New(stdouttrace.WithPrettyPrint(), stdouttrace.WithWriter(os.Stderr))
exp, err := stdouttraceNew(stdouttrace.WithPrettyPrint(), stdouttrace.WithWriter(os.Stderr))
if err != nil {
return nil, fmt.Errorf("trace: build stdout exporter: %w", err)
}
Expand All @@ -117,7 +127,7 @@
if h := os.Getenv("OTEL_EXPORTER_OTLP_HEADERS"); h != "" {
opts = append(opts, otlptracehttp.WithHeaders(parseHeaders(h)))
}
exp, err := otlptracehttp.New(ctx, opts...)
exp, err := otlptracehttpNew(ctx, opts...)
if err != nil {
return nil, fmt.Errorf("trace: build OTLP exporter: %w", err)
}
Expand Down
Loading
Loading