Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
run: |
total=$(go tool cover -func=coverage.out | awk '/^total:/ {gsub("%","",$3); print $3}')
# Current enforced coverage floor. Codex PRs raise this incrementally toward 90%.
min=50.0
min=55.0
awk -v t="$total" -v m="$min" 'BEGIN {
if (t+0 < m+0) {
printf "Coverage %.1f%% is below floor %.1f%%\n", t, m
Expand Down
43 changes: 43 additions & 0 deletions cmd/drift_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,46 @@ func TestDriftWarning_Fields(t *testing.T) {
t.Error("unexpected commits behind")
}
}

func TestResolveCodePaths(t *testing.T) {
tests := []struct {
name string
subsystem string
paths map[string][]string
wantPrefix string
}{
{
name: "configured path strips globs",
Comment on lines +93 to +100
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

The field name wantPrefix is a bit misleading here: the assertion checks for an exact match (p == tt.wantPrefix) rather than a prefix. Consider renaming it to something like wantPath/wantEntry (or update the assertion to actually check a prefix) to keep the test intent clear.

Copilot uses AI. Check for mistakes.
subsystem: "watching",
paths: map[string][]string{
"watching": {"watch/**", "cmd/hooks.go"},
},
wantPrefix: "watch/",
},
{
name: "fallback to guessed paths",
subsystem: "scanning",
paths: map[string][]string{},
wantPrefix: "scanner/",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := resolveCodePaths(tt.subsystem, tt.paths)
if len(got) == 0 {
t.Fatal("expected at least one path")
}
found := false
for _, p := range got {
if p == tt.wantPrefix {
found = true
break
}
}
if !found {
t.Fatalf("expected %q in paths %v", tt.wantPrefix, got)
}
})
}
}
90 changes: 90 additions & 0 deletions cmd/serve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package cmd

import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)

func TestWriteJSON_WritesIndentedJSONAndContentType(t *testing.T) {
tests := []struct {
name string
payload map[string]interface{}
}{
{
name: "simple map",
payload: map[string]interface{}{
"status": "ok",
"count": 2,
},
},
{
name: "nested payload",
payload: map[string]interface{}{
"outer": map[string]interface{}{"inner": true},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rr := httptest.NewRecorder()
writeJSON(rr, tt.payload)

if got := rr.Header().Get("Content-Type"); got != "application/json" {
t.Fatalf("expected content-type application/json, got %q", got)
}
if rr.Code != http.StatusOK {
t.Fatalf("expected status 200, got %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), "\n ") {
t.Fatalf("expected indented JSON output, got %q", rr.Body.String())
}

var decoded map[string]interface{}
if err := json.Unmarshal(rr.Body.Bytes(), &decoded); err != nil {
t.Fatalf("expected valid JSON body: %v", err)
}
})
}
}

func TestWriteError_WritesStatusJSONAndContentType(t *testing.T) {
tests := []struct {
name string
code int
msg string
wantCode int
wantSubstr string
}{
{name: "bad request", code: http.StatusBadRequest, msg: "bad input", wantCode: http.StatusBadRequest, wantSubstr: "bad input"},
{name: "internal server error", code: http.StatusInternalServerError, msg: "boom", wantCode: http.StatusInternalServerError, wantSubstr: "boom"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rr := httptest.NewRecorder()
writeError(rr, tt.code, tt.msg)

if rr.Code != tt.wantCode {
t.Fatalf("expected status %d, got %d", tt.wantCode, rr.Code)
}
if got := rr.Header().Get("Content-Type"); got != "application/json" {
t.Fatalf("expected content-type application/json, got %q", got)
}

var decoded map[string]string
if err := json.Unmarshal(rr.Body.Bytes(), &decoded); err != nil {
t.Fatalf("expected valid JSON body: %v", err)
}
if decoded["error"] != tt.msg {
t.Fatalf("expected error message %q, got %q", tt.msg, decoded["error"])
}
if !strings.Contains(rr.Body.String(), tt.wantSubstr) {
t.Fatalf("expected body to contain %q, got %q", tt.wantSubstr, rr.Body.String())
}
})
}
}
130 changes: 130 additions & 0 deletions cmd/skill_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package cmd

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

func TestRunSkill_PrintsUsageForUnknownOrMissingSubcommand(t *testing.T) {
tests := []struct {
name string
args []string
}{
{name: "no arguments", args: nil},
{name: "unknown subcommand", args: []string{"bogus"}},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out := captureOutput(func() {
RunSkill(tt.args, t.TempDir())
})

checks := []string{
"Usage: codemap skill <list|show|init>",
"Commands:",
"list",
"show <name>",
"init",
}
for _, check := range checks {
if !strings.Contains(out, check) {
t.Fatalf("expected output to contain %q, got:\n%s", check, out)
}
}
})
}
}

func TestRunSkillList_PrintsBuiltinSkills(t *testing.T) {
root := t.TempDir()

out := captureOutput(func() {
runSkillList(root)
})
Comment on lines +41 to +46
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

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

These tests invoke runSkillList/runSkillShow which call skills.LoadSkills(root). LoadSkills also reads global skills from the user’s home directory (~/.codemap/skills), so local developer environments with custom/overriding skills can change the output and make these assertions flaky (e.g., no remaining "[builtin]" lines or "Source: builtin"). Consider making the tests hermetic by setting HOME (and on Windows USERPROFILE) to t.TempDir() (or otherwise disabling global skill discovery) before running the commands.

Copilot uses AI. Check for mistakes.

checks := []string{
"Available skills",
"[builtin]",
}
for _, check := range checks {
if !strings.Contains(out, check) {
t.Fatalf("expected output to contain %q, got:\n%s", check, out)
}
}
}

func TestRunSkillShow_PrintsBuiltinSkillDetails(t *testing.T) {
root := t.TempDir()

out := captureOutput(func() {
runSkillShow(root, "explore")
})

checks := []string{
"# explore",
"Source: builtin",
"Description:",
}
for _, check := range checks {
if !strings.Contains(out, check) {
t.Fatalf("expected output to contain %q, got:\n%s", check, out)
}
}
}

func TestRunSkillInit_CreatesTemplateAndIsIdempotent(t *testing.T) {
root := t.TempDir()
path := filepath.Join(root, ".codemap", "skills", "my-skill.md")

tests := []struct {
name string
run func() string
wantContains []string
}{
{
name: "first run creates template",
run: func() string {
return captureOutput(func() {
runSkillInit(root)
})
},
wantContains: []string{
"Created skill template",
"Run 'codemap skill list'",
},
},
{
name: "second run reports already exists",
run: func() string {
return captureOutput(func() {
runSkillInit(root)
})
},
wantContains: []string{
"Skill template already exists",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out := tt.run()
for _, check := range tt.wantContains {
if !strings.Contains(out, check) {
t.Fatalf("expected output to contain %q, got:\n%s", check, out)
}
}
})
}

data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("expected template file at %s: %v", path, err)
}
if !strings.Contains(string(data), "name: my-skill") {
t.Fatalf("expected template content in %s, got:\n%s", path, string(data))
}
}
Loading