chore(ax): normalise test naming and usage annotations
Some checks failed
Security Scan / security (push) Failing after 10s
Test / test (push) Successful in 2m2s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-30 06:37:20 +00:00
parent a6c15980a3
commit dd59b177c6
81 changed files with 555 additions and 500 deletions

View file

@ -45,7 +45,7 @@ agentci:
assert.Equal(t, "claude", agent.Runner)
}
func TestLoadAgents_Good_MultipleAgents(t *testing.T) {
func TestLoadAgents_Good_MultipleAgents_Good(t *testing.T) {
cfg := newTestConfig(t, `
agentci:
agents:
@ -66,7 +66,7 @@ agentci:
assert.Contains(t, agents, "local-codex")
}
func TestLoadAgents_Good_SkipsInactive(t *testing.T) {
func TestLoadAgents_Good_SkipsInactive_Good(t *testing.T) {
cfg := newTestConfig(t, `
agentci:
agents:
@ -101,7 +101,7 @@ agentci:
assert.Contains(t, active, "active-agent")
}
func TestLoadAgents_Good_Defaults(t *testing.T) {
func TestLoadAgents_Good_Defaults_Good(t *testing.T) {
cfg := newTestConfig(t, `
agentci:
agents:
@ -119,14 +119,14 @@ agentci:
assert.Equal(t, "claude", agent.Runner)
}
func TestLoadAgents_Good_NoConfig(t *testing.T) {
func TestLoadAgents_Good_NoConfig_Good(t *testing.T) {
cfg := newTestConfig(t, "")
agents, err := LoadAgents(cfg)
require.NoError(t, err)
assert.Empty(t, agents)
}
func TestLoadAgents_Bad_MissingHost(t *testing.T) {
func TestLoadAgents_Bad_MissingHost_Good(t *testing.T) {
cfg := newTestConfig(t, `
agentci:
agents:
@ -139,7 +139,7 @@ agentci:
assert.Contains(t, err.Error(), "host is required")
}
func TestLoadAgents_Good_WithDualRun(t *testing.T) {
func TestLoadAgents_Good_WithDualRun_Good(t *testing.T) {
cfg := newTestConfig(t, `
agentci:
agents:
@ -176,7 +176,7 @@ agentci:
assert.Equal(t, "/etc/core/keys/clotho.pub", cc.SigningKeyPath)
}
func TestLoadClothoConfig_Good_Defaults(t *testing.T) {
func TestLoadClothoConfig_Good_Defaults_Good(t *testing.T) {
cfg := newTestConfig(t, "")
cc, err := LoadClothoConfig(cfg)
require.NoError(t, err)
@ -204,7 +204,7 @@ func TestSaveAgent_Good(t *testing.T) {
assert.Equal(t, "haiku", agents["new-agent"].Model)
}
func TestSaveAgent_Good_WithDualRun(t *testing.T) {
func TestSaveAgent_Good_WithDualRun_Good(t *testing.T) {
cfg := newTestConfig(t, "")
err := SaveAgent(cfg, "verified-agent", AgentConfig{
@ -222,7 +222,7 @@ func TestSaveAgent_Good_WithDualRun(t *testing.T) {
assert.True(t, agents["verified-agent"].DualRun)
}
func TestSaveAgent_Good_OmitsEmptyOptionals(t *testing.T) {
func TestSaveAgent_Good_OmitsEmptyOptionals_Good(t *testing.T) {
cfg := newTestConfig(t, "")
err := SaveAgent(cfg, "minimal", AgentConfig{
@ -256,7 +256,7 @@ agentci:
assert.Contains(t, agents, "to-keep")
}
func TestRemoveAgent_Bad_NotFound(t *testing.T) {
func TestRemoveAgent_Bad_NotFound_Good(t *testing.T) {
cfg := newTestConfig(t, `
agentci:
agents:
@ -269,7 +269,7 @@ agentci:
assert.Contains(t, err.Error(), "not found")
}
func TestRemoveAgent_Bad_NoAgents(t *testing.T) {
func TestRemoveAgent_Bad_NoAgents_Good(t *testing.T) {
cfg := newTestConfig(t, "")
err := RemoveAgent(cfg, "anything")
assert.Error(t, err)
@ -294,14 +294,14 @@ agentci:
assert.False(t, agents["agent-b"].Active)
}
func TestListAgents_Good_Empty(t *testing.T) {
func TestListAgents_Good_Empty_Good(t *testing.T) {
cfg := newTestConfig(t, "")
agents, err := ListAgents(cfg)
require.NoError(t, err)
assert.Empty(t, agents)
}
func TestRoundTrip_Good_SaveThenLoad(t *testing.T) {
func TestRoundTrip_Good_SaveThenLoad_Good(t *testing.T) {
cfg := newTestConfig(t, "")
err := SaveAgent(cfg, "alpha", AgentConfig{

View file

@ -20,7 +20,7 @@ func TestBuildSyncRepoList_Good(t *testing.T) {
assert.Equal(t, filepath.Join(basePath, "core"), repos[0].localPath)
}
func TestBuildSyncRepoList_Bad_PathTraversal(t *testing.T) {
func TestBuildSyncRepoList_Bad_PathTraversal_Good(t *testing.T) {
basePath := filepath.Join(t.TempDir(), "repos")
_, err := buildSyncRepoList(nil, []string{"../escape"}, basePath)
@ -28,7 +28,7 @@ func TestBuildSyncRepoList_Bad_PathTraversal(t *testing.T) {
assert.Contains(t, err.Error(), "invalid repo argument")
}
func TestBuildSyncRepoList_Good_OwnerRepo(t *testing.T) {
func TestBuildSyncRepoList_Good_OwnerRepo_Good(t *testing.T) {
basePath := filepath.Join(t.TempDir(), "repos")
repos, err := buildSyncRepoList(nil, []string{"Host-UK/core"}, basePath)
@ -38,7 +38,7 @@ func TestBuildSyncRepoList_Good_OwnerRepo(t *testing.T) {
assert.Equal(t, filepath.Join(basePath, "core"), repos[0].localPath)
}
func TestBuildSyncRepoList_Bad_PathTraversal_OwnerRepo(t *testing.T) {
func TestBuildSyncRepoList_Bad_PathTraversal_OwnerRepo_Good(t *testing.T) {
basePath := filepath.Join(t.TempDir(), "repos")
_, err := buildSyncRepoList(nil, []string{"host-uk/../escape"}, basePath)
@ -46,7 +46,7 @@ func TestBuildSyncRepoList_Bad_PathTraversal_OwnerRepo(t *testing.T) {
assert.Contains(t, err.Error(), "invalid repo argument")
}
func TestBuildSyncRepoList_Bad_PathTraversal_OwnerRepoEncoded(t *testing.T) {
func TestBuildSyncRepoList_Bad_PathTraversal_OwnerRepoEncoded_Good(t *testing.T) {
basePath := filepath.Join(t.TempDir(), "repos")
_, err := buildSyncRepoList(nil, []string{"host-uk%2F..%2Fescape"}, basePath)

View file

@ -20,7 +20,7 @@ func TestBuildRepoList_Good(t *testing.T) {
assert.Equal(t, filepath.Join(basePath, "core"), repos[0].localPath)
}
func TestBuildRepoList_Bad_PathTraversal(t *testing.T) {
func TestBuildRepoList_Bad_PathTraversal_Good(t *testing.T) {
basePath := filepath.Join(t.TempDir(), "repos")
_, err := buildRepoList(nil, []string{"../escape"}, basePath)
@ -28,7 +28,7 @@ func TestBuildRepoList_Bad_PathTraversal(t *testing.T) {
assert.Contains(t, err.Error(), "invalid repo argument")
}
func TestBuildRepoList_Good_OwnerRepo(t *testing.T) {
func TestBuildRepoList_Good_OwnerRepo_Good(t *testing.T) {
basePath := filepath.Join(t.TempDir(), "repos")
repos, err := buildRepoList(nil, []string{"Host-UK/core"}, basePath)
@ -38,7 +38,7 @@ func TestBuildRepoList_Good_OwnerRepo(t *testing.T) {
assert.Equal(t, filepath.Join(basePath, "core"), repos[0].localPath)
}
func TestBuildRepoList_Bad_PathTraversal_OwnerRepo(t *testing.T) {
func TestBuildRepoList_Bad_PathTraversal_OwnerRepo_Good(t *testing.T) {
basePath := filepath.Join(t.TempDir(), "repos")
_, err := buildRepoList(nil, []string{"host-uk/../escape"}, basePath)
@ -46,7 +46,7 @@ func TestBuildRepoList_Bad_PathTraversal_OwnerRepo(t *testing.T) {
assert.Contains(t, err.Error(), "invalid repo argument")
}
func TestBuildRepoList_Bad_PathTraversal_OwnerRepoEncoded(t *testing.T) {
func TestBuildRepoList_Bad_PathTraversal_OwnerRepoEncoded_Good(t *testing.T) {
basePath := filepath.Join(t.TempDir(), "repos")
_, err := buildRepoList(nil, []string{"host-uk%2F..%2Fescape"}, basePath)

View file

@ -35,7 +35,7 @@ func sampleBTCTalkPage(count int) string {
return page.String()
}
func TestBitcoinTalkCollector_Collect_Good_OnePage(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Good_OnePage_Good(t *testing.T) {
// Serve a single page with 5 posts (< 20, so collection stops after one page).
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
@ -75,7 +75,7 @@ func TestBitcoinTalkCollector_Collect_Good_OnePage(t *testing.T) {
}
}
func TestBitcoinTalkCollector_Collect_Good_PageLimit(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Good_PageLimit_Good(t *testing.T) {
pageCount := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pageCount++
@ -103,7 +103,7 @@ func TestBitcoinTalkCollector_Collect_Good_PageLimit(t *testing.T) {
assert.Equal(t, 2, pageCount)
}
func TestBitcoinTalkCollector_Collect_Good_CancelledContext(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Good_CancelledContext_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(sampleBTCTalkPage(5)))
@ -127,7 +127,7 @@ func TestBitcoinTalkCollector_Collect_Good_CancelledContext(t *testing.T) {
assert.Error(t, err)
}
func TestBitcoinTalkCollector_Collect_Bad_ServerError(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Bad_ServerError_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
@ -151,7 +151,7 @@ func TestBitcoinTalkCollector_Collect_Bad_ServerError(t *testing.T) {
assert.Equal(t, 1, result.Errors)
}
func TestBitcoinTalkCollector_Collect_Good_EmitsEvents(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Good_EmitsEvents_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(sampleBTCTalkPage(2)))
@ -209,7 +209,7 @@ func TestFetchPage_Good(t *testing.T) {
assert.Len(t, posts, 3)
}
func TestFetchPage_Bad_StatusCode(t *testing.T) {
func TestFetchPage_Bad_StatusCode_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
@ -224,7 +224,7 @@ func TestFetchPage_Bad_StatusCode(t *testing.T) {
assert.Error(t, err)
}
func TestFetchPage_Bad_InvalidHTML(t *testing.T) {
func TestFetchPage_Bad_InvalidHTML_Good(t *testing.T) {
// html.Parse is very forgiving, so serve an empty page.
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")

View file

@ -15,12 +15,12 @@ func TestBitcoinTalkCollector_Name_Good(t *testing.T) {
assert.Equal(t, "bitcointalk:12345", b.Name())
}
func TestBitcoinTalkCollector_Name_Good_URL(t *testing.T) {
func TestBitcoinTalkCollector_Name_Good_URL_Good(t *testing.T) {
b := &BitcoinTalkCollector{URL: "https://bitcointalk.org/index.php?topic=12345.0"}
assert.Equal(t, "bitcointalk:url", b.Name())
}
func TestBitcoinTalkCollector_Collect_Bad_NoTopicID(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Bad_NoTopicID_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
@ -29,7 +29,7 @@ func TestBitcoinTalkCollector_Collect_Bad_NoTopicID(t *testing.T) {
assert.Error(t, err)
}
func TestBitcoinTalkCollector_Collect_Good_DryRun(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Good_DryRun_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -72,7 +72,7 @@ func TestParsePostsFromHTML_Good(t *testing.T) {
assert.Contains(t, posts[1].Content, "Running bitcoin!")
}
func TestParsePostsFromHTML_Good_Empty(t *testing.T) {
func TestParsePostsFromHTML_Good_Empty_Good(t *testing.T) {
posts, err := ParsePostsFromHTML("<html><body></body></html>")
assert.NoError(t, err)
assert.Empty(t, posts)
@ -86,7 +86,7 @@ func TestFormatPostMarkdown_Good(t *testing.T) {
assert.Contains(t, md, "Hello, world!")
}
func TestFormatPostMarkdown_Good_NoDate(t *testing.T) {
func TestFormatPostMarkdown_Good_NoDate_Good(t *testing.T) {
md := FormatPostMarkdown(5, "user", "", "Content here")
assert.Contains(t, md, "# Post 5 by user")

View file

@ -56,13 +56,13 @@ func TestMergeResults_Good(t *testing.T) {
assert.Len(t, merged.Files, 3)
}
func TestMergeResults_Good_NilResults(t *testing.T) {
func TestMergeResults_Good_NilResults_Good(t *testing.T) {
r1 := &Result{Items: 3}
merged := MergeResults("test", r1, nil, nil)
assert.Equal(t, 3, merged.Items)
}
func TestMergeResults_Good_Empty(t *testing.T) {
func TestMergeResults_Good_Empty_Good(t *testing.T) {
merged := MergeResults("empty")
assert.Equal(t, 0, merged.Items)
assert.Equal(t, 0, merged.Errors)

View file

@ -17,7 +17,7 @@ import (
// --- GitHub collector: context cancellation and orchestration ---
func TestGitHubCollector_Collect_Good_ContextCancelledInLoop(t *testing.T) {
func TestGitHubCollector_Collect_Good_ContextCancelledInLoop_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = false
@ -33,7 +33,7 @@ func TestGitHubCollector_Collect_Good_ContextCancelledInLoop(t *testing.T) {
assert.NotNil(t, result)
}
func TestGitHubCollector_Collect_Good_IssuesOnlyDryRunProgress(t *testing.T) {
func TestGitHubCollector_Collect_Good_IssuesOnlyDryRunProgress_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -49,7 +49,7 @@ func TestGitHubCollector_Collect_Good_IssuesOnlyDryRunProgress(t *testing.T) {
assert.GreaterOrEqual(t, progressCount, 1)
}
func TestGitHubCollector_Collect_Good_PRsOnlyDryRunSkipsIssues(t *testing.T) {
func TestGitHubCollector_Collect_Good_PRsOnlyDryRunSkipsIssues_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -61,7 +61,7 @@ func TestGitHubCollector_Collect_Good_PRsOnlyDryRunSkipsIssues(t *testing.T) {
assert.Equal(t, 0, result.Items)
}
func TestGitHubCollector_Collect_Good_EmitsStartAndComplete(t *testing.T) {
func TestGitHubCollector_Collect_Good_EmitsStartAndComplete_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -78,7 +78,7 @@ func TestGitHubCollector_Collect_Good_EmitsStartAndComplete(t *testing.T) {
assert.Equal(t, 1, completes)
}
func TestGitHubCollector_Collect_Good_NilDispatcherHandled(t *testing.T) {
func TestGitHubCollector_Collect_Good_NilDispatcherHandled_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -91,7 +91,7 @@ func TestGitHubCollector_Collect_Good_NilDispatcherHandled(t *testing.T) {
assert.Equal(t, 0, result.Items)
}
func TestFormatIssueMarkdown_Good_NoBodyNoURL(t *testing.T) {
func TestFormatIssueMarkdown_Good_NoBodyNoURL_Good(t *testing.T) {
issue := ghIssue{
Number: 1,
Title: "No Body Issue",
@ -108,7 +108,7 @@ func TestFormatIssueMarkdown_Good_NoBodyNoURL(t *testing.T) {
// --- Market collector: fetchJSON edge cases ---
func TestFetchJSON_Bad_NonJSONBody(t *testing.T) {
func TestFetchJSON_Bad_NonJSONBody_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(`<html>not json</html>`))
@ -119,17 +119,17 @@ func TestFetchJSON_Bad_NonJSONBody(t *testing.T) {
assert.Error(t, err)
}
func TestFetchJSON_Bad_MalformedURL(t *testing.T) {
func TestFetchJSON_Bad_MalformedURL_Good(t *testing.T) {
_, err := fetchJSON[coinData](context.Background(), "://bad-url")
assert.Error(t, err)
}
func TestFetchJSON_Bad_ServerUnavailable(t *testing.T) {
func TestFetchJSON_Bad_ServerUnavailable_Good(t *testing.T) {
_, err := fetchJSON[coinData](context.Background(), "http://127.0.0.1:1")
assert.Error(t, err)
}
func TestFetchJSON_Bad_Non200StatusCode(t *testing.T) {
func TestFetchJSON_Bad_Non200StatusCode_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNotFound)
}))
@ -140,7 +140,7 @@ func TestFetchJSON_Bad_Non200StatusCode(t *testing.T) {
assert.Contains(t, err.Error(), "unexpected status code")
}
func TestMarketCollector_Collect_Bad_MissingCoinID(t *testing.T) {
func TestMarketCollector_Collect_Bad_MissingCoinID_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
@ -150,7 +150,7 @@ func TestMarketCollector_Collect_Bad_MissingCoinID(t *testing.T) {
assert.Contains(t, err.Error(), "coin ID is required")
}
func TestMarketCollector_Collect_Good_NoDispatcher(t *testing.T) {
func TestMarketCollector_Collect_Good_NoDispatcher_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
data := coinData{ID: "test", Symbol: "tst", Name: "Test",
@ -175,7 +175,7 @@ func TestMarketCollector_Collect_Good_NoDispatcher(t *testing.T) {
assert.Equal(t, 2, result.Items)
}
func TestMarketCollector_Collect_Bad_CurrentFetchFails(t *testing.T) {
func TestMarketCollector_Collect_Bad_CurrentFetchFails_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
@ -197,7 +197,7 @@ func TestMarketCollector_Collect_Bad_CurrentFetchFails(t *testing.T) {
assert.Equal(t, 1, result.Errors)
}
func TestMarketCollector_CollectHistorical_Good_DefaultDays(t *testing.T) {
func TestMarketCollector_CollectHistorical_Good_DefaultDays_Good(t *testing.T) {
callCount := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
@ -229,7 +229,7 @@ func TestMarketCollector_CollectHistorical_Good_DefaultDays(t *testing.T) {
assert.Equal(t, 3, result.Items)
}
func TestMarketCollector_CollectHistorical_Good_WithRateLimiter(t *testing.T) {
func TestMarketCollector_CollectHistorical_Good_WithRateLimiter_Good(t *testing.T) {
callCount := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
@ -263,7 +263,7 @@ func TestMarketCollector_CollectHistorical_Good_WithRateLimiter(t *testing.T) {
// --- State: error paths ---
func TestState_Load_Bad_MalformedJSON(t *testing.T) {
func TestState_Load_Bad_MalformedJSON_Good(t *testing.T) {
m := io.NewMockMedium()
m.Files["/state.json"] = `{invalid json`
@ -274,7 +274,7 @@ func TestState_Load_Bad_MalformedJSON(t *testing.T) {
// --- Process: additional coverage for uncovered branches ---
func TestHTMLToMarkdown_Good_PreCodeBlock(t *testing.T) {
func TestHTMLToMarkdown_Good_PreCodeBlock_Good(t *testing.T) {
input := `<pre>some code here</pre>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -282,7 +282,7 @@ func TestHTMLToMarkdown_Good_PreCodeBlock(t *testing.T) {
assert.Contains(t, result, "some code here")
}
func TestHTMLToMarkdown_Good_StrongAndEmElements(t *testing.T) {
func TestHTMLToMarkdown_Good_StrongAndEmElements_Good(t *testing.T) {
input := `<strong>bold</strong> and <em>italic</em>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -290,21 +290,21 @@ func TestHTMLToMarkdown_Good_StrongAndEmElements(t *testing.T) {
assert.Contains(t, result, "*italic*")
}
func TestHTMLToMarkdown_Good_InlineCode(t *testing.T) {
func TestHTMLToMarkdown_Good_InlineCode_Good(t *testing.T) {
input := `<code>var x = 1</code>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "`var x = 1`")
}
func TestHTMLToMarkdown_Good_AnchorWithHref(t *testing.T) {
func TestHTMLToMarkdown_Good_AnchorWithHref_Good(t *testing.T) {
input := `<a href="https://example.com">Click here</a>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "[Click here](https://example.com)")
}
func TestHTMLToMarkdown_Good_ScriptTagRemoved(t *testing.T) {
func TestHTMLToMarkdown_Good_ScriptTagRemoved_Good(t *testing.T) {
input := `<html><body><script>alert('xss')</script><p>Safe text</p></body></html>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -312,7 +312,7 @@ func TestHTMLToMarkdown_Good_ScriptTagRemoved(t *testing.T) {
assert.NotContains(t, result, "alert")
}
func TestHTMLToMarkdown_Good_H1H2H3Headers(t *testing.T) {
func TestHTMLToMarkdown_Good_H1H2H3Headers_Good(t *testing.T) {
input := `<h1>One</h1><h2>Two</h2><h3>Three</h3>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -321,7 +321,7 @@ func TestHTMLToMarkdown_Good_H1H2H3Headers(t *testing.T) {
assert.Contains(t, result, "### Three")
}
func TestHTMLToMarkdown_Good_MultiParagraph(t *testing.T) {
func TestHTMLToMarkdown_Good_MultiParagraph_Good(t *testing.T) {
input := `<p>First paragraph</p><p>Second paragraph</p>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -329,12 +329,12 @@ func TestHTMLToMarkdown_Good_MultiParagraph(t *testing.T) {
assert.Contains(t, result, "Second paragraph")
}
func TestJSONToMarkdown_Bad_Malformed(t *testing.T) {
func TestJSONToMarkdown_Bad_Malformed_Good(t *testing.T) {
_, err := JSONToMarkdown(`{invalid}`)
assert.Error(t, err)
}
func TestJSONToMarkdown_Good_FlatObject(t *testing.T) {
func TestJSONToMarkdown_Good_FlatObject_Good(t *testing.T) {
input := `{"name": "Alice", "age": 30}`
result, err := JSONToMarkdown(input)
require.NoError(t, err)
@ -342,7 +342,7 @@ func TestJSONToMarkdown_Good_FlatObject(t *testing.T) {
assert.Contains(t, result, "**age:** 30")
}
func TestJSONToMarkdown_Good_ScalarList(t *testing.T) {
func TestJSONToMarkdown_Good_ScalarList_Good(t *testing.T) {
input := `["hello", "world"]`
result, err := JSONToMarkdown(input)
require.NoError(t, err)
@ -350,14 +350,14 @@ func TestJSONToMarkdown_Good_ScalarList(t *testing.T) {
assert.Contains(t, result, "- world")
}
func TestJSONToMarkdown_Good_ObjectContainingArray(t *testing.T) {
func TestJSONToMarkdown_Good_ObjectContainingArray_Good(t *testing.T) {
input := `{"items": [1, 2, 3]}`
result, err := JSONToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "**items:**")
}
func TestProcessor_Process_Bad_MissingDir(t *testing.T) {
func TestProcessor_Process_Bad_MissingDir_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
@ -367,7 +367,7 @@ func TestProcessor_Process_Bad_MissingDir(t *testing.T) {
assert.Contains(t, err.Error(), "directory is required")
}
func TestProcessor_Process_Good_DryRunEmitsProgress(t *testing.T) {
func TestProcessor_Process_Good_DryRunEmitsProgress_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -383,7 +383,7 @@ func TestProcessor_Process_Good_DryRunEmitsProgress(t *testing.T) {
assert.Equal(t, 1, progressCount)
}
func TestProcessor_Process_Good_SkipsUnsupportedExtension(t *testing.T) {
func TestProcessor_Process_Good_SkipsUnsupportedExtension_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/data.csv"] = `a,b,c`
@ -399,7 +399,7 @@ func TestProcessor_Process_Good_SkipsUnsupportedExtension(t *testing.T) {
assert.Equal(t, 1, result.Skipped)
}
func TestProcessor_Process_Good_MarkdownPassthroughTrimmed(t *testing.T) {
func TestProcessor_Process_Good_MarkdownPassthroughTrimmed_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/readme.md"] = `# Hello World `
@ -418,7 +418,7 @@ func TestProcessor_Process_Good_MarkdownPassthroughTrimmed(t *testing.T) {
assert.Equal(t, "# Hello World", content)
}
func TestProcessor_Process_Good_HTMExtensionHandled(t *testing.T) {
func TestProcessor_Process_Good_HTMExtensionHandled_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/page.htm"] = `<h1>HTM File</h1>`
@ -433,7 +433,7 @@ func TestProcessor_Process_Good_HTMExtensionHandled(t *testing.T) {
assert.Equal(t, 1, result.Items)
}
func TestProcessor_Process_Good_NilDispatcherHandled(t *testing.T) {
func TestProcessor_Process_Good_NilDispatcherHandled_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/test.html"] = `<p>Text</p>`
@ -451,12 +451,12 @@ func TestProcessor_Process_Good_NilDispatcherHandled(t *testing.T) {
// --- BitcoinTalk: additional edge cases ---
func TestBitcoinTalkCollector_Name_Good_EmptyTopicAndURL(t *testing.T) {
func TestBitcoinTalkCollector_Name_Good_EmptyTopicAndURL_Good(t *testing.T) {
b := &BitcoinTalkCollector{}
assert.Equal(t, "bitcointalk:", b.Name())
}
func TestBitcoinTalkCollector_Collect_Good_NilDispatcherHandled(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Good_NilDispatcherHandled_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(sampleBTCTalkPage(2)))
@ -480,7 +480,7 @@ func TestBitcoinTalkCollector_Collect_Good_NilDispatcherHandled(t *testing.T) {
assert.Equal(t, 2, result.Items)
}
func TestBitcoinTalkCollector_Collect_Good_DryRunEmitsProgress(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Good_DryRunEmitsProgress_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -496,7 +496,7 @@ func TestBitcoinTalkCollector_Collect_Good_DryRunEmitsProgress(t *testing.T) {
assert.True(t, progressEmitted)
}
func TestParsePostsFromHTML_Good_PostWithNoInnerContent(t *testing.T) {
func TestParsePostsFromHTML_Good_PostWithNoInnerContent_Good(t *testing.T) {
htmlContent := `<html><body>
<div class="post">
<div class="poster_info">user1</div>
@ -507,7 +507,7 @@ func TestParsePostsFromHTML_Good_PostWithNoInnerContent(t *testing.T) {
assert.Empty(t, posts)
}
func TestFormatPostMarkdown_Good_WithDateContent(t *testing.T) {
func TestFormatPostMarkdown_Good_WithDateContent_Good(t *testing.T) {
md := FormatPostMarkdown(1, "alice", "2025-01-15", "Hello world")
assert.Contains(t, md, "# Post 1 by alice")
assert.Contains(t, md, "**Date:** 2025-01-15")
@ -516,7 +516,7 @@ func TestFormatPostMarkdown_Good_WithDateContent(t *testing.T) {
// --- Papers collector: edge cases ---
func TestPapersCollector_Collect_Good_DryRunEmitsProgress(t *testing.T) {
func TestPapersCollector_Collect_Good_DryRunEmitsProgress_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -532,7 +532,7 @@ func TestPapersCollector_Collect_Good_DryRunEmitsProgress(t *testing.T) {
assert.True(t, progressEmitted)
}
func TestPapersCollector_Collect_Good_NilDispatcherIACR(t *testing.T) {
func TestPapersCollector_Collect_Good_NilDispatcherIACR_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(sampleIACRHTML))
@ -556,7 +556,7 @@ func TestPapersCollector_Collect_Good_NilDispatcherIACR(t *testing.T) {
assert.Equal(t, 2, result.Items)
}
func TestArXivEntryToPaper_Good_NoAlternateLink(t *testing.T) {
func TestArXivEntryToPaper_Good_NoAlternateLink_Good(t *testing.T) {
entry := arxivEntry{
ID: "http://arxiv.org/abs/2501.99999v1",
Title: "No Alternate",
@ -571,7 +571,7 @@ func TestArXivEntryToPaper_Good_NoAlternateLink(t *testing.T) {
// --- Excavator: additional edge cases ---
func TestExcavator_Run_Good_ResumeLoadError(t *testing.T) {
func TestExcavator_Run_Good_ResumeLoadError_Good(t *testing.T) {
m := io.NewMockMedium()
m.Files["/output/.collect-state.json"] = `{invalid`
@ -591,7 +591,7 @@ func TestExcavator_Run_Good_ResumeLoadError(t *testing.T) {
// --- RateLimiter: additional edge cases ---
func TestRateLimiter_Wait_Good_QuickSuccessiveCallsAfterDelay(t *testing.T) {
func TestRateLimiter_Wait_Good_QuickSuccessiveCallsAfterDelay_Good(t *testing.T) {
rl := NewRateLimiter()
rl.SetDelay("fast", 1*time.Millisecond)
@ -610,7 +610,7 @@ func TestRateLimiter_Wait_Good_QuickSuccessiveCallsAfterDelay(t *testing.T) {
// --- FormatMarketSummary: with empty market data values ---
func TestFormatMarketSummary_Good_ZeroRank(t *testing.T) {
func TestFormatMarketSummary_Good_ZeroRank_Good(t *testing.T) {
data := &coinData{
Name: "Tiny Token",
Symbol: "tiny",
@ -624,7 +624,7 @@ func TestFormatMarketSummary_Good_ZeroRank(t *testing.T) {
assert.NotContains(t, summary, "Market Cap Rank")
}
func TestFormatMarketSummary_Good_ZeroSupply(t *testing.T) {
func TestFormatMarketSummary_Good_ZeroSupply_Good(t *testing.T) {
data := &coinData{
Name: "Zero Supply",
Symbol: "zs",
@ -638,7 +638,7 @@ func TestFormatMarketSummary_Good_ZeroSupply(t *testing.T) {
assert.NotContains(t, summary, "Total Supply")
}
func TestFormatMarketSummary_Good_NoLastUpdated(t *testing.T) {
func TestFormatMarketSummary_Good_NoLastUpdated_Good(t *testing.T) {
data := &coinData{
Name: "No Update",
Symbol: "nu",

View file

@ -86,7 +86,7 @@ type errorLimiterWaiter struct{}
// --- Processor: list error ---
func TestProcessor_Process_Bad_ListError(t *testing.T) {
func TestProcessor_Process_Bad_ListError_Good(t *testing.T) {
em := &errorMedium{MockMedium: io.NewMockMedium(), listErr: testErr("list denied")}
cfg := &Config{Output: em, OutputDir: "/output", Dispatcher: NewDispatcher()}
@ -98,7 +98,7 @@ func TestProcessor_Process_Bad_ListError(t *testing.T) {
// --- Processor: ensureDir error ---
func TestProcessor_Process_Bad_EnsureDirError(t *testing.T) {
func TestProcessor_Process_Bad_EnsureDirError_Good(t *testing.T) {
em := &errorMedium{MockMedium: io.NewMockMedium(), ensureDirErr: testErr("mkdir denied")}
// Need to ensure List returns entries
em.MockMedium.Dirs["/input"] = true
@ -114,7 +114,7 @@ func TestProcessor_Process_Bad_EnsureDirError(t *testing.T) {
// --- Processor: context cancellation during processing ---
func TestProcessor_Process_Bad_ContextCancelledDuringLoop(t *testing.T) {
func TestProcessor_Process_Bad_ContextCancelledDuringLoop_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/a.html"] = "<h1>Test</h1>"
@ -133,7 +133,7 @@ func TestProcessor_Process_Bad_ContextCancelledDuringLoop(t *testing.T) {
// --- Processor: read error during file processing ---
func TestProcessor_Process_Bad_ReadError(t *testing.T) {
func TestProcessor_Process_Bad_ReadError_Good(t *testing.T) {
em := &errorMedium{MockMedium: io.NewMockMedium(), readErr: testErr("read denied")}
em.MockMedium.Dirs["/input"] = true
em.MockMedium.Files["/input/test.html"] = "<h1>Test</h1>"
@ -148,7 +148,7 @@ func TestProcessor_Process_Bad_ReadError(t *testing.T) {
// --- Processor: JSON conversion error ---
func TestProcessor_Process_Bad_InvalidJSONFile(t *testing.T) {
func TestProcessor_Process_Bad_InvalidJSONFile_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/bad.json"] = "not valid json {"
@ -166,7 +166,7 @@ func TestProcessor_Process_Bad_InvalidJSONFile(t *testing.T) {
// --- Processor: write error during output ---
func TestProcessor_Process_Bad_WriteError(t *testing.T) {
func TestProcessor_Process_Bad_WriteError_Good(t *testing.T) {
em := &errorMedium{MockMedium: io.NewMockMedium(), writeErr: testErr("disk full")}
em.MockMedium.Dirs["/input"] = true
em.MockMedium.Files["/input/page.html"] = "<h1>Title</h1>"
@ -181,7 +181,7 @@ func TestProcessor_Process_Bad_WriteError(t *testing.T) {
// --- Processor: successful processing with events ---
func TestProcessor_Process_Good_EmitsItemAndComplete(t *testing.T) {
func TestProcessor_Process_Good_EmitsItemAndComplete_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/page.html"] = "<h1>Title</h1><p>Body</p>"
@ -201,7 +201,7 @@ func TestProcessor_Process_Good_EmitsItemAndComplete(t *testing.T) {
// --- Papers: with rate limiter that fails ---
func TestPapersCollector_CollectIACR_Bad_LimiterError(t *testing.T) {
func TestPapersCollector_CollectIACR_Bad_LimiterError_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = NewRateLimiter()
@ -215,7 +215,7 @@ func TestPapersCollector_CollectIACR_Bad_LimiterError(t *testing.T) {
assert.Error(t, err)
}
func TestPapersCollector_CollectArXiv_Bad_LimiterError(t *testing.T) {
func TestPapersCollector_CollectArXiv_Bad_LimiterError_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = NewRateLimiter()
@ -231,7 +231,7 @@ func TestPapersCollector_CollectArXiv_Bad_LimiterError(t *testing.T) {
// --- Papers: IACR with bad HTML response ---
func TestPapersCollector_CollectIACR_Bad_InvalidHTML(t *testing.T) {
func TestPapersCollector_CollectIACR_Bad_InvalidHTML_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
// Serve valid-ish HTML but with no papers - the parse succeeds but returns empty.
@ -256,7 +256,7 @@ func TestPapersCollector_CollectIACR_Bad_InvalidHTML(t *testing.T) {
// --- Papers: IACR write error ---
func TestPapersCollector_CollectIACR_Bad_WriteError(t *testing.T) {
func TestPapersCollector_CollectIACR_Bad_WriteError_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(sampleIACRHTML))
@ -280,7 +280,7 @@ func TestPapersCollector_CollectIACR_Bad_WriteError(t *testing.T) {
// --- Papers: IACR EnsureDir error ---
func TestPapersCollector_CollectIACR_Bad_EnsureDirError(t *testing.T) {
func TestPapersCollector_CollectIACR_Bad_EnsureDirError_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(sampleIACRHTML))
@ -304,7 +304,7 @@ func TestPapersCollector_CollectIACR_Bad_EnsureDirError(t *testing.T) {
// --- Papers: arXiv write error ---
func TestPapersCollector_CollectArXiv_Bad_WriteError(t *testing.T) {
func TestPapersCollector_CollectArXiv_Bad_WriteError_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write([]byte(sampleArXivXML))
@ -328,7 +328,7 @@ func TestPapersCollector_CollectArXiv_Bad_WriteError(t *testing.T) {
// --- Papers: arXiv EnsureDir error ---
func TestPapersCollector_CollectArXiv_Bad_EnsureDirError(t *testing.T) {
func TestPapersCollector_CollectArXiv_Bad_EnsureDirError_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write([]byte(sampleArXivXML))
@ -352,7 +352,7 @@ func TestPapersCollector_CollectArXiv_Bad_EnsureDirError(t *testing.T) {
// --- Papers: collectAll with dispatcher events ---
func TestPapersCollector_CollectAll_Good_WithDispatcher(t *testing.T) {
func TestPapersCollector_CollectAll_Good_WithDispatcher_Good(t *testing.T) {
callCount := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
@ -387,7 +387,7 @@ func TestPapersCollector_CollectAll_Good_WithDispatcher(t *testing.T) {
// --- Papers: IACR with events on item emit ---
func TestPapersCollector_CollectIACR_Good_EmitsItemEvents(t *testing.T) {
func TestPapersCollector_CollectIACR_Good_EmitsItemEvents_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(sampleIACRHTML))
@ -415,7 +415,7 @@ func TestPapersCollector_CollectIACR_Good_EmitsItemEvents(t *testing.T) {
// --- Papers: arXiv with events on item emit ---
func TestPapersCollector_CollectArXiv_Good_EmitsItemEvents(t *testing.T) {
func TestPapersCollector_CollectArXiv_Good_EmitsItemEvents_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write([]byte(sampleArXivXML))
@ -443,7 +443,7 @@ func TestPapersCollector_CollectArXiv_Good_EmitsItemEvents(t *testing.T) {
// --- Market: collectCurrent write error (summary path) ---
func TestMarketCollector_Collect_Bad_WriteError(t *testing.T) {
func TestMarketCollector_Collect_Bad_WriteError_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if strings.Contains(r.URL.Path, "/market_chart") {
@ -479,7 +479,7 @@ func TestMarketCollector_Collect_Bad_WriteError(t *testing.T) {
// --- Market: EnsureDir error ---
func TestMarketCollector_Collect_Bad_EnsureDirError(t *testing.T) {
func TestMarketCollector_Collect_Bad_EnsureDirError_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(coinData{ID: "bitcoin"})
@ -502,7 +502,7 @@ func TestMarketCollector_Collect_Bad_EnsureDirError(t *testing.T) {
// --- Market: collectCurrent with limiter wait error ---
func TestMarketCollector_Collect_Bad_LimiterError(t *testing.T) {
func TestMarketCollector_Collect_Bad_LimiterError_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(coinData{ID: "bitcoin"})
@ -529,7 +529,7 @@ func TestMarketCollector_Collect_Bad_LimiterError(t *testing.T) {
// --- Market: collectHistorical with custom FromDate ---
func TestMarketCollector_Collect_Good_HistoricalCustomDate(t *testing.T) {
func TestMarketCollector_Collect_Good_HistoricalCustomDate_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if strings.Contains(r.URL.Path, "/market_chart") {
@ -564,7 +564,7 @@ func TestMarketCollector_Collect_Good_HistoricalCustomDate(t *testing.T) {
// --- BitcoinTalk: EnsureDir error ---
func TestBitcoinTalkCollector_Collect_Bad_EnsureDirError(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Bad_EnsureDirError_Good(t *testing.T) {
em := &errorMedium{MockMedium: io.NewMockMedium(), ensureDirErr: testErr("mkdir denied")}
cfg := &Config{Output: em, OutputDir: "/output", Dispatcher: NewDispatcher()}
cfg.Limiter = nil
@ -577,7 +577,7 @@ func TestBitcoinTalkCollector_Collect_Bad_EnsureDirError(t *testing.T) {
// --- BitcoinTalk: limiter error ---
func TestBitcoinTalkCollector_Collect_Bad_LimiterError(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Bad_LimiterError_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = NewRateLimiter()
@ -593,7 +593,7 @@ func TestBitcoinTalkCollector_Collect_Bad_LimiterError(t *testing.T) {
// --- BitcoinTalk: write error during post saving ---
func TestBitcoinTalkCollector_Collect_Bad_WriteErrorOnPosts(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Bad_WriteErrorOnPosts_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte(sampleBTCTalkPage(3)))
@ -618,7 +618,7 @@ func TestBitcoinTalkCollector_Collect_Bad_WriteErrorOnPosts(t *testing.T) {
// --- BitcoinTalk: fetchPage with bad HTTP status ---
func TestBitcoinTalkCollector_FetchPage_Bad_NonOKStatus(t *testing.T) {
func TestBitcoinTalkCollector_FetchPage_Bad_NonOKStatus_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusForbidden)
}))
@ -632,7 +632,7 @@ func TestBitcoinTalkCollector_FetchPage_Bad_NonOKStatus(t *testing.T) {
// --- BitcoinTalk: fetchPage with request error ---
func TestBitcoinTalkCollector_FetchPage_Bad_RequestError(t *testing.T) {
func TestBitcoinTalkCollector_FetchPage_Bad_RequestError_Good(t *testing.T) {
old := httpClient
httpClient = &http.Client{Transport: &rewriteTransport{target: "http://127.0.0.1:1"}} // Connection refused
defer func() { httpClient = old }()
@ -645,7 +645,7 @@ func TestBitcoinTalkCollector_FetchPage_Bad_RequestError(t *testing.T) {
// --- BitcoinTalk: fetchPage with valid but empty page ---
func TestBitcoinTalkCollector_FetchPage_Good_EmptyPage(t *testing.T) {
func TestBitcoinTalkCollector_FetchPage_Good_EmptyPage_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
_, _ = w.Write([]byte("<html><body></body></html>"))
@ -664,7 +664,7 @@ func TestBitcoinTalkCollector_FetchPage_Good_EmptyPage(t *testing.T) {
// --- BitcoinTalk: Collect with fetch error + dispatcher ---
func TestBitcoinTalkCollector_Collect_Bad_FetchErrorWithDispatcher(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Bad_FetchErrorWithDispatcher_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
@ -691,7 +691,7 @@ func TestBitcoinTalkCollector_Collect_Bad_FetchErrorWithDispatcher(t *testing.T)
// --- State: Save with a populated state ---
func TestState_Save_Good_RoundTrip(t *testing.T) {
func TestState_Save_Good_RoundTrip_Good(t *testing.T) {
m := io.NewMockMedium()
s := NewState(m, "/data/state.json")
@ -717,7 +717,7 @@ func TestState_Save_Good_RoundTrip(t *testing.T) {
// --- GitHub: Collect with Repo set triggers collectIssues/collectPRs (which fail via gh) ---
func TestGitHubCollector_Collect_Bad_GhNotAuthenticated(t *testing.T) {
func TestGitHubCollector_Collect_Bad_GhNotAuthenticated_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = nil
@ -737,7 +737,7 @@ func TestGitHubCollector_Collect_Bad_GhNotAuthenticated(t *testing.T) {
// --- GitHub: Collect IssuesOnly triggers only issues, not PRs ---
func TestGitHubCollector_Collect_Bad_IssuesOnlyGhFails(t *testing.T) {
func TestGitHubCollector_Collect_Bad_IssuesOnlyGhFails_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = nil
@ -750,7 +750,7 @@ func TestGitHubCollector_Collect_Bad_IssuesOnlyGhFails(t *testing.T) {
// --- GitHub: Collect PRsOnly triggers only PRs, not issues ---
func TestGitHubCollector_Collect_Bad_PRsOnlyGhFails(t *testing.T) {
func TestGitHubCollector_Collect_Bad_PRsOnlyGhFails_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = nil
@ -763,7 +763,7 @@ func TestGitHubCollector_Collect_Bad_PRsOnlyGhFails(t *testing.T) {
// --- extractText: text before a br/p/div element adds newline ---
func TestExtractText_Good_TextBeforeBR(t *testing.T) {
func TestExtractText_Good_TextBeforeBR_Good(t *testing.T) {
htmlStr := `<div class="inner">Hello<br>World<p>End</p></div>`
posts, err := ParsePostsFromHTML(fmt.Sprintf(`<html><body><div class="post"><div class="inner">%s</div></div></body></html>`,
"First text<br>Second text<div>Third text</div>"))
@ -777,7 +777,7 @@ func TestExtractText_Good_TextBeforeBR(t *testing.T) {
// --- ParsePostsFromHTML: posts with full structure ---
func TestParsePostsFromHTML_Good_FullStructure(t *testing.T) {
func TestParsePostsFromHTML_Good_FullStructure_Good(t *testing.T) {
htmlContent := `<html><body>
<div class="post">
<div class="poster_info">TestAuthor</div>
@ -796,7 +796,7 @@ func TestParsePostsFromHTML_Good_FullStructure(t *testing.T) {
// --- getChildrenText: nested element node path ---
func TestHTMLToMarkdown_Good_NestedElements(t *testing.T) {
func TestHTMLToMarkdown_Good_NestedElements_Good(t *testing.T) {
// <a> with nested <span> triggers getChildrenText with non-text child nodes
input := `<p><a href="https://example.com"><span>Nested</span> Link</a></p>`
result, err := HTMLToMarkdown(input)
@ -806,7 +806,7 @@ func TestHTMLToMarkdown_Good_NestedElements(t *testing.T) {
// --- HTML: ordered list ---
func TestHTMLToMarkdown_Good_OL(t *testing.T) {
func TestHTMLToMarkdown_Good_OL_Good(t *testing.T) {
input := `<ol><li>First</li><li>Second</li></ol>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -816,7 +816,7 @@ func TestHTMLToMarkdown_Good_OL(t *testing.T) {
// --- HTML: blockquote ---
func TestHTMLToMarkdown_Good_BlockquoteElement(t *testing.T) {
func TestHTMLToMarkdown_Good_BlockquoteElement_Good(t *testing.T) {
input := `<blockquote>Quoted text</blockquote>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -825,7 +825,7 @@ func TestHTMLToMarkdown_Good_BlockquoteElement(t *testing.T) {
// --- HTML: hr ---
func TestHTMLToMarkdown_Good_HR(t *testing.T) {
func TestHTMLToMarkdown_Good_HR_Good(t *testing.T) {
input := `<p>Before</p><hr><p>After</p>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -834,7 +834,7 @@ func TestHTMLToMarkdown_Good_HR(t *testing.T) {
// --- HTML: h4, h5, h6 ---
func TestHTMLToMarkdown_Good_AllHeadingLevels(t *testing.T) {
func TestHTMLToMarkdown_Good_AllHeadingLevels_Good(t *testing.T) {
input := `<h4>H4</h4><h5>H5</h5><h6>H6</h6>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -845,7 +845,7 @@ func TestHTMLToMarkdown_Good_AllHeadingLevels(t *testing.T) {
// --- HTML: link without href ---
func TestHTMLToMarkdown_Good_LinkNoHref(t *testing.T) {
func TestHTMLToMarkdown_Good_LinkNoHref_Good(t *testing.T) {
input := `<a>bare link text</a>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -855,7 +855,7 @@ func TestHTMLToMarkdown_Good_LinkNoHref(t *testing.T) {
// --- HTML: unordered list ---
func TestHTMLToMarkdown_Good_UL(t *testing.T) {
func TestHTMLToMarkdown_Good_UL_Good(t *testing.T) {
input := `<ul><li>Item A</li><li>Item B</li></ul>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -865,7 +865,7 @@ func TestHTMLToMarkdown_Good_UL(t *testing.T) {
// --- HTML: br tag ---
func TestHTMLToMarkdown_Good_BRTag(t *testing.T) {
func TestHTMLToMarkdown_Good_BRTag_Good(t *testing.T) {
input := `<p>Line one<br>Line two</p>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -875,7 +875,7 @@ func TestHTMLToMarkdown_Good_BRTag(t *testing.T) {
// --- HTML: style tag stripped ---
func TestHTMLToMarkdown_Good_StyleStripped(t *testing.T) {
func TestHTMLToMarkdown_Good_StyleStripped_Good(t *testing.T) {
input := `<html><head><style>body{color:red}</style></head><body><p>Clean</p></body></html>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -885,7 +885,7 @@ func TestHTMLToMarkdown_Good_StyleStripped(t *testing.T) {
// --- HTML: i and b tags ---
func TestHTMLToMarkdown_Good_AlternateBoldItalic(t *testing.T) {
func TestHTMLToMarkdown_Good_AlternateBoldItalic_Good(t *testing.T) {
input := `<p><b>bold</b> and <i>italic</i></p>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -895,7 +895,7 @@ func TestHTMLToMarkdown_Good_AlternateBoldItalic(t *testing.T) {
// --- Market: collectCurrent with limiter that actually blocks ---
func TestMarketCollector_Collect_Bad_LimiterBlocksThenCancelled(t *testing.T) {
func TestMarketCollector_Collect_Bad_LimiterBlocksThenCancelled_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(coinData{ID: "bitcoin", Symbol: "btc", Name: "Bitcoin",
@ -927,7 +927,7 @@ func TestMarketCollector_Collect_Bad_LimiterBlocksThenCancelled(t *testing.T) {
// --- Papers: IACR with limiter that blocks ---
func TestPapersCollector_CollectIACR_Bad_LimiterBlocks(t *testing.T) {
func TestPapersCollector_CollectIACR_Bad_LimiterBlocks_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = NewRateLimiter()
@ -944,7 +944,7 @@ func TestPapersCollector_CollectIACR_Bad_LimiterBlocks(t *testing.T) {
// --- Papers: arXiv with limiter that blocks ---
func TestPapersCollector_CollectArXiv_Bad_LimiterBlocks(t *testing.T) {
func TestPapersCollector_CollectArXiv_Bad_LimiterBlocks_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = NewRateLimiter()
@ -961,7 +961,7 @@ func TestPapersCollector_CollectArXiv_Bad_LimiterBlocks(t *testing.T) {
// --- BitcoinTalk: limiter that blocks ---
func TestBitcoinTalkCollector_Collect_Bad_LimiterBlocks(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Bad_LimiterBlocks_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = NewRateLimiter()
@ -1021,7 +1021,7 @@ func (w *writeCountMedium) Exists(path string) bool { return w.MockMedium.Exists
func (w *writeCountMedium) IsDir(path string) bool { return w.MockMedium.IsDir(path) }
// Test that the summary.md write error in collectCurrent is handled.
func TestMarketCollector_Collect_Bad_SummaryWriteError(t *testing.T) {
func TestMarketCollector_Collect_Bad_SummaryWriteError_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if strings.Contains(r.URL.Path, "/market_chart") {
@ -1058,7 +1058,7 @@ func TestMarketCollector_Collect_Bad_SummaryWriteError(t *testing.T) {
// --- Market: collectHistorical write error ---
func TestMarketCollector_Collect_Bad_HistoricalWriteError(t *testing.T) {
func TestMarketCollector_Collect_Bad_HistoricalWriteError_Good(t *testing.T) {
callCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
@ -1097,7 +1097,7 @@ func TestMarketCollector_Collect_Bad_HistoricalWriteError(t *testing.T) {
// --- State: Save write error ---
func TestState_Save_Bad_WriteError(t *testing.T) {
func TestState_Save_Bad_WriteError_Good(t *testing.T) {
em := &errorMedium{MockMedium: io.NewMockMedium(), writeErr: testErr("disk full")}
s := NewState(em, "/state.json")
s.Set("test", &StateEntry{Source: "test", Items: 1})
@ -1109,7 +1109,7 @@ func TestState_Save_Bad_WriteError(t *testing.T) {
// --- Excavator: collector with state error ---
func TestExcavator_Run_Bad_CollectorStateError(t *testing.T) {
func TestExcavator_Run_Bad_CollectorStateError_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.State = NewState(m, "/state.json")
@ -1131,7 +1131,7 @@ func TestExcavator_Run_Bad_CollectorStateError(t *testing.T) {
// --- BitcoinTalk: page returns zero posts (empty content) ---
func TestBitcoinTalkCollector_Collect_Good_ZeroPostsPage(t *testing.T) {
func TestBitcoinTalkCollector_Collect_Good_ZeroPostsPage_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
// Valid HTML with no post divs at all
@ -1156,7 +1156,7 @@ func TestBitcoinTalkCollector_Collect_Good_ZeroPostsPage(t *testing.T) {
// --- Excavator: state save error after collection ---
func TestExcavator_Run_Bad_StateSaveError(t *testing.T) {
func TestExcavator_Run_Bad_StateSaveError_Good(t *testing.T) {
em := &errorMedium{MockMedium: io.NewMockMedium(), writeErr: testErr("state write failed")}
cfg := &Config{
Output: io.NewMockMedium(), // Use regular medium for output
@ -1180,7 +1180,7 @@ func TestExcavator_Run_Bad_StateSaveError(t *testing.T) {
// --- State: Load with read error ---
func TestState_Load_Bad_ReadError(t *testing.T) {
func TestState_Load_Bad_ReadError_Good(t *testing.T) {
em := &errorMedium{MockMedium: io.NewMockMedium(), readErr: testErr("read denied")}
em.MockMedium.Files["/state.json"] = "{}" // File exists but read will fail
@ -1192,7 +1192,7 @@ func TestState_Load_Bad_ReadError(t *testing.T) {
// --- Papers: PaperSourceAll emits complete ---
func TestPapersCollector_CollectAll_Good_ArxivFailsWithIACR(t *testing.T) {
func TestPapersCollector_CollectAll_Good_ArxivFailsWithIACR_Good(t *testing.T) {
callCount := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
@ -1229,7 +1229,7 @@ func TestPapersCollector_CollectAll_Good_ArxivFailsWithIACR(t *testing.T) {
// --- Papers: IACR with cancelled context (request creation fails) ---
func TestPapersCollector_CollectIACR_Bad_CancelledContextRequestFails(t *testing.T) {
func TestPapersCollector_CollectIACR_Bad_CancelledContextRequestFails_Good(t *testing.T) {
// Don't set up any server - the request should fail because context is cancelled.
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
@ -1245,7 +1245,7 @@ func TestPapersCollector_CollectIACR_Bad_CancelledContextRequestFails(t *testing
// --- Papers: arXiv with cancelled context ---
func TestPapersCollector_CollectArXiv_Bad_CancelledContextRequestFails(t *testing.T) {
func TestPapersCollector_CollectArXiv_Bad_CancelledContextRequestFails_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = nil
@ -1260,7 +1260,7 @@ func TestPapersCollector_CollectArXiv_Bad_CancelledContextRequestFails(t *testin
// --- Market: collectHistorical limiter blocks ---
func TestMarketCollector_Collect_Bad_HistoricalLimiterBlocks(t *testing.T) {
func TestMarketCollector_Collect_Bad_HistoricalLimiterBlocks_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(coinData{
@ -1299,7 +1299,7 @@ func TestMarketCollector_Collect_Bad_HistoricalLimiterBlocks(t *testing.T) {
// --- BitcoinTalk: fetchPage with invalid URL ---
func TestBitcoinTalkCollector_FetchPage_Bad_InvalidURL(t *testing.T) {
func TestBitcoinTalkCollector_FetchPage_Bad_InvalidURL_Good(t *testing.T) {
b := &BitcoinTalkCollector{TopicID: "12345"}
// Use a URL with control character that will fail NewRequestWithContext
_, err := b.fetchPage(context.Background(), "http://\x7f/invalid")

View file

@ -43,7 +43,7 @@ func TestDispatcher_On_Good(t *testing.T) {
assert.Equal(t, 3, count, "All three handlers should be called")
}
func TestDispatcher_Emit_Good_NoHandlers(t *testing.T) {
func TestDispatcher_Emit_Good_NoHandlers_Good(t *testing.T) {
d := NewDispatcher()
// Should not panic when emitting an event with no handlers
@ -56,7 +56,7 @@ func TestDispatcher_Emit_Good_NoHandlers(t *testing.T) {
})
}
func TestDispatcher_Emit_Good_MultipleEventTypes(t *testing.T) {
func TestDispatcher_Emit_Good_MultipleEventTypes_Good(t *testing.T) {
d := NewDispatcher()
var starts, errors int
@ -71,7 +71,7 @@ func TestDispatcher_Emit_Good_MultipleEventTypes(t *testing.T) {
assert.Equal(t, 1, errors)
}
func TestDispatcher_Emit_Good_SetsTime(t *testing.T) {
func TestDispatcher_Emit_Good_SetsTime_Good(t *testing.T) {
d := NewDispatcher()
var received Event
@ -87,7 +87,7 @@ func TestDispatcher_Emit_Good_SetsTime(t *testing.T) {
assert.True(t, received.Time.Before(after) || received.Time.Equal(after))
}
func TestDispatcher_Emit_Good_PreservesExistingTime(t *testing.T) {
func TestDispatcher_Emit_Good_PreservesExistingTime_Good(t *testing.T) {
d := NewDispatcher()
customTime := time.Date(2025, 6, 15, 12, 0, 0, 0, time.UTC)

View file

@ -12,7 +12,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestExcavator_Run_Good_ResumeSkipsCompleted(t *testing.T) {
func TestExcavator_Run_Good_ResumeSkipsCompleted_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = nil
@ -41,7 +41,7 @@ func TestExcavator_Run_Good_ResumeSkipsCompleted(t *testing.T) {
assert.Equal(t, 1, result.Skipped)
}
func TestExcavator_Run_Good_ResumeRunsIncomplete(t *testing.T) {
func TestExcavator_Run_Good_ResumeRunsIncomplete_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = nil
@ -67,7 +67,7 @@ func TestExcavator_Run_Good_ResumeRunsIncomplete(t *testing.T) {
assert.Equal(t, 5, result.Items)
}
func TestExcavator_Run_Good_NilState(t *testing.T) {
func TestExcavator_Run_Good_NilState_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.State = nil
@ -85,7 +85,7 @@ func TestExcavator_Run_Good_NilState(t *testing.T) {
assert.Equal(t, 3, result.Items)
}
func TestExcavator_Run_Good_NilDispatcher(t *testing.T) {
func TestExcavator_Run_Good_NilDispatcher_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Dispatcher = nil
@ -103,7 +103,7 @@ func TestExcavator_Run_Good_NilDispatcher(t *testing.T) {
assert.Equal(t, 2, result.Items)
}
func TestExcavator_Run_Good_ProgressEvents(t *testing.T) {
func TestExcavator_Run_Good_ProgressEvents_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = nil

View file

@ -66,7 +66,7 @@ func TestExcavator_Run_Good(t *testing.T) {
assert.Len(t, result.Files, 8)
}
func TestExcavator_Run_Good_Empty(t *testing.T) {
func TestExcavator_Run_Good_Empty_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
@ -77,7 +77,7 @@ func TestExcavator_Run_Good_Empty(t *testing.T) {
assert.Equal(t, 0, result.Items)
}
func TestExcavator_Run_Good_DryRun(t *testing.T) {
func TestExcavator_Run_Good_DryRun_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -98,7 +98,7 @@ func TestExcavator_Run_Good_DryRun(t *testing.T) {
assert.Equal(t, 0, result.Items)
}
func TestExcavator_Run_Good_ScanOnly(t *testing.T) {
func TestExcavator_Run_Good_ScanOnly_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
@ -123,7 +123,7 @@ func TestExcavator_Run_Good_ScanOnly(t *testing.T) {
assert.Contains(t, progressMessages[0], "source-a")
}
func TestExcavator_Run_Good_WithErrors(t *testing.T) {
func TestExcavator_Run_Good_WithErrors_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = nil
@ -146,7 +146,7 @@ func TestExcavator_Run_Good_WithErrors(t *testing.T) {
assert.True(t, c3.called)
}
func TestExcavator_Run_Good_CancelledContext(t *testing.T) {
func TestExcavator_Run_Good_CancelledContext_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
@ -163,7 +163,7 @@ func TestExcavator_Run_Good_CancelledContext(t *testing.T) {
assert.Error(t, err)
}
func TestExcavator_Run_Good_SavesState(t *testing.T) {
func TestExcavator_Run_Good_SavesState_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = nil
@ -184,7 +184,7 @@ func TestExcavator_Run_Good_SavesState(t *testing.T) {
assert.Equal(t, "source-a", entry.Source)
}
func TestExcavator_Run_Good_Events(t *testing.T) {
func TestExcavator_Run_Good_Events_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.Limiter = nil

View file

@ -16,12 +16,12 @@ func TestGitHubCollector_Name_Good(t *testing.T) {
assert.Equal(t, "github:host-uk/core", g.Name())
}
func TestGitHubCollector_Name_Good_OrgOnly(t *testing.T) {
func TestGitHubCollector_Name_Good_OrgOnly_Good(t *testing.T) {
g := &GitHubCollector{Org: "host-uk"}
assert.Equal(t, "github:host-uk", g.Name())
}
func TestGitHubCollector_Collect_Good_DryRun(t *testing.T) {
func TestGitHubCollector_Collect_Good_DryRun_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -40,7 +40,7 @@ func TestGitHubCollector_Collect_Good_DryRun(t *testing.T) {
assert.True(t, progressEmitted, "Should emit progress event in dry-run mode")
}
func TestGitHubCollector_Collect_Good_DryRun_IssuesOnly(t *testing.T) {
func TestGitHubCollector_Collect_Good_DryRun_IssuesOnly_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -52,7 +52,7 @@ func TestGitHubCollector_Collect_Good_DryRun_IssuesOnly(t *testing.T) {
assert.Equal(t, 0, result.Items)
}
func TestGitHubCollector_Collect_Good_DryRun_PRsOnly(t *testing.T) {
func TestGitHubCollector_Collect_Good_DryRun_PRsOnly_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -90,7 +90,7 @@ func TestFormatIssueMarkdown_Good(t *testing.T) {
assert.Contains(t, md, "**URL:** https://github.com/test/repo/issues/42")
}
func TestFormatIssueMarkdown_Good_NoLabels(t *testing.T) {
func TestFormatIssueMarkdown_Good_NoLabels_Good(t *testing.T) {
issue := ghIssue{
Number: 1,
Title: "Simple",

View file

@ -14,7 +14,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestMarketCollector_Collect_Good_HistoricalWithFromDate(t *testing.T) {
func TestMarketCollector_Collect_Good_HistoricalWithFromDate_Good(t *testing.T) {
callCount := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
@ -58,7 +58,7 @@ func TestMarketCollector_Collect_Good_HistoricalWithFromDate(t *testing.T) {
assert.Equal(t, 3, result.Items)
}
func TestMarketCollector_Collect_Good_HistoricalInvalidDate(t *testing.T) {
func TestMarketCollector_Collect_Good_HistoricalInvalidDate_Good(t *testing.T) {
callCount := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
@ -100,7 +100,7 @@ func TestMarketCollector_Collect_Good_HistoricalInvalidDate(t *testing.T) {
assert.Equal(t, 3, result.Items)
}
func TestMarketCollector_Collect_Bad_HistoricalServerError(t *testing.T) {
func TestMarketCollector_Collect_Bad_HistoricalServerError_Good(t *testing.T) {
callCount := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
@ -139,7 +139,7 @@ func TestMarketCollector_Collect_Bad_HistoricalServerError(t *testing.T) {
assert.Equal(t, 1, result.Errors) // historical failed
}
func TestMarketCollector_Collect_Good_EmitsEvents(t *testing.T) {
func TestMarketCollector_Collect_Good_EmitsEvents_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
data := coinData{
@ -174,7 +174,7 @@ func TestMarketCollector_Collect_Good_EmitsEvents(t *testing.T) {
assert.Equal(t, 1, completes)
}
func TestMarketCollector_Collect_Good_CancelledContext(t *testing.T) {
func TestMarketCollector_Collect_Good_CancelledContext_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}))
@ -199,7 +199,7 @@ func TestMarketCollector_Collect_Good_CancelledContext(t *testing.T) {
assert.Equal(t, 1, result.Errors)
}
func TestFormatMarketSummary_Good_AllFields(t *testing.T) {
func TestFormatMarketSummary_Good_AllFields_Good(t *testing.T) {
data := &coinData{
Name: "Lethean",
Symbol: "lthn",
@ -231,7 +231,7 @@ func TestFormatMarketSummary_Good_AllFields(t *testing.T) {
assert.Contains(t, summary, "Last updated")
}
func TestFormatMarketSummary_Good_Minimal(t *testing.T) {
func TestFormatMarketSummary_Good_Minimal_Good(t *testing.T) {
data := &coinData{
Name: "Unknown",
Symbol: "ukn",

View file

@ -18,7 +18,7 @@ func TestMarketCollector_Name_Good(t *testing.T) {
assert.Equal(t, "market:bitcoin", m.Name())
}
func TestMarketCollector_Collect_Bad_NoCoinID(t *testing.T) {
func TestMarketCollector_Collect_Bad_NoCoinID_Good(t *testing.T) {
mock := io.NewMockMedium()
cfg := NewConfigWithMedium(mock, "/output")
@ -27,7 +27,7 @@ func TestMarketCollector_Collect_Bad_NoCoinID(t *testing.T) {
assert.Error(t, err)
}
func TestMarketCollector_Collect_Good_DryRun(t *testing.T) {
func TestMarketCollector_Collect_Good_DryRun_Good(t *testing.T) {
mock := io.NewMockMedium()
cfg := NewConfigWithMedium(mock, "/output")
cfg.DryRun = true
@ -39,7 +39,7 @@ func TestMarketCollector_Collect_Good_DryRun(t *testing.T) {
assert.Equal(t, 0, result.Items)
}
func TestMarketCollector_Collect_Good_CurrentData(t *testing.T) {
func TestMarketCollector_Collect_Good_CurrentData_Good(t *testing.T) {
// Set up a mock CoinGecko server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
data := coinData{
@ -94,7 +94,7 @@ func TestMarketCollector_Collect_Good_CurrentData(t *testing.T) {
assert.Contains(t, summary, "42000.50")
}
func TestMarketCollector_Collect_Good_Historical(t *testing.T) {
func TestMarketCollector_Collect_Good_Historical_Good(t *testing.T) {
callCount := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
@ -166,7 +166,7 @@ func TestFormatMarketSummary_Good(t *testing.T) {
assert.Contains(t, summary, "Total Supply")
}
func TestMarketCollector_Collect_Bad_ServerError(t *testing.T) {
func TestMarketCollector_Collect_Bad_ServerError_Good(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))

View file

@ -111,7 +111,7 @@ func TestPapersCollector_CollectArXiv_Good(t *testing.T) {
assert.Contains(t, content, "Alice")
}
func TestPapersCollector_CollectArXiv_Good_WithCategory(t *testing.T) {
func TestPapersCollector_CollectArXiv_Good_WithCategory_Good(t *testing.T) {
var capturedQuery string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
capturedQuery = r.URL.RawQuery
@ -167,7 +167,7 @@ func TestPapersCollector_CollectAll_Good(t *testing.T) {
assert.Equal(t, 4, result.Items) // 2 IACR + 2 arXiv
}
func TestPapersCollector_CollectIACR_Bad_ServerError(t *testing.T) {
func TestPapersCollector_CollectIACR_Bad_ServerError_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
@ -187,7 +187,7 @@ func TestPapersCollector_CollectIACR_Bad_ServerError(t *testing.T) {
assert.Error(t, err)
}
func TestPapersCollector_CollectArXiv_Bad_ServerError(t *testing.T) {
func TestPapersCollector_CollectArXiv_Bad_ServerError_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusServiceUnavailable)
}))
@ -207,7 +207,7 @@ func TestPapersCollector_CollectArXiv_Bad_ServerError(t *testing.T) {
assert.Error(t, err)
}
func TestPapersCollector_CollectArXiv_Bad_InvalidXML(t *testing.T) {
func TestPapersCollector_CollectArXiv_Bad_InvalidXML_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/xml")
_, _ = w.Write([]byte(`not xml at all`))
@ -228,7 +228,7 @@ func TestPapersCollector_CollectArXiv_Bad_InvalidXML(t *testing.T) {
assert.Error(t, err)
}
func TestPapersCollector_CollectAll_Bad_BothFail(t *testing.T) {
func TestPapersCollector_CollectAll_Bad_BothFail_Good(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
}))
@ -248,7 +248,7 @@ func TestPapersCollector_CollectAll_Bad_BothFail(t *testing.T) {
assert.Error(t, err)
}
func TestPapersCollector_CollectAll_Good_OneFails(t *testing.T) {
func TestPapersCollector_CollectAll_Good_OneFails_Good(t *testing.T) {
callCount := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
callCount++
@ -297,7 +297,7 @@ func TestExtractIACRPapers_Good(t *testing.T) {
assert.Equal(t, "Lattice Cryptography", papers[1].Title)
}
func TestExtractIACRPapers_Good_Empty(t *testing.T) {
func TestExtractIACRPapers_Good_Empty_Good(t *testing.T) {
doc, err := html.Parse(strings.NewReader(`<html><body></body></html>`))
require.NoError(t, err)
@ -305,7 +305,7 @@ func TestExtractIACRPapers_Good_Empty(t *testing.T) {
assert.Empty(t, papers)
}
func TestExtractIACRPapers_Good_NoTitle(t *testing.T) {
func TestExtractIACRPapers_Good_NoTitle_Good(t *testing.T) {
doc, err := html.Parse(strings.NewReader(`<html><body><div class="paperentry"></div></body></html>`))
require.NoError(t, err)

View file

@ -15,17 +15,17 @@ func TestPapersCollector_Name_Good(t *testing.T) {
assert.Equal(t, "papers:iacr", p.Name())
}
func TestPapersCollector_Name_Good_ArXiv(t *testing.T) {
func TestPapersCollector_Name_Good_ArXiv_Good(t *testing.T) {
p := &PapersCollector{Source: PaperSourceArXiv}
assert.Equal(t, "papers:arxiv", p.Name())
}
func TestPapersCollector_Name_Good_All(t *testing.T) {
func TestPapersCollector_Name_Good_All_Good(t *testing.T) {
p := &PapersCollector{Source: PaperSourceAll}
assert.Equal(t, "papers:all", p.Name())
}
func TestPapersCollector_Collect_Bad_NoQuery(t *testing.T) {
func TestPapersCollector_Collect_Bad_NoQuery_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
@ -34,7 +34,7 @@ func TestPapersCollector_Collect_Bad_NoQuery(t *testing.T) {
assert.Error(t, err)
}
func TestPapersCollector_Collect_Bad_UnknownSource(t *testing.T) {
func TestPapersCollector_Collect_Bad_UnknownSource_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
@ -43,7 +43,7 @@ func TestPapersCollector_Collect_Bad_UnknownSource(t *testing.T) {
assert.Error(t, err)
}
func TestPapersCollector_Collect_Good_DryRun(t *testing.T) {
func TestPapersCollector_Collect_Good_DryRun_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -74,7 +74,7 @@ func TestFormatPaperMarkdown_Good(t *testing.T) {
assert.Contains(t, md, "zero-knowledge proofs")
}
func TestFormatPaperMarkdown_Good_Minimal(t *testing.T) {
func TestFormatPaperMarkdown_Good_Minimal_Good(t *testing.T) {
md := FormatPaperMarkdown("Title Only", nil, "", "", "", "")
assert.Contains(t, md, "# Title Only")

View file

@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestHTMLToMarkdown_Good_OrderedList(t *testing.T) {
func TestHTMLToMarkdown_Good_OrderedList_Good(t *testing.T) {
input := `<ol><li>First</li><li>Second</li><li>Third</li></ol>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -20,7 +20,7 @@ func TestHTMLToMarkdown_Good_OrderedList(t *testing.T) {
assert.Contains(t, result, "3. Third")
}
func TestHTMLToMarkdown_Good_UnorderedList(t *testing.T) {
func TestHTMLToMarkdown_Good_UnorderedList_Good(t *testing.T) {
input := `<ul><li>Alpha</li><li>Beta</li></ul>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -28,21 +28,21 @@ func TestHTMLToMarkdown_Good_UnorderedList(t *testing.T) {
assert.Contains(t, result, "- Beta")
}
func TestHTMLToMarkdown_Good_Blockquote(t *testing.T) {
func TestHTMLToMarkdown_Good_Blockquote_Good(t *testing.T) {
input := `<blockquote>A wise quote</blockquote>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "> A wise quote")
}
func TestHTMLToMarkdown_Good_HorizontalRule(t *testing.T) {
func TestHTMLToMarkdown_Good_HorizontalRule_Good(t *testing.T) {
input := `<p>Before</p><hr/><p>After</p>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "---")
}
func TestHTMLToMarkdown_Good_LinkWithoutHref(t *testing.T) {
func TestHTMLToMarkdown_Good_LinkWithoutHref_Good(t *testing.T) {
input := `<a>bare link text</a>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -50,7 +50,7 @@ func TestHTMLToMarkdown_Good_LinkWithoutHref(t *testing.T) {
assert.NotContains(t, result, "[")
}
func TestHTMLToMarkdown_Good_H4H5H6(t *testing.T) {
func TestHTMLToMarkdown_Good_H4H5H6_Good(t *testing.T) {
input := `<h4>H4</h4><h5>H5</h5><h6>H6</h6>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -59,7 +59,7 @@ func TestHTMLToMarkdown_Good_H4H5H6(t *testing.T) {
assert.Contains(t, result, "###### H6")
}
func TestHTMLToMarkdown_Good_StripsStyle(t *testing.T) {
func TestHTMLToMarkdown_Good_StripsStyle_Good(t *testing.T) {
input := `<html><head><style>.foo{color:red}</style></head><body><p>Clean</p></body></html>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -67,7 +67,7 @@ func TestHTMLToMarkdown_Good_StripsStyle(t *testing.T) {
assert.NotContains(t, result, "color")
}
func TestHTMLToMarkdown_Good_LineBreak(t *testing.T) {
func TestHTMLToMarkdown_Good_LineBreak_Good(t *testing.T) {
input := `<p>Line one<br/>Line two</p>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -75,7 +75,7 @@ func TestHTMLToMarkdown_Good_LineBreak(t *testing.T) {
assert.Contains(t, result, "Line two")
}
func TestHTMLToMarkdown_Good_NestedBoldItalic(t *testing.T) {
func TestHTMLToMarkdown_Good_NestedBoldItalic_Good(t *testing.T) {
input := `<b>bold text</b> and <i>italic text</i>`
result, err := HTMLToMarkdown(input)
require.NoError(t, err)
@ -83,7 +83,7 @@ func TestHTMLToMarkdown_Good_NestedBoldItalic(t *testing.T) {
assert.Contains(t, result, "*italic text*")
}
func TestJSONToMarkdown_Good_NestedObject(t *testing.T) {
func TestJSONToMarkdown_Good_NestedObject_Good(t *testing.T) {
input := `{"outer": {"inner_key": "inner_value"}}`
result, err := JSONToMarkdown(input)
require.NoError(t, err)
@ -91,7 +91,7 @@ func TestJSONToMarkdown_Good_NestedObject(t *testing.T) {
assert.Contains(t, result, "**inner_key:** inner_value")
}
func TestJSONToMarkdown_Good_NestedArray(t *testing.T) {
func TestJSONToMarkdown_Good_NestedArray_Good(t *testing.T) {
input := `[["a", "b"], ["c"]]`
result, err := JSONToMarkdown(input)
require.NoError(t, err)
@ -100,14 +100,14 @@ func TestJSONToMarkdown_Good_NestedArray(t *testing.T) {
assert.Contains(t, result, "b")
}
func TestJSONToMarkdown_Good_ScalarValue(t *testing.T) {
func TestJSONToMarkdown_Good_ScalarValue_Good(t *testing.T) {
input := `42`
result, err := JSONToMarkdown(input)
require.NoError(t, err)
assert.Contains(t, result, "42")
}
func TestJSONToMarkdown_Good_ArrayOfObjects(t *testing.T) {
func TestJSONToMarkdown_Good_ArrayOfObjects_Good(t *testing.T) {
input := `[{"name": "Alice"}, {"name": "Bob"}]`
result, err := JSONToMarkdown(input)
require.NoError(t, err)
@ -117,7 +117,7 @@ func TestJSONToMarkdown_Good_ArrayOfObjects(t *testing.T) {
assert.Contains(t, result, "Bob")
}
func TestProcessor_Process_Good_CancelledContext(t *testing.T) {
func TestProcessor_Process_Good_CancelledContext_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/file.html"] = `<h1>Test</h1>`
@ -133,7 +133,7 @@ func TestProcessor_Process_Good_CancelledContext(t *testing.T) {
assert.Error(t, err)
}
func TestProcessor_Process_Good_EmitsEvents(t *testing.T) {
func TestProcessor_Process_Good_EmitsEvents_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/a.html"] = `<h1>Title</h1>`
@ -157,7 +157,7 @@ func TestProcessor_Process_Good_EmitsEvents(t *testing.T) {
assert.Equal(t, 1, completes)
}
func TestProcessor_Process_Good_BadHTML(t *testing.T) {
func TestProcessor_Process_Good_BadHTML_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
// html.Parse is very tolerant, so even bad HTML will parse. But we test
@ -174,7 +174,7 @@ func TestProcessor_Process_Good_BadHTML(t *testing.T) {
assert.Equal(t, 1, result.Items)
}
func TestProcessor_Process_Good_BadJSON(t *testing.T) {
func TestProcessor_Process_Good_BadJSON_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/bad.json"] = `not valid json`

View file

@ -15,7 +15,7 @@ func TestProcessor_Name_Good(t *testing.T) {
assert.Equal(t, "process:github", p.Name())
}
func TestProcessor_Process_Bad_NoDir(t *testing.T) {
func TestProcessor_Process_Bad_NoDir_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
@ -24,7 +24,7 @@ func TestProcessor_Process_Bad_NoDir(t *testing.T) {
assert.Error(t, err)
}
func TestProcessor_Process_Good_DryRun(t *testing.T) {
func TestProcessor_Process_Good_DryRun_Good(t *testing.T) {
m := io.NewMockMedium()
cfg := NewConfigWithMedium(m, "/output")
cfg.DryRun = true
@ -36,7 +36,7 @@ func TestProcessor_Process_Good_DryRun(t *testing.T) {
assert.Equal(t, 0, result.Items)
}
func TestProcessor_Process_Good_HTMLFiles(t *testing.T) {
func TestProcessor_Process_Good_HTMLFiles_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/page.html"] = `<html><body><h1>Hello</h1><p>World</p></body></html>`
@ -57,7 +57,7 @@ func TestProcessor_Process_Good_HTMLFiles(t *testing.T) {
assert.Contains(t, content, "World")
}
func TestProcessor_Process_Good_JSONFiles(t *testing.T) {
func TestProcessor_Process_Good_JSONFiles_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/data.json"] = `{"name": "Bitcoin", "price": 42000}`
@ -77,7 +77,7 @@ func TestProcessor_Process_Good_JSONFiles(t *testing.T) {
assert.Contains(t, content, "Bitcoin")
}
func TestProcessor_Process_Good_MarkdownPassthrough(t *testing.T) {
func TestProcessor_Process_Good_MarkdownPassthrough_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/readme.md"] = "# Already Markdown\n\nThis is already formatted."
@ -96,7 +96,7 @@ func TestProcessor_Process_Good_MarkdownPassthrough(t *testing.T) {
assert.Contains(t, content, "# Already Markdown")
}
func TestProcessor_Process_Good_SkipUnknownTypes(t *testing.T) {
func TestProcessor_Process_Good_SkipUnknownTypes_Good(t *testing.T) {
m := io.NewMockMedium()
m.Dirs["/input"] = true
m.Files["/input/image.png"] = "binary data"
@ -172,7 +172,7 @@ func TestHTMLToMarkdown_Good(t *testing.T) {
}
}
func TestHTMLToMarkdown_Good_StripsScripts(t *testing.T) {
func TestHTMLToMarkdown_Good_StripsScripts_Good(t *testing.T) {
input := `<html><head><script>alert('xss')</script></head><body><p>Clean</p></body></html>`
result, err := HTMLToMarkdown(input)
assert.NoError(t, err)
@ -190,14 +190,14 @@ func TestJSONToMarkdown_Good(t *testing.T) {
assert.Contains(t, result, "42")
}
func TestJSONToMarkdown_Good_Array(t *testing.T) {
func TestJSONToMarkdown_Good_Array_Good(t *testing.T) {
input := `[{"id": 1}, {"id": 2}]`
result, err := JSONToMarkdown(input)
assert.NoError(t, err)
assert.Contains(t, result, "# Data")
}
func TestJSONToMarkdown_Bad_InvalidJSON(t *testing.T) {
func TestJSONToMarkdown_Bad_InvalidJSON_Good(t *testing.T) {
_, err := JSONToMarkdown("not json")
assert.Error(t, err)
}

View file

@ -43,6 +43,7 @@ func NewRateLimiter() *RateLimiter {
// Wait blocks until the rate limit allows the next request for the given source.
// It respects context cancellation.
// Usage: Wait(...)
func (r *RateLimiter) Wait(ctx context.Context, source string) error {
r.mu.Lock()
delay, ok := r.delays[source]
@ -77,6 +78,7 @@ func (r *RateLimiter) Wait(ctx context.Context, source string) error {
}
// SetDelay sets the delay for a source.
// Usage: SetDelay(...)
func (r *RateLimiter) SetDelay(source string, d time.Duration) {
r.mu.Lock()
defer r.mu.Unlock()
@ -84,6 +86,7 @@ func (r *RateLimiter) SetDelay(source string, d time.Duration) {
}
// GetDelay returns the delay configured for a source.
// Usage: GetDelay(...)
func (r *RateLimiter) GetDelay(source string) time.Duration {
r.mu.Lock()
defer r.mu.Unlock()
@ -97,6 +100,7 @@ func (r *RateLimiter) GetDelay(source string) time.Duration {
// Returns used and limit counts. Auto-pauses at 75% usage by increasing
// the GitHub rate limit delay.
// Deprecated: Use CheckGitHubRateLimitCtx for context-aware cancellation.
// Usage: CheckGitHubRateLimit(...)
func (r *RateLimiter) CheckGitHubRateLimit() (used, limit int, err error) {
return r.CheckGitHubRateLimitCtx(context.Background())
}
@ -104,6 +108,7 @@ func (r *RateLimiter) CheckGitHubRateLimit() (used, limit int, err error) {
// CheckGitHubRateLimitCtx checks GitHub API rate limit status via gh api with context support.
// Returns used and limit counts. Auto-pauses at 75% usage by increasing
// the GitHub rate limit delay.
// Usage: CheckGitHubRateLimitCtx(...)
func (r *RateLimiter) CheckGitHubRateLimitCtx(ctx context.Context) (used, limit int, err error) {
cmd := exec.CommandContext(ctx, "gh", "api", "rate_limit", "--jq", ".rate | \"\\(.used) \\(.limit)\"")
out, err := cmd.Output()

View file

@ -29,7 +29,7 @@ func TestRateLimiter_Wait_Good(t *testing.T) {
assert.GreaterOrEqual(t, time.Since(start), 40*time.Millisecond) // allow small timing variance
}
func TestRateLimiter_Wait_Bad_ContextCancelled(t *testing.T) {
func TestRateLimiter_Wait_Bad_ContextCancelled_Good(t *testing.T) {
rl := NewRateLimiter()
rl.SetDelay("test", 5*time.Second)
@ -53,7 +53,7 @@ func TestRateLimiter_SetDelay_Good(t *testing.T) {
assert.Equal(t, 3*time.Second, rl.GetDelay("custom"))
}
func TestRateLimiter_GetDelay_Good_Defaults(t *testing.T) {
func TestRateLimiter_GetDelay_Good_Defaults_Good(t *testing.T) {
rl := NewRateLimiter()
assert.Equal(t, 500*time.Millisecond, rl.GetDelay("github"))
@ -62,13 +62,13 @@ func TestRateLimiter_GetDelay_Good_Defaults(t *testing.T) {
assert.Equal(t, 1*time.Second, rl.GetDelay("iacr"))
}
func TestRateLimiter_GetDelay_Good_UnknownSource(t *testing.T) {
func TestRateLimiter_GetDelay_Good_UnknownSource_Good(t *testing.T) {
rl := NewRateLimiter()
// Unknown sources should get the default 500ms delay
assert.Equal(t, 500*time.Millisecond, rl.GetDelay("unknown"))
}
func TestRateLimiter_Wait_Good_UnknownSource(t *testing.T) {
func TestRateLimiter_Wait_Good_UnknownSource_Good(t *testing.T) {
rl := NewRateLimiter()
ctx := context.Background()

View file

@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/require"
)
func TestState_Get_Good_ReturnsCopy(t *testing.T) {
func TestState_Get_Good_ReturnsCopy_Good(t *testing.T) {
m := io.NewMockMedium()
s := NewState(m, "/state.json")
@ -26,7 +26,7 @@ func TestState_Get_Good_ReturnsCopy(t *testing.T) {
assert.Equal(t, 5, again.Items, "internal state should not be mutated")
}
func TestState_Save_Good_WritesJSON(t *testing.T) {
func TestState_Save_Good_WritesJSON_Good(t *testing.T) {
m := io.NewMockMedium()
s := NewState(m, "/data/state.json")
@ -42,7 +42,7 @@ func TestState_Save_Good_WritesJSON(t *testing.T) {
assert.Contains(t, content, `"abc"`)
}
func TestState_Load_Good_NullJSON(t *testing.T) {
func TestState_Load_Good_NullJSON_Good(t *testing.T) {
m := io.NewMockMedium()
m.Files["/state.json"] = "null"
@ -55,7 +55,7 @@ func TestState_Load_Good_NullJSON(t *testing.T) {
assert.False(t, ok)
}
func TestState_SaveLoad_Good_WithCursor(t *testing.T) {
func TestState_SaveLoad_Good_WithCursor_Good(t *testing.T) {
m := io.NewMockMedium()
s := NewState(m, "/state.json")

View file

@ -75,7 +75,7 @@ func TestState_SaveLoad_Good(t *testing.T) {
assert.True(t, now.Equal(got.LastRun))
}
func TestState_Load_Good_NoFile(t *testing.T) {
func TestState_Load_Good_NoFile_Good(t *testing.T) {
m := io.NewMockMedium()
s := NewState(m, "/nonexistent.json")
@ -88,7 +88,7 @@ func TestState_Load_Good_NoFile(t *testing.T) {
assert.False(t, ok)
}
func TestState_Load_Bad_InvalidJSON(t *testing.T) {
func TestState_Load_Bad_InvalidJSON_Good(t *testing.T) {
m := io.NewMockMedium()
m.Files["/state.json"] = "not valid json"
@ -97,7 +97,7 @@ func TestState_Load_Bad_InvalidJSON(t *testing.T) {
assert.Error(t, err)
}
func TestState_SaveLoad_Good_MultipleEntries(t *testing.T) {
func TestState_SaveLoad_Good_MultipleEntries_Good(t *testing.T) {
m := io.NewMockMedium()
s := NewState(m, "/state.json")
@ -125,7 +125,7 @@ func TestState_SaveLoad_Good_MultipleEntries(t *testing.T) {
assert.Equal(t, 30, c.Items)
}
func TestState_Set_Good_Overwrite(t *testing.T) {
func TestState_Set_Good_Overwrite_Good(t *testing.T) {
m := io.NewMockMedium()
s := NewState(m, "/state.json")

View file

@ -34,15 +34,19 @@ func New(url, token string) (*Client, error) {
}
// API exposes the underlying SDK client for direct access.
// Usage: API(...)
func (c *Client) API() *forgejo.Client { return c.api }
// URL returns the Forgejo instance URL.
// Usage: URL(...)
func (c *Client) URL() string { return c.url }
// Token returns the Forgejo API token.
// Usage: Token(...)
func (c *Client) Token() string { return c.token }
// GetCurrentUser returns the authenticated user's information.
// Usage: GetCurrentUser(...)
func (c *Client) GetCurrentUser() (*forgejo.User, error) {
user, _, err := c.api.GetMyUserInfo()
if err != nil {
@ -52,6 +56,7 @@ func (c *Client) GetCurrentUser() (*forgejo.User, error) {
}
// ForkRepo forks a repository. If org is non-empty, forks into that organisation.
// Usage: ForkRepo(...)
func (c *Client) ForkRepo(owner, repo string, org string) (*forgejo.Repository, error) {
opts := forgejo.CreateForkOption{}
if org != "" {
@ -66,6 +71,7 @@ func (c *Client) ForkRepo(owner, repo string, org string) (*forgejo.Repository,
}
// CreatePullRequest creates a pull request on the given repository.
// Usage: CreatePullRequest(...)
func (c *Client) CreatePullRequest(owner, repo string, opts forgejo.CreatePullRequestOption) (*forgejo.PullRequest, error) {
pr, _, err := c.api.CreatePullRequest(owner, repo, opts)
if err != nil {

View file

@ -27,7 +27,7 @@ func TestNew_Good(t *testing.T) {
assert.Equal(t, "test-token-123", client.Token())
}
func TestNew_Bad_InvalidURL(t *testing.T) {
func TestNew_Bad_InvalidURL_Good(t *testing.T) {
// The Forgejo SDK may reject certain URL formats.
_, err := New("://invalid-url", "token")
assert.Error(t, err)
@ -63,7 +63,7 @@ func TestClient_GetCurrentUser_Good(t *testing.T) {
assert.Equal(t, "test-user", user.UserName)
}
func TestClient_GetCurrentUser_Bad_ServerError(t *testing.T) {
func TestClient_GetCurrentUser_Bad_ServerError_Good(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
@ -91,7 +91,7 @@ func TestClient_SetPRDraft_Good(t *testing.T) {
require.NoError(t, err)
}
func TestClient_SetPRDraft_Good_Undraft(t *testing.T) {
func TestClient_SetPRDraft_Good_Undraft_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -99,7 +99,7 @@ func TestClient_SetPRDraft_Good_Undraft(t *testing.T) {
require.NoError(t, err)
}
func TestClient_SetPRDraft_Bad_ServerError(t *testing.T) {
func TestClient_SetPRDraft_Bad_ServerError_Good(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
@ -123,7 +123,7 @@ func TestClient_SetPRDraft_Bad_ServerError(t *testing.T) {
assert.Contains(t, err.Error(), "unexpected status 403")
}
func TestClient_SetPRDraft_Bad_ConnectionRefused(t *testing.T) {
func TestClient_SetPRDraft_Bad_ConnectionRefused_Good(t *testing.T) {
// Use a closed server to simulate connection errors.
srv := newMockForgejoServer(t)
client, err := New(srv.URL, "token")
@ -134,7 +134,7 @@ func TestClient_SetPRDraft_Bad_ConnectionRefused(t *testing.T) {
assert.Error(t, err)
}
func TestClient_SetPRDraft_Good_URLConstruction(t *testing.T) {
func TestClient_SetPRDraft_Good_URLConstruction_Good(t *testing.T) {
// Verify the URL is constructed correctly by checking the request path.
var capturedPath string
mux := http.NewServeMux()
@ -158,7 +158,7 @@ func TestClient_SetPRDraft_Good_URLConstruction(t *testing.T) {
assert.Equal(t, "/api/v1/repos/my-org/my-repo/pulls/42", capturedPath)
}
func TestClient_SetPRDraft_Good_AuthHeader(t *testing.T) {
func TestClient_SetPRDraft_Good_AuthHeader_Good(t *testing.T) {
// Verify the authorisation header is set correctly.
var capturedAuth string
mux := http.NewServeMux()
@ -184,7 +184,7 @@ func TestClient_SetPRDraft_Good_AuthHeader(t *testing.T) {
// --- PRMeta and Comment struct tests ---
func TestPRMeta_Good_Fields(t *testing.T) {
func TestPRMeta_Good_Fields_Good(t *testing.T) {
meta := &PRMeta{
Number: 42,
Title: "Test PR",
@ -210,7 +210,7 @@ func TestPRMeta_Good_Fields(t *testing.T) {
assert.Equal(t, 5, meta.CommentCount)
}
func TestComment_Good_Fields(t *testing.T) {
func TestComment_Good_Fields_Good(t *testing.T) {
comment := Comment{
ID: 123,
Author: "reviewer",
@ -224,7 +224,7 @@ func TestComment_Good_Fields(t *testing.T) {
// --- MergePullRequest merge style mapping ---
func TestMergePullRequest_Good_StyleMapping(t *testing.T) {
func TestMergePullRequest_Good_StyleMapping_Good(t *testing.T) {
// We can't easily test the SDK call, but we can verify the method
// errors when the server returns failure. This exercises the style mapping code.
tests := []struct {
@ -262,7 +262,7 @@ func TestMergePullRequest_Good_StyleMapping(t *testing.T) {
// --- ListIssuesOpts defaulting ---
func TestListIssuesOpts_Good_Defaults(t *testing.T) {
func TestListIssuesOpts_Good_Defaults_Good(t *testing.T) {
tests := []struct {
name string
opts ListIssuesOpts
@ -325,7 +325,7 @@ func TestListIssuesOpts_Good_Defaults(t *testing.T) {
// --- ForkRepo error handling ---
func TestClient_ForkRepo_Good_WithOrg(t *testing.T) {
func TestClient_ForkRepo_Good_WithOrg_Good(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
@ -355,7 +355,7 @@ func TestClient_ForkRepo_Good_WithOrg(t *testing.T) {
assert.Equal(t, "target-org", capturedBody["organization"])
}
func TestClient_ForkRepo_Good_WithoutOrg(t *testing.T) {
func TestClient_ForkRepo_Good_WithoutOrg_Good(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
@ -386,7 +386,7 @@ func TestClient_ForkRepo_Good_WithoutOrg(t *testing.T) {
// The SDK may or may not include it in the JSON; just verify the fork succeeded.
}
func TestClient_ForkRepo_Bad_ServerError(t *testing.T) {
func TestClient_ForkRepo_Bad_ServerError_Good(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
@ -408,7 +408,7 @@ func TestClient_ForkRepo_Bad_ServerError(t *testing.T) {
// --- CreatePullRequest error handling ---
func TestClient_CreatePullRequest_Bad_ServerError(t *testing.T) {
func TestClient_CreatePullRequest_Bad_ServerError_Good(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
@ -440,7 +440,7 @@ func TestCommentPageSize_Good(t *testing.T) {
// --- ListPullRequests state mapping ---
func TestListPullRequests_Good_StateMapping(t *testing.T) {
func TestListPullRequests_Good_StateMapping_Good(t *testing.T) {
// Verify state mapping via error path (server returns error).
tests := []struct {
name string

View file

@ -18,7 +18,7 @@ func isolateConfigEnv(t *testing.T) {
t.Setenv("HOME", t.TempDir())
}
func TestResolveConfig_Good_Defaults(t *testing.T) {
func TestResolveConfig_Good_Defaults_Good(t *testing.T) {
isolateConfigEnv(t)
url, token, err := ResolveConfig("", "")
@ -27,7 +27,7 @@ func TestResolveConfig_Good_Defaults(t *testing.T) {
assert.Empty(t, token, "token should be empty when nothing configured")
}
func TestResolveConfig_Good_FlagsOverrideAll(t *testing.T) {
func TestResolveConfig_Good_FlagsOverrideAll_Good(t *testing.T) {
isolateConfigEnv(t)
t.Setenv("FORGE_URL", "https://env-url.example.com")
t.Setenv("FORGE_TOKEN", "env-token-abc")
@ -38,7 +38,7 @@ func TestResolveConfig_Good_FlagsOverrideAll(t *testing.T) {
assert.Equal(t, "flag-token-xyz", token, "flag token should override env")
}
func TestResolveConfig_Good_EnvVarsOverrideConfig(t *testing.T) {
func TestResolveConfig_Good_EnvVarsOverrideConfig_Good(t *testing.T) {
isolateConfigEnv(t)
t.Setenv("FORGE_URL", "https://env-url.example.com")
t.Setenv("FORGE_TOKEN", "env-token-123")
@ -49,7 +49,7 @@ func TestResolveConfig_Good_EnvVarsOverrideConfig(t *testing.T) {
assert.Equal(t, "env-token-123", token)
}
func TestResolveConfig_Good_PartialOverrides(t *testing.T) {
func TestResolveConfig_Good_PartialOverrides_Good(t *testing.T) {
isolateConfigEnv(t)
// Set only env URL, flag token.
t.Setenv("FORGE_URL", "https://env-only.example.com")
@ -60,7 +60,7 @@ func TestResolveConfig_Good_PartialOverrides(t *testing.T) {
assert.Equal(t, "flag-only-token", token, "flag token should be used")
}
func TestResolveConfig_Good_URLDefaultsWhenEmpty(t *testing.T) {
func TestResolveConfig_Good_URLDefaultsWhenEmpty_Good(t *testing.T) {
isolateConfigEnv(t)
t.Setenv("FORGE_TOKEN", "some-token")
@ -76,7 +76,7 @@ func TestConstants_Good(t *testing.T) {
assert.Equal(t, "http://localhost:4000", DefaultURL)
}
func TestNewFromConfig_Bad_NoToken(t *testing.T) {
func TestNewFromConfig_Bad_NoToken_Good(t *testing.T) {
isolateConfigEnv(t)
_, err := NewFromConfig("", "")
@ -84,7 +84,7 @@ func TestNewFromConfig_Bad_NoToken(t *testing.T) {
assert.Contains(t, err.Error(), "no API token configured")
}
func TestNewFromConfig_Good_WithFlagToken(t *testing.T) {
func TestNewFromConfig_Good_WithFlagToken_Good(t *testing.T) {
isolateConfigEnv(t)
// The Forgejo SDK NewClient validates the token by calling /api/v1/version,
@ -99,7 +99,7 @@ func TestNewFromConfig_Good_WithFlagToken(t *testing.T) {
assert.Equal(t, "test-token", client.Token())
}
func TestNewFromConfig_Good_EnvToken(t *testing.T) {
func TestNewFromConfig_Good_EnvToken_Good(t *testing.T) {
isolateConfigEnv(t)
srv := newMockForgejoServer(t)

View file

@ -21,7 +21,7 @@ func TestClient_ListIssues_Good(t *testing.T) {
assert.Equal(t, "Issue 1", issues[0].Title)
}
func TestClient_ListIssues_Good_StateMapping(t *testing.T) {
func TestClient_ListIssues_Good_StateMapping_Good(t *testing.T) {
tests := []struct {
name string
state string
@ -43,7 +43,7 @@ func TestClient_ListIssues_Good_StateMapping(t *testing.T) {
}
}
func TestClient_ListIssues_Good_CustomPageAndLimit(t *testing.T) {
func TestClient_ListIssues_Good_CustomPageAndLimit_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -54,7 +54,7 @@ func TestClient_ListIssues_Good_CustomPageAndLimit(t *testing.T) {
require.NoError(t, err)
}
func TestClient_ListIssues_Bad_ServerError(t *testing.T) {
func TestClient_ListIssues_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -72,7 +72,7 @@ func TestClient_GetIssue_Good(t *testing.T) {
assert.Equal(t, "Issue 1", issue.Title)
}
func TestClient_GetIssue_Bad_ServerError(t *testing.T) {
func TestClient_GetIssue_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -93,7 +93,7 @@ func TestClient_CreateIssue_Good(t *testing.T) {
assert.NotNil(t, issue)
}
func TestClient_CreateIssue_Bad_ServerError(t *testing.T) {
func TestClient_CreateIssue_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -115,7 +115,7 @@ func TestClient_EditIssue_Good(t *testing.T) {
assert.NotNil(t, issue)
}
func TestClient_EditIssue_Bad_ServerError(t *testing.T) {
func TestClient_EditIssue_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -134,7 +134,7 @@ func TestClient_AssignIssue_Good(t *testing.T) {
require.NoError(t, err)
}
func TestClient_AssignIssue_Bad_ServerError(t *testing.T) {
func TestClient_AssignIssue_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -153,7 +153,7 @@ func TestClient_ListPullRequests_Good(t *testing.T) {
assert.Equal(t, "PR 1", prs[0].Title)
}
func TestClient_ListPullRequests_Good_StateMapping(t *testing.T) {
func TestClient_ListPullRequests_Good_StateMapping_Good(t *testing.T) {
tests := []struct {
name string
state string
@ -175,7 +175,7 @@ func TestClient_ListPullRequests_Good_StateMapping(t *testing.T) {
}
}
func TestClient_ListPullRequests_Bad_ServerError(t *testing.T) {
func TestClient_ListPullRequests_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -193,7 +193,7 @@ func TestClient_GetPullRequest_Good(t *testing.T) {
assert.Equal(t, "PR 1", pr.Title)
}
func TestClient_GetPullRequest_Bad_ServerError(t *testing.T) {
func TestClient_GetPullRequest_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -210,7 +210,7 @@ func TestClient_CreateIssueComment_Good(t *testing.T) {
require.NoError(t, err)
}
func TestClient_CreateIssueComment_Bad_ServerError(t *testing.T) {
func TestClient_CreateIssueComment_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -229,7 +229,7 @@ func TestClient_ListIssueComments_Good(t *testing.T) {
assert.Equal(t, "comment 1", comments[0].Body)
}
func TestClient_ListIssueComments_Bad_ServerError(t *testing.T) {
func TestClient_ListIssueComments_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -246,7 +246,7 @@ func TestClient_CloseIssue_Good(t *testing.T) {
require.NoError(t, err)
}
func TestClient_CloseIssue_Bad_ServerError(t *testing.T) {
func TestClient_CloseIssue_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()

View file

@ -26,7 +26,7 @@ func TestClient_ListRepoLabels_Good(t *testing.T) {
assert.Equal(t, "feature", labels[1].Name)
}
func TestClient_ListRepoLabels_Bad_ServerError(t *testing.T) {
func TestClient_ListRepoLabels_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -44,7 +44,7 @@ func TestClient_CreateRepoLabel_Good(t *testing.T) {
assert.NotNil(t, label)
}
func TestClient_CreateRepoLabel_Bad_ServerError(t *testing.T) {
func TestClient_CreateRepoLabel_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -62,7 +62,7 @@ func TestClient_GetLabelByName_Good(t *testing.T) {
assert.Equal(t, "bug", label.Name)
}
func TestClient_GetLabelByName_Good_CaseInsensitive(t *testing.T) {
func TestClient_GetLabelByName_Good_CaseInsensitive_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -71,7 +71,7 @@ func TestClient_GetLabelByName_Good_CaseInsensitive(t *testing.T) {
assert.Equal(t, "bug", label.Name)
}
func TestClient_GetLabelByName_Bad_NotFound(t *testing.T) {
func TestClient_GetLabelByName_Bad_NotFound_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -80,7 +80,7 @@ func TestClient_GetLabelByName_Bad_NotFound(t *testing.T) {
assert.Contains(t, err.Error(), "label nonexistent not found")
}
func TestClient_EnsureLabel_Good_Exists(t *testing.T) {
func TestClient_EnsureLabel_Good_Exists_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -90,7 +90,7 @@ func TestClient_EnsureLabel_Good_Exists(t *testing.T) {
assert.Equal(t, "bug", label.Name)
}
func TestClient_EnsureLabel_Good_Creates(t *testing.T) {
func TestClient_EnsureLabel_Good_Creates_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -110,7 +110,7 @@ func TestClient_ListOrgLabels_Good(t *testing.T) {
assert.NotEmpty(t, labels)
}
func TestClient_ListOrgLabels_Bad_ServerError(t *testing.T) {
func TestClient_ListOrgLabels_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -126,7 +126,7 @@ func TestClient_AddIssueLabels_Good(t *testing.T) {
require.NoError(t, err)
}
func TestClient_AddIssueLabels_Bad_ServerError(t *testing.T) {
func TestClient_AddIssueLabels_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -143,7 +143,7 @@ func TestClient_RemoveIssueLabel_Good(t *testing.T) {
require.NoError(t, err)
}
func TestClient_RemoveIssueLabel_Bad_ServerError(t *testing.T) {
func TestClient_RemoveIssueLabel_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()

View file

@ -40,6 +40,7 @@ const commentPageSize = 50
// GetPRMeta returns structural signals for a pull request.
// This is the Forgejo side of the dual MetaReader described in the pipeline design.
// Usage: GetPRMeta(...)
func (c *Client) GetPRMeta(owner, repo string, pr int64) (*PRMeta, error) {
pull, _, err := c.api.GetPullRequest(owner, repo, pr)
if err != nil {
@ -97,6 +98,7 @@ func (c *Client) GetPRMeta(owner, repo string, pr int64) (*PRMeta, error) {
}
// GetCommentBodies returns all comment bodies for a pull request.
// Usage: GetCommentBodies(...)
func (c *Client) GetCommentBodies(owner, repo string, pr int64) ([]Comment, error) {
var comments []Comment
page := 1
@ -136,6 +138,7 @@ func (c *Client) GetCommentBodies(owner, repo string, pr int64) ([]Comment, erro
}
// GetIssueBody returns the body text of an issue.
// Usage: GetIssueBody(...)
func (c *Client) GetIssueBody(owner, repo string, issue int64) (string, error) {
iss, _, err := c.api.GetIssue(owner, repo, issue)
if err != nil {

View file

@ -25,7 +25,7 @@ func TestClient_GetPRMeta_Good(t *testing.T) {
assert.False(t, meta.IsMerged)
}
func TestClient_GetPRMeta_Bad_ServerError(t *testing.T) {
func TestClient_GetPRMeta_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -47,7 +47,7 @@ func TestClient_GetCommentBodies_Good(t *testing.T) {
assert.Equal(t, "user2", comments[1].Author)
}
func TestClient_GetCommentBodies_Bad_ServerError(t *testing.T) {
func TestClient_GetCommentBodies_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -65,7 +65,7 @@ func TestClient_GetIssueBody_Good(t *testing.T) {
assert.Equal(t, "First issue body", body)
}
func TestClient_GetIssueBody_Bad_ServerError(t *testing.T) {
func TestClient_GetIssueBody_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()

View file

@ -21,7 +21,7 @@ func TestClient_ListMyOrgs_Good(t *testing.T) {
assert.Equal(t, "test-org", orgs[0].UserName)
}
func TestClient_ListMyOrgs_Bad_ServerError(t *testing.T) {
func TestClient_ListMyOrgs_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -39,7 +39,7 @@ func TestClient_GetOrg_Good(t *testing.T) {
assert.Equal(t, "test-org", org.UserName)
}
func TestClient_GetOrg_Bad_ServerError(t *testing.T) {
func TestClient_GetOrg_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -61,7 +61,7 @@ func TestClient_CreateOrg_Good(t *testing.T) {
assert.NotNil(t, org)
}
func TestClient_CreateOrg_Bad_ServerError(t *testing.T) {
func TestClient_CreateOrg_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()

View file

@ -21,7 +21,7 @@ func TestClient_MergePullRequest_Good(t *testing.T) {
require.NoError(t, err)
}
func TestClient_MergePullRequest_Good_Squash(t *testing.T) {
func TestClient_MergePullRequest_Good_Squash_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -29,7 +29,7 @@ func TestClient_MergePullRequest_Good_Squash(t *testing.T) {
require.NoError(t, err)
}
func TestClient_MergePullRequest_Good_Rebase(t *testing.T) {
func TestClient_MergePullRequest_Good_Rebase_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -37,7 +37,7 @@ func TestClient_MergePullRequest_Good_Rebase(t *testing.T) {
require.NoError(t, err)
}
func TestClient_MergePullRequest_Bad_ServerError(t *testing.T) {
func TestClient_MergePullRequest_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -60,7 +60,7 @@ func TestClient_ListPRReviews_Good(t *testing.T) {
require.Len(t, reviews, 1)
}
func TestClient_ListPRReviews_Bad_ServerError(t *testing.T) {
func TestClient_ListPRReviews_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -78,7 +78,7 @@ func TestClient_GetCombinedStatus_Good(t *testing.T) {
assert.NotNil(t, status)
}
func TestClient_GetCombinedStatus_Bad_ServerError(t *testing.T) {
func TestClient_GetCombinedStatus_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -95,7 +95,7 @@ func TestClient_DismissReview_Good(t *testing.T) {
require.NoError(t, err)
}
func TestClient_DismissReview_Bad_ServerError(t *testing.T) {
func TestClient_DismissReview_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -104,7 +104,7 @@ func TestClient_DismissReview_Bad_ServerError(t *testing.T) {
assert.Contains(t, err.Error(), "failed to dismiss review")
}
func TestClient_SetPRDraft_Good_Request(t *testing.T) {
func TestClient_SetPRDraft_Good_Request_Good(t *testing.T) {
var method, path string
var payload map[string]any
@ -132,7 +132,7 @@ func TestClient_SetPRDraft_Good_Request(t *testing.T) {
assert.Equal(t, false, payload["draft"])
}
func TestClient_SetPRDraft_Bad_PathTraversalOwner(t *testing.T) {
func TestClient_SetPRDraft_Bad_PathTraversalOwner_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -141,7 +141,7 @@ func TestClient_SetPRDraft_Bad_PathTraversalOwner(t *testing.T) {
assert.Contains(t, err.Error(), "invalid owner")
}
func TestClient_SetPRDraft_Bad_PathTraversalRepo(t *testing.T) {
func TestClient_SetPRDraft_Bad_PathTraversalRepo_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()

View file

@ -21,7 +21,7 @@ func TestClient_ListOrgRepos_Good(t *testing.T) {
assert.Equal(t, "org-repo", repos[0].Name)
}
func TestClient_ListOrgRepos_Bad_ServerError(t *testing.T) {
func TestClient_ListOrgRepos_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -41,7 +41,7 @@ func TestClient_ListUserRepos_Good(t *testing.T) {
assert.Equal(t, "repo-b", repos[1].Name)
}
func TestClient_ListUserRepos_Bad_ServerError(t *testing.T) {
func TestClient_ListUserRepos_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -59,7 +59,7 @@ func TestClient_GetRepo_Good(t *testing.T) {
assert.Equal(t, "org-repo", repo.Name)
}
func TestClient_GetRepo_Bad_ServerError(t *testing.T) {
func TestClient_GetRepo_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -80,7 +80,7 @@ func TestClient_CreateOrgRepo_Good(t *testing.T) {
assert.NotNil(t, repo)
}
func TestClient_CreateOrgRepo_Bad_ServerError(t *testing.T) {
func TestClient_CreateOrgRepo_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -99,7 +99,7 @@ func TestClient_DeleteRepo_Good(t *testing.T) {
require.NoError(t, err)
}
func TestClient_DeleteRepo_Bad_ServerError(t *testing.T) {
func TestClient_DeleteRepo_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -121,7 +121,7 @@ func TestClient_MigrateRepo_Good(t *testing.T) {
assert.NotNil(t, repo)
}
func TestClient_MigrateRepo_Bad_ServerError(t *testing.T) {
func TestClient_MigrateRepo_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()

View file

@ -25,7 +25,7 @@ func TestClient_CreateRepoWebhook_Good(t *testing.T) {
assert.NotNil(t, hook)
}
func TestClient_CreateRepoWebhook_Bad_ServerError(t *testing.T) {
func TestClient_CreateRepoWebhook_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -45,7 +45,7 @@ func TestClient_ListRepoWebhooks_Good(t *testing.T) {
require.Len(t, hooks, 1)
}
func TestClient_ListRepoWebhooks_Bad_ServerError(t *testing.T) {
func TestClient_ListRepoWebhooks_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()

View file

@ -33,7 +33,9 @@ func New(url, token string) (*Client, error) {
}
// API exposes the underlying SDK client for direct access.
// Usage: API(...)
func (c *Client) API() *gitea.Client { return c.api }
// URL returns the Gitea instance URL.
// Usage: URL(...)
func (c *Client) URL() string { return c.url }

View file

@ -20,7 +20,7 @@ func TestNew_Good(t *testing.T) {
assert.Equal(t, srv.URL, client.URL())
}
func TestNew_Bad_InvalidURL(t *testing.T) {
func TestNew_Bad_InvalidURL_Good(t *testing.T) {
_, err := New("://invalid-url", "token")
assert.Error(t, err)
}

View file

@ -17,7 +17,7 @@ func isolateConfigEnv(t *testing.T) {
t.Setenv("HOME", t.TempDir())
}
func TestResolveConfig_Good_Defaults(t *testing.T) {
func TestResolveConfig_Good_Defaults_Good(t *testing.T) {
isolateConfigEnv(t)
url, token, err := ResolveConfig("", "")
@ -26,7 +26,7 @@ func TestResolveConfig_Good_Defaults(t *testing.T) {
assert.Empty(t, token, "token should be empty when nothing configured")
}
func TestResolveConfig_Good_FlagsOverrideAll(t *testing.T) {
func TestResolveConfig_Good_FlagsOverrideAll_Good(t *testing.T) {
isolateConfigEnv(t)
t.Setenv("GITEA_URL", "https://env-url.example.com")
t.Setenv("GITEA_TOKEN", "env-token-abc")
@ -37,7 +37,7 @@ func TestResolveConfig_Good_FlagsOverrideAll(t *testing.T) {
assert.Equal(t, "flag-token-xyz", token, "flag token should override env")
}
func TestResolveConfig_Good_EnvVarsOverrideConfig(t *testing.T) {
func TestResolveConfig_Good_EnvVarsOverrideConfig_Good(t *testing.T) {
isolateConfigEnv(t)
t.Setenv("GITEA_URL", "https://env-url.example.com")
t.Setenv("GITEA_TOKEN", "env-token-123")
@ -48,7 +48,7 @@ func TestResolveConfig_Good_EnvVarsOverrideConfig(t *testing.T) {
assert.Equal(t, "env-token-123", token)
}
func TestResolveConfig_Good_PartialOverrides(t *testing.T) {
func TestResolveConfig_Good_PartialOverrides_Good(t *testing.T) {
isolateConfigEnv(t)
t.Setenv("GITEA_URL", "https://env-only.example.com")
@ -58,7 +58,7 @@ func TestResolveConfig_Good_PartialOverrides(t *testing.T) {
assert.Equal(t, "flag-only-token", token, "flag token should be used")
}
func TestResolveConfig_Good_URLDefaultsWhenEmpty(t *testing.T) {
func TestResolveConfig_Good_URLDefaultsWhenEmpty_Good(t *testing.T) {
isolateConfigEnv(t)
t.Setenv("GITEA_TOKEN", "some-token")
@ -74,7 +74,7 @@ func TestConstants_Good(t *testing.T) {
assert.Equal(t, "https://gitea.snider.dev", DefaultURL)
}
func TestNewFromConfig_Bad_NoToken(t *testing.T) {
func TestNewFromConfig_Bad_NoToken_Good(t *testing.T) {
isolateConfigEnv(t)
_, err := NewFromConfig("", "")
@ -82,7 +82,7 @@ func TestNewFromConfig_Bad_NoToken(t *testing.T) {
assert.Contains(t, err.Error(), "no API token configured")
}
func TestNewFromConfig_Good_WithFlagToken(t *testing.T) {
func TestNewFromConfig_Good_WithFlagToken_Good(t *testing.T) {
isolateConfigEnv(t)
srv := newMockGiteaServer(t)
@ -94,7 +94,7 @@ func TestNewFromConfig_Good_WithFlagToken(t *testing.T) {
assert.Equal(t, srv.URL, client.URL())
}
func TestNewFromConfig_Good_EnvToken(t *testing.T) {
func TestNewFromConfig_Good_EnvToken_Good(t *testing.T) {
isolateConfigEnv(t)
srv := newMockGiteaServer(t)

View file

@ -14,7 +14,7 @@ import (
// --- SaveConfig tests ---
func TestSaveConfig_Good_URLAndToken(t *testing.T) {
func TestSaveConfig_Good_URLAndToken_Good(t *testing.T) {
isolateConfigEnv(t)
err := SaveConfig("https://gitea.example.com", "test-token-123")
@ -25,7 +25,7 @@ func TestSaveConfig_Good_URLAndToken(t *testing.T) {
}
}
func TestSaveConfig_Good_URLOnly(t *testing.T) {
func TestSaveConfig_Good_URLOnly_Good(t *testing.T) {
isolateConfigEnv(t)
err := SaveConfig("https://gitea.example.com", "")
@ -34,7 +34,7 @@ func TestSaveConfig_Good_URLOnly(t *testing.T) {
}
}
func TestSaveConfig_Good_TokenOnly(t *testing.T) {
func TestSaveConfig_Good_TokenOnly_Good(t *testing.T) {
isolateConfigEnv(t)
err := SaveConfig("", "some-token")
@ -43,7 +43,7 @@ func TestSaveConfig_Good_TokenOnly(t *testing.T) {
}
}
func TestSaveConfig_Good_Empty(t *testing.T) {
func TestSaveConfig_Good_Empty_Good(t *testing.T) {
isolateConfigEnv(t)
err := SaveConfig("", "")
@ -89,7 +89,7 @@ func newPaginatedOrgReposServer(t *testing.T) *httptest.Server {
return httptest.NewServer(mux)
}
func TestClient_ListOrgRepos_Good_Pagination(t *testing.T) {
func TestClient_ListOrgRepos_Good_Pagination_Good(t *testing.T) {
srv := newPaginatedOrgReposServer(t)
defer srv.Close()
@ -123,7 +123,7 @@ func newPaginatedUserReposServer(t *testing.T) *httptest.Server {
return httptest.NewServer(mux)
}
func TestClient_ListUserRepos_Good_SinglePage(t *testing.T) {
func TestClient_ListUserRepos_Good_SinglePage_Good(t *testing.T) {
srv := newPaginatedUserReposServer(t)
defer srv.Close()
@ -175,7 +175,7 @@ func newPRMetaWithManyCommentsServer(t *testing.T) *httptest.Server {
return httptest.NewServer(mux)
}
func TestClient_GetPRMeta_Good_CommentCount(t *testing.T) {
func TestClient_GetPRMeta_Good_CommentCount_Good(t *testing.T) {
srv := newPRMetaWithManyCommentsServer(t)
defer srv.Close()
@ -219,7 +219,7 @@ func newPRMetaWithNilDatesServer(t *testing.T) *httptest.Server {
return httptest.NewServer(mux)
}
func TestClient_GetPRMeta_Good_MinimalFields(t *testing.T) {
func TestClient_GetPRMeta_Good_MinimalFields_Good(t *testing.T) {
srv := newPRMetaWithNilDatesServer(t)
defer srv.Close()
@ -238,7 +238,7 @@ func TestClient_GetPRMeta_Good_MinimalFields(t *testing.T) {
// --- GetCommentBodies: empty result ---
func TestClient_GetCommentBodies_Good_Empty(t *testing.T) {
func TestClient_GetCommentBodies_Good_Empty_Good(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, map[string]string{"version": "1.21.0"})
@ -263,7 +263,7 @@ func TestClient_GetCommentBodies_Good_Empty(t *testing.T) {
// --- GetCommentBodies: poster is nil ---
func TestClient_GetCommentBodies_Good_NilPoster(t *testing.T) {
func TestClient_GetCommentBodies_Good_NilPoster_Good(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, map[string]string{"version": "1.21.0"})
@ -293,7 +293,7 @@ func TestClient_GetCommentBodies_Good_NilPoster(t *testing.T) {
// --- ListPullRequests: state mapping ---
func TestClient_ListPullRequests_Good_AllStates(t *testing.T) {
func TestClient_ListPullRequests_Good_AllStates_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -305,7 +305,7 @@ func TestClient_ListPullRequests_Good_AllStates(t *testing.T) {
// --- NewFromConfig: additional paths ---
func TestNewFromConfig_Good_FlagOverridesEnv(t *testing.T) {
func TestNewFromConfig_Good_FlagOverridesEnv_Good(t *testing.T) {
isolateConfigEnv(t)
srv := newMockGiteaServer(t)

View file

@ -21,7 +21,7 @@ func TestClient_ListIssues_Good(t *testing.T) {
assert.Equal(t, "Issue 1", issues[0].Title)
}
func TestClient_ListIssues_Good_StateMapping(t *testing.T) {
func TestClient_ListIssues_Good_StateMapping_Good(t *testing.T) {
tests := []struct {
name string
state string
@ -43,7 +43,7 @@ func TestClient_ListIssues_Good_StateMapping(t *testing.T) {
}
}
func TestClient_ListIssues_Good_CustomPageAndLimit(t *testing.T) {
func TestClient_ListIssues_Good_CustomPageAndLimit_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -54,7 +54,7 @@ func TestClient_ListIssues_Good_CustomPageAndLimit(t *testing.T) {
require.NoError(t, err)
}
func TestClient_ListIssues_Bad_ServerError(t *testing.T) {
func TestClient_ListIssues_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -72,7 +72,7 @@ func TestClient_GetIssue_Good(t *testing.T) {
assert.Equal(t, "Issue 1", issue.Title)
}
func TestClient_GetIssue_Bad_ServerError(t *testing.T) {
func TestClient_GetIssue_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -93,7 +93,7 @@ func TestClient_CreateIssue_Good(t *testing.T) {
assert.NotNil(t, issue)
}
func TestClient_CreateIssue_Bad_ServerError(t *testing.T) {
func TestClient_CreateIssue_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -114,7 +114,7 @@ func TestClient_ListPullRequests_Good(t *testing.T) {
assert.Equal(t, "PR 1", prs[0].Title)
}
func TestClient_ListPullRequests_Good_StateMapping(t *testing.T) {
func TestClient_ListPullRequests_Good_StateMapping_Good(t *testing.T) {
tests := []struct {
name string
state string
@ -136,7 +136,7 @@ func TestClient_ListPullRequests_Good_StateMapping(t *testing.T) {
}
}
func TestClient_ListPullRequests_Bad_ServerError(t *testing.T) {
func TestClient_ListPullRequests_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -154,7 +154,7 @@ func TestClient_GetPullRequest_Good(t *testing.T) {
assert.Equal(t, "PR 1", pr.Title)
}
func TestClient_GetPullRequest_Bad_ServerError(t *testing.T) {
func TestClient_GetPullRequest_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -165,7 +165,7 @@ func TestClient_GetPullRequest_Bad_ServerError(t *testing.T) {
// --- ListIssuesOpts defaulting ---
func TestListIssuesOpts_Good_Defaults(t *testing.T) {
func TestListIssuesOpts_Good_Defaults_Good(t *testing.T) {
tests := []struct {
name string
opts ListIssuesOpts

View file

@ -40,6 +40,7 @@ const commentPageSize = 50
// GetPRMeta returns structural signals for a pull request.
// This is the Gitea side of the dual MetaReader described in the pipeline design.
// Usage: GetPRMeta(...)
func (c *Client) GetPRMeta(owner, repo string, pr int64) (*PRMeta, error) {
pull, _, err := c.api.GetPullRequest(owner, repo, pr)
if err != nil {
@ -98,6 +99,7 @@ func (c *Client) GetPRMeta(owner, repo string, pr int64) (*PRMeta, error) {
// GetCommentBodies returns all comment bodies for a pull request.
// This reads full content, which is safe on the home lab Gitea instance.
// Usage: GetCommentBodies(...)
func (c *Client) GetCommentBodies(owner, repo string, pr int64) ([]Comment, error) {
var comments []Comment
page := 1
@ -138,6 +140,7 @@ func (c *Client) GetCommentBodies(owner, repo string, pr int64) ([]Comment, erro
// GetIssueBody returns the body text of an issue.
// This reads full content, which is safe on the home lab Gitea instance.
// Usage: GetIssueBody(...)
func (c *Client) GetIssueBody(owner, repo string, issue int64) (string, error) {
iss, _, err := c.api.GetIssue(owner, repo, issue)
if err != nil {

View file

@ -25,7 +25,7 @@ func TestClient_GetPRMeta_Good(t *testing.T) {
assert.False(t, meta.IsMerged)
}
func TestClient_GetPRMeta_Bad_ServerError(t *testing.T) {
func TestClient_GetPRMeta_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -47,7 +47,7 @@ func TestClient_GetCommentBodies_Good(t *testing.T) {
assert.Equal(t, "user2", comments[1].Author)
}
func TestClient_GetCommentBodies_Bad_ServerError(t *testing.T) {
func TestClient_GetCommentBodies_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -65,7 +65,7 @@ func TestClient_GetIssueBody_Good(t *testing.T) {
assert.Equal(t, "First issue body", body)
}
func TestClient_GetIssueBody_Bad_ServerError(t *testing.T) {
func TestClient_GetIssueBody_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -76,7 +76,7 @@ func TestClient_GetIssueBody_Bad_ServerError(t *testing.T) {
// --- PRMeta struct tests ---
func TestPRMeta_Good_Fields(t *testing.T) {
func TestPRMeta_Good_Fields_Good(t *testing.T) {
meta := &PRMeta{
Number: 42,
Title: "Test PR",
@ -102,7 +102,7 @@ func TestPRMeta_Good_Fields(t *testing.T) {
assert.Equal(t, 5, meta.CommentCount)
}
func TestComment_Good_Fields(t *testing.T) {
func TestComment_Good_Fields_Good(t *testing.T) {
comment := Comment{
ID: 123,
Author: "reviewer",

View file

@ -21,7 +21,7 @@ func TestClient_ListOrgRepos_Good(t *testing.T) {
assert.Equal(t, "org-repo", repos[0].Name)
}
func TestClient_ListOrgRepos_Bad_ServerError(t *testing.T) {
func TestClient_ListOrgRepos_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -41,7 +41,7 @@ func TestClient_ListUserRepos_Good(t *testing.T) {
assert.Equal(t, "repo-b", repos[1].Name)
}
func TestClient_ListUserRepos_Bad_ServerError(t *testing.T) {
func TestClient_ListUserRepos_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -59,7 +59,7 @@ func TestClient_GetRepo_Good(t *testing.T) {
assert.Equal(t, "org-repo", repo.Name)
}
func TestClient_GetRepo_Bad_ServerError(t *testing.T) {
func TestClient_GetRepo_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -68,7 +68,7 @@ func TestClient_GetRepo_Bad_ServerError(t *testing.T) {
assert.Contains(t, err.Error(), "failed to get repo")
}
func TestClient_CreateMirror_Good_WithAuth(t *testing.T) {
func TestClient_CreateMirror_Good_WithAuth_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -78,7 +78,7 @@ func TestClient_CreateMirror_Good_WithAuth(t *testing.T) {
assert.NotNil(t, repo)
}
func TestClient_CreateMirror_Bad_NoAuthToken(t *testing.T) {
func TestClient_CreateMirror_Bad_NoAuthToken_Good(t *testing.T) {
client, srv := newTestClient(t)
defer srv.Close()
@ -88,7 +88,7 @@ func TestClient_CreateMirror_Bad_NoAuthToken(t *testing.T) {
assert.Contains(t, err.Error(), "failed to create mirror")
}
func TestClient_CreateMirror_Bad_ServerError(t *testing.T) {
func TestClient_CreateMirror_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -105,7 +105,7 @@ func TestClient_DeleteRepo_Good(t *testing.T) {
require.NoError(t, err)
}
func TestClient_DeleteRepo_Bad_ServerError(t *testing.T) {
func TestClient_DeleteRepo_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()
@ -126,7 +126,7 @@ func TestClient_CreateOrgRepo_Good(t *testing.T) {
assert.NotNil(t, repo)
}
func TestClient_CreateOrgRepo_Bad_ServerError(t *testing.T) {
func TestClient_CreateOrgRepo_Bad_ServerError_Good(t *testing.T) {
client, srv := newErrorServer(t)
defer srv.Close()

View file

@ -37,7 +37,7 @@ func newTestClient(t *testing.T, url string) *forge.Client {
return client
}
func TestForgejoSource_Good_Name(t *testing.T) {
func TestForgejoSource_Good_Name_Good(t *testing.T) {
s := New(Config{}, nil)
assert.Equal(t, "forgejo", s.Name())
}
@ -108,7 +108,7 @@ func TestForgejoSource_Poll_Good(t *testing.T) {
assert.Equal(t, "abc123", sig.LastCommitSHA)
}
func TestForgejoSource_Poll_Good_NoEpics(t *testing.T) {
func TestForgejoSource_Poll_Good_NoEpics_Good(t *testing.T) {
srv := httptest.NewServer(withVersion(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode([]any{})

View file

@ -30,16 +30,19 @@ func NewCompletionHandler(client *forge.Client) *CompletionHandler {
}
// Name returns the handler identifier.
// Usage: Name(...)
func (h *CompletionHandler) Name() string {
return "completion"
}
// Match returns true if the signal indicates an agent has finished a task.
// Usage: Match(...)
func (h *CompletionHandler) Match(signal *jobrunner.PipelineSignal) bool {
return signal.Type == "agent_completion"
}
// Execute updates the issue labels based on the completion status.
// Usage: Execute(...)
func (h *CompletionHandler) Execute(ctx context.Context, signal *jobrunner.PipelineSignal) (*jobrunner.ActionResult, error) {
start := time.Now()

View file

@ -37,7 +37,7 @@ func newTestSpinner(agents map[string]agentci.AgentConfig) *agentci.Spinner {
// --- Match tests ---
func TestDispatch_Match_Good_NeedsCoding(t *testing.T) {
func TestDispatch_Match_Good_NeedsCoding_Good(t *testing.T) {
spinner := newTestSpinner(map[string]agentci.AgentConfig{
"darbs-claude": {Host: "claude@192.168.0.201", QueueDir: "~/ai-work/queue", Active: true},
})
@ -49,7 +49,7 @@ func TestDispatch_Match_Good_NeedsCoding(t *testing.T) {
assert.True(t, h.Match(sig))
}
func TestDispatch_Match_Good_MultipleAgents(t *testing.T) {
func TestDispatch_Match_Good_MultipleAgents_Good(t *testing.T) {
spinner := newTestSpinner(map[string]agentci.AgentConfig{
"darbs-claude": {Host: "claude@192.168.0.201", QueueDir: "~/ai-work/queue", Active: true},
"local-codex": {Host: "localhost", QueueDir: "~/ai-work/queue", Active: true},
@ -62,7 +62,7 @@ func TestDispatch_Match_Good_MultipleAgents(t *testing.T) {
assert.True(t, h.Match(sig))
}
func TestDispatch_Match_Bad_HasPR(t *testing.T) {
func TestDispatch_Match_Bad_HasPR_Good(t *testing.T) {
spinner := newTestSpinner(map[string]agentci.AgentConfig{
"darbs-claude": {Host: "claude@192.168.0.201", QueueDir: "~/ai-work/queue", Active: true},
})
@ -75,7 +75,7 @@ func TestDispatch_Match_Bad_HasPR(t *testing.T) {
assert.False(t, h.Match(sig))
}
func TestDispatch_Match_Bad_UnknownAgent(t *testing.T) {
func TestDispatch_Match_Bad_UnknownAgent_Good(t *testing.T) {
spinner := newTestSpinner(map[string]agentci.AgentConfig{
"darbs-claude": {Host: "claude@192.168.0.201", QueueDir: "~/ai-work/queue", Active: true},
})
@ -87,7 +87,7 @@ func TestDispatch_Match_Bad_UnknownAgent(t *testing.T) {
assert.False(t, h.Match(sig))
}
func TestDispatch_Match_Bad_NotAssigned(t *testing.T) {
func TestDispatch_Match_Bad_NotAssigned_Good(t *testing.T) {
spinner := newTestSpinner(map[string]agentci.AgentConfig{
"darbs-claude": {Host: "claude@192.168.0.201", QueueDir: "~/ai-work/queue", Active: true},
})
@ -99,7 +99,7 @@ func TestDispatch_Match_Bad_NotAssigned(t *testing.T) {
assert.False(t, h.Match(sig))
}
func TestDispatch_Match_Bad_EmptyAgentMap(t *testing.T) {
func TestDispatch_Match_Bad_EmptyAgentMap_Good(t *testing.T) {
spinner := newTestSpinner(map[string]agentci.AgentConfig{})
h := NewDispatchHandler(nil, "", "", spinner)
sig := &jobrunner.PipelineSignal{
@ -119,7 +119,7 @@ func TestDispatch_Name_Good(t *testing.T) {
// --- Execute tests ---
func TestDispatch_Execute_Bad_UnknownAgent(t *testing.T) {
func TestDispatch_Execute_Bad_UnknownAgent_Good(t *testing.T) {
srv := httptest.NewServer(withVersion(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})))
@ -144,7 +144,7 @@ func TestDispatch_Execute_Bad_UnknownAgent(t *testing.T) {
assert.Contains(t, err.Error(), "unknown agent")
}
func TestDispatch_Execute_Bad_InvalidQueueDir(t *testing.T) {
func TestDispatch_Execute_Bad_InvalidQueueDir_Good(t *testing.T) {
spinner := newTestSpinner(map[string]agentci.AgentConfig{
"darbs-claude": {
Host: "localhost",
@ -209,7 +209,7 @@ func TestDispatch_TicketJSON_Good(t *testing.T) {
assert.False(t, hasToken, "forge_token must not be in ticket JSON")
}
func TestDispatch_TicketJSON_Good_DualRun(t *testing.T) {
func TestDispatch_TicketJSON_Good_DualRun_Good(t *testing.T) {
ticket := DispatchTicket{
ID: "test-dual",
RepoOwner: "host-uk",
@ -231,7 +231,7 @@ func TestDispatch_TicketJSON_Good_DualRun(t *testing.T) {
assert.Equal(t, "gemini-1.5-pro", roundtrip.VerifyModel)
}
func TestDispatch_TicketJSON_Good_OmitsEmptyModelRunner(t *testing.T) {
func TestDispatch_TicketJSON_Good_OmitsEmptyModelRunner_Good(t *testing.T) {
ticket := DispatchTicket{
ID: "test-1",
RepoOwner: "host-uk",
@ -254,7 +254,7 @@ func TestDispatch_TicketJSON_Good_OmitsEmptyModelRunner(t *testing.T) {
assert.False(t, hasRunner, "runner should be omitted when empty")
}
func TestDispatch_runRemote_Good_EscapesPath(t *testing.T) {
func TestDispatch_runRemote_Good_EscapesPath_Good(t *testing.T) {
outputPath := filepath.Join(t.TempDir(), "ssh-output.txt")
toolPath := writeFakeSSHCommand(t, outputPath)
t.Setenv("PATH", toolPath+":"+os.Getenv("PATH"))
@ -275,7 +275,7 @@ func TestDispatch_runRemote_Good_EscapesPath(t *testing.T) {
assert.Contains(t, string(output), "rm '-f' '"+dangerousPath+"'\n")
}
func TestDispatch_secureTransfer_Good_EscapesPath(t *testing.T) {
func TestDispatch_secureTransfer_Good_EscapesPath_Good(t *testing.T) {
outputPath := filepath.Join(t.TempDir(), "ssh-output.txt")
toolPath := writeFakeSSHCommand(t, outputPath)
t.Setenv("PATH", toolPath+":"+os.Getenv("PATH"))
@ -301,7 +301,7 @@ func TestDispatch_secureTransfer_Good_EscapesPath(t *testing.T) {
assert.Equal(t, "hello", string(input))
}
func TestDispatch_TicketJSON_Good_ModelRunnerVariants(t *testing.T) {
func TestDispatch_TicketJSON_Good_ModelRunnerVariants_Good(t *testing.T) {
tests := []struct {
name string
model string
@ -338,7 +338,7 @@ func TestDispatch_TicketJSON_Good_ModelRunnerVariants(t *testing.T) {
}
}
func TestDispatch_Execute_Good_PostsComment(t *testing.T) {
func TestDispatch_Execute_Good_PostsComment_Good(t *testing.T) {
var commentPosted bool
var commentBody string

View file

@ -22,12 +22,14 @@ func NewEnableAutoMergeHandler(f *forge.Client) *EnableAutoMergeHandler {
}
// Name returns the handler identifier.
// Usage: Name(...)
func (h *EnableAutoMergeHandler) Name() string {
return "enable_auto_merge"
}
// Match returns true when the PR is open, not a draft, mergeable, checks
// are passing, and there are no unresolved review threads.
// Usage: Match(...)
func (h *EnableAutoMergeHandler) Match(signal *jobrunner.PipelineSignal) bool {
return signal.PRState == "OPEN" &&
!signal.IsDraft &&
@ -37,6 +39,7 @@ func (h *EnableAutoMergeHandler) Match(signal *jobrunner.PipelineSignal) bool {
}
// Execute merges the pull request with squash strategy.
// Usage: Execute(...)
func (h *EnableAutoMergeHandler) Execute(ctx context.Context, signal *jobrunner.PipelineSignal) (*jobrunner.ActionResult, error) {
start := time.Now()

View file

@ -28,7 +28,7 @@ func TestEnableAutoMerge_Match_Good(t *testing.T) {
assert.True(t, h.Match(sig))
}
func TestEnableAutoMerge_Match_Bad_Draft(t *testing.T) {
func TestEnableAutoMerge_Match_Bad_Draft_Good(t *testing.T) {
h := NewEnableAutoMergeHandler(nil)
sig := &jobrunner.PipelineSignal{
PRState: "OPEN",
@ -41,7 +41,7 @@ func TestEnableAutoMerge_Match_Bad_Draft(t *testing.T) {
assert.False(t, h.Match(sig))
}
func TestEnableAutoMerge_Match_Bad_UnresolvedThreads(t *testing.T) {
func TestEnableAutoMerge_Match_Bad_UnresolvedThreads_Good(t *testing.T) {
h := NewEnableAutoMergeHandler(nil)
sig := &jobrunner.PipelineSignal{
PRState: "OPEN",
@ -83,7 +83,7 @@ func TestEnableAutoMerge_Execute_Good(t *testing.T) {
assert.Equal(t, "/api/v1/repos/host-uk/core-php/pulls/55/merge", capturedPath)
}
func TestEnableAutoMerge_Execute_Bad_MergeFailed(t *testing.T) {
func TestEnableAutoMerge_Execute_Bad_MergeFailed_Good(t *testing.T) {
srv := httptest.NewServer(withVersion(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusConflict)
_ = json.NewEncoder(w).Encode(map[string]string{"message": "merge conflict"})

View file

@ -22,11 +22,13 @@ func NewPublishDraftHandler(f *forge.Client) *PublishDraftHandler {
}
// Name returns the handler identifier.
// Usage: Name(...)
func (h *PublishDraftHandler) Name() string {
return "publish_draft"
}
// Match returns true when the PR is a draft, open, and all checks have passed.
// Usage: Match(...)
func (h *PublishDraftHandler) Match(signal *jobrunner.PipelineSignal) bool {
return signal.IsDraft &&
signal.PRState == "OPEN" &&
@ -34,6 +36,7 @@ func (h *PublishDraftHandler) Match(signal *jobrunner.PipelineSignal) bool {
}
// Execute marks the PR as no longer a draft.
// Usage: Execute(...)
func (h *PublishDraftHandler) Execute(ctx context.Context, signal *jobrunner.PipelineSignal) (*jobrunner.ActionResult, error) {
start := time.Now()

View file

@ -25,7 +25,7 @@ func TestPublishDraft_Match_Good(t *testing.T) {
assert.True(t, h.Match(sig))
}
func TestPublishDraft_Match_Bad_NotDraft(t *testing.T) {
func TestPublishDraft_Match_Bad_NotDraft_Good(t *testing.T) {
h := NewPublishDraftHandler(nil)
sig := &jobrunner.PipelineSignal{
IsDraft: false,
@ -35,7 +35,7 @@ func TestPublishDraft_Match_Bad_NotDraft(t *testing.T) {
assert.False(t, h.Match(sig))
}
func TestPublishDraft_Match_Bad_ChecksFailing(t *testing.T) {
func TestPublishDraft_Match_Bad_ChecksFailing_Good(t *testing.T) {
h := NewPublishDraftHandler(nil)
sig := &jobrunner.PipelineSignal{
IsDraft: true,

View file

@ -27,16 +27,19 @@ func NewDismissReviewsHandler(f *forge.Client) *DismissReviewsHandler {
}
// Name returns the handler identifier.
// Usage: Name(...)
func (h *DismissReviewsHandler) Name() string {
return "dismiss_reviews"
}
// Match returns true when the PR is open and has unresolved review threads.
// Usage: Match(...)
func (h *DismissReviewsHandler) Match(signal *jobrunner.PipelineSignal) bool {
return signal.PRState == "OPEN" && signal.HasUnresolvedThreads()
}
// Execute dismisses stale "request changes" reviews on the PR.
// Usage: Execute(...)
func (h *DismissReviewsHandler) Execute(ctx context.Context, signal *jobrunner.PipelineSignal) (*jobrunner.ActionResult, error) {
start := time.Now()

View file

@ -25,7 +25,7 @@ func TestDismissReviews_Match_Good(t *testing.T) {
assert.True(t, h.Match(sig))
}
func TestDismissReviews_Match_Bad_AllResolved(t *testing.T) {
func TestDismissReviews_Match_Bad_AllResolved_Good(t *testing.T) {
h := NewDismissReviewsHandler(nil)
sig := &jobrunner.PipelineSignal{
PRState: "OPEN",

View file

@ -23,12 +23,14 @@ func NewSendFixCommandHandler(f *forge.Client) *SendFixCommandHandler {
}
// Name returns the handler identifier.
// Usage: Name(...)
func (h *SendFixCommandHandler) Name() string {
return "send_fix_command"
}
// Match returns true when the PR is open and either has merge conflicts or
// has unresolved threads with failing checks.
// Usage: Match(...)
func (h *SendFixCommandHandler) Match(signal *jobrunner.PipelineSignal) bool {
if signal.PRState != "OPEN" {
return false
@ -43,6 +45,7 @@ func (h *SendFixCommandHandler) Match(signal *jobrunner.PipelineSignal) bool {
}
// Execute posts a comment on the PR asking for a fix.
// Usage: Execute(...)
func (h *SendFixCommandHandler) Execute(ctx context.Context, signal *jobrunner.PipelineSignal) (*jobrunner.ActionResult, error) {
start := time.Now()

View file

@ -15,7 +15,7 @@ import (
"dappco.re/go/core/scm/jobrunner"
)
func TestSendFixCommand_Match_Good_Conflicting(t *testing.T) {
func TestSendFixCommand_Match_Good_Conflicting_Good(t *testing.T) {
h := NewSendFixCommandHandler(nil)
sig := &jobrunner.PipelineSignal{
PRState: "OPEN",
@ -24,7 +24,7 @@ func TestSendFixCommand_Match_Good_Conflicting(t *testing.T) {
assert.True(t, h.Match(sig))
}
func TestSendFixCommand_Match_Good_UnresolvedThreads(t *testing.T) {
func TestSendFixCommand_Match_Good_UnresolvedThreads_Good(t *testing.T) {
h := NewSendFixCommandHandler(nil)
sig := &jobrunner.PipelineSignal{
PRState: "OPEN",
@ -36,7 +36,7 @@ func TestSendFixCommand_Match_Good_UnresolvedThreads(t *testing.T) {
assert.True(t, h.Match(sig))
}
func TestSendFixCommand_Match_Bad_Clean(t *testing.T) {
func TestSendFixCommand_Match_Bad_Clean_Good(t *testing.T) {
h := NewSendFixCommandHandler(nil)
sig := &jobrunner.PipelineSignal{
PRState: "OPEN",
@ -48,7 +48,7 @@ func TestSendFixCommand_Match_Bad_Clean(t *testing.T) {
assert.False(t, h.Match(sig))
}
func TestSendFixCommand_Execute_Good_Conflict(t *testing.T) {
func TestSendFixCommand_Execute_Good_Conflict_Good(t *testing.T) {
var capturedMethod string
var capturedPath string
var capturedBody string

View file

@ -27,17 +27,20 @@ func NewTickParentHandler(f *forge.Client) *TickParentHandler {
}
// Name returns the handler identifier.
// Usage: Name(...)
func (h *TickParentHandler) Name() string {
return "tick_parent"
}
// Match returns true when the child PR has been merged.
// Usage: Match(...)
func (h *TickParentHandler) Match(signal *jobrunner.PipelineSignal) bool {
return signal.PRState == "MERGED"
}
// Execute fetches the epic body, replaces the unchecked checkbox for the
// child issue with a checked one, updates the epic, and closes the child issue.
// Usage: Execute(...)
func (h *TickParentHandler) Execute(ctx context.Context, signal *jobrunner.PipelineSignal) (*jobrunner.ActionResult, error) {
start := time.Now()

View file

@ -25,7 +25,7 @@ func TestTickParent_Match_Good(t *testing.T) {
assert.True(t, h.Match(sig))
}
func TestTickParent_Match_Bad_Open(t *testing.T) {
func TestTickParent_Match_Bad_Open_Good(t *testing.T) {
h := NewTickParentHandler(nil)
sig := &jobrunner.PipelineSignal{
PRState: "OPEN",

View file

@ -115,7 +115,7 @@ func TestJournal_Append_Good(t *testing.T) {
assert.Equal(t, 2, lines, "expected two JSONL lines after two appends")
}
func TestJournal_Append_Bad_PathTraversal(t *testing.T) {
func TestJournal_Append_Bad_PathTraversal_Good(t *testing.T) {
dir := t.TempDir()
j, err := NewJournal(dir)
@ -197,7 +197,7 @@ func TestJournal_Append_Bad_PathTraversal(t *testing.T) {
}
}
func TestJournal_Append_Good_ValidNames(t *testing.T) {
func TestJournal_Append_Good_ValidNames_Good(t *testing.T) {
dir := t.TempDir()
j, err := NewJournal(dir)
@ -232,7 +232,7 @@ func TestJournal_Append_Good_ValidNames(t *testing.T) {
}
}
func TestJournal_Append_Bad_NilSignal(t *testing.T) {
func TestJournal_Append_Bad_NilSignal_Good(t *testing.T) {
dir := t.TempDir()
j, err := NewJournal(dir)
@ -248,7 +248,7 @@ func TestJournal_Append_Bad_NilSignal(t *testing.T) {
assert.Contains(t, err.Error(), "signal is required")
}
func TestJournal_Append_Bad_NilResult(t *testing.T) {
func TestJournal_Append_Bad_NilResult_Good(t *testing.T) {
dir := t.TempDir()
j, err := NewJournal(dir)

View file

@ -120,7 +120,7 @@ func TestPoller_RunOnce_Good(t *testing.T) {
assert.Equal(t, 1, p.Cycle())
}
func TestPoller_RunOnce_Good_NoSignals(t *testing.T) {
func TestPoller_RunOnce_Good_NoSignals_Good(t *testing.T) {
src := &mockSource{
name: "empty-source",
signals: nil,
@ -151,7 +151,7 @@ func TestPoller_RunOnce_Good_NoSignals(t *testing.T) {
assert.Equal(t, 1, p.Cycle())
}
func TestPoller_RunOnce_Good_NoMatchingHandler(t *testing.T) {
func TestPoller_RunOnce_Good_NoMatchingHandler_Good(t *testing.T) {
sig := &PipelineSignal{
EpicNumber: 5,
ChildNumber: 8,
@ -192,7 +192,7 @@ func TestPoller_RunOnce_Good_NoMatchingHandler(t *testing.T) {
assert.Empty(t, src.reports)
}
func TestPoller_RunOnce_Good_DryRun(t *testing.T) {
func TestPoller_RunOnce_Good_DryRun_Good(t *testing.T) {
sig := &PipelineSignal{
EpicNumber: 1,
ChildNumber: 3,

View file

@ -27,7 +27,7 @@ func TestPipelineSignal_HasUnresolvedThreads_Good(t *testing.T) {
assert.True(t, sig.HasUnresolvedThreads())
}
func TestPipelineSignal_HasUnresolvedThreads_Bad_AllResolved(t *testing.T) {
func TestPipelineSignal_HasUnresolvedThreads_Bad_AllResolved_Good(t *testing.T) {
sig := &PipelineSignal{
ThreadsTotal: 4,
ThreadsResolved: 4,

View file

@ -37,7 +37,7 @@ func TestCompile_Good(t *testing.T) {
assert.NotEmpty(t, cm.BuiltAt)
}
func TestCompile_Good_WithSigning(t *testing.T) {
func TestCompile_Good_WithSigning_Good(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
@ -60,20 +60,20 @@ func TestCompile_Good_WithSigning(t *testing.T) {
assert.True(t, ok)
}
func TestCompile_Bad_NilManifest(t *testing.T) {
func TestCompile_Bad_NilManifest_Good(t *testing.T) {
_, err := Compile(nil, CompileOptions{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "nil manifest")
}
func TestCompile_Bad_MissingCode(t *testing.T) {
func TestCompile_Bad_MissingCode_Good(t *testing.T) {
m := &Manifest{Version: "1.0.0"}
_, err := Compile(m, CompileOptions{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "missing code")
}
func TestCompile_Bad_MissingVersion(t *testing.T) {
func TestCompile_Bad_MissingVersion_Good(t *testing.T) {
m := &Manifest{Code: "test"}
_, err := Compile(m, CompileOptions{})
assert.Error(t, err)
@ -162,13 +162,13 @@ func TestLoadCompiled_Good(t *testing.T) {
assert.Equal(t, "ddd444", cm.Commit)
}
func TestLoadCompiled_Bad_NotFound(t *testing.T) {
func TestLoadCompiled_Bad_NotFound_Good(t *testing.T) {
medium := io.NewMockMedium()
_, err := LoadCompiled(medium, "/missing")
assert.Error(t, err)
}
func TestCompile_Good_MinimalOptions(t *testing.T) {
func TestCompile_Good_MinimalOptions_Good(t *testing.T) {
m := &Manifest{
Code: "minimal",
Name: "Minimal",

View file

@ -27,7 +27,7 @@ slots:
assert.Equal(t, "main-content", m.Slots["C"])
}
func TestLoad_Bad_NoManifest(t *testing.T) {
func TestLoad_Bad_NoManifest_Good(t *testing.T) {
fs := io.NewMockMedium()
_, err := Load(fs, ".")
assert.Error(t, err)
@ -50,7 +50,7 @@ func TestLoadVerified_Good(t *testing.T) {
assert.Equal(t, "signed-app", loaded.Code)
}
func TestLoadVerified_Bad_Tampered(t *testing.T) {
func TestLoadVerified_Bad_Tampered_Good(t *testing.T) {
pub, priv, _ := ed25519.GenerateKey(nil)
m := &Manifest{Code: "app", Version: "1.0.0"}
_ = Sign(m, priv)

View file

@ -43,6 +43,7 @@ type ElementSpec struct {
// IsProvider returns true if this manifest declares provider fields
// (namespace and binary), indicating it is a runtime provider.
// Usage: IsProvider(...)
func (m *Manifest) IsProvider() bool {
return m.Namespace != "" && m.Binary != ""
}

View file

@ -66,7 +66,7 @@ func TestManifest_SlotNames_Good(t *testing.T) {
assert.Len(t, names, 2)
}
func TestParse_Good_WithDaemons(t *testing.T) {
func TestParse_Good_WithDaemons_Good(t *testing.T) {
raw := `
code: my-service
name: My Service
@ -126,7 +126,7 @@ func TestManifest_DefaultDaemon_Good(t *testing.T) {
assert.True(t, spec.Default)
}
func TestManifest_DefaultDaemon_Bad_NoDaemons(t *testing.T) {
func TestManifest_DefaultDaemon_Bad_NoDaemons_Good(t *testing.T) {
m := Manifest{}
name, spec, ok := m.DefaultDaemon()
assert.False(t, ok)
@ -134,7 +134,7 @@ func TestManifest_DefaultDaemon_Bad_NoDaemons(t *testing.T) {
assert.Empty(t, spec.Binary)
}
func TestManifest_DefaultDaemon_Bad_MultipleDefaults(t *testing.T) {
func TestManifest_DefaultDaemon_Bad_MultipleDefaults_Good(t *testing.T) {
m := Manifest{
Daemons: map[string]DaemonSpec{
"api": {Binary: "./bin/api", Default: true},
@ -145,7 +145,7 @@ func TestManifest_DefaultDaemon_Bad_MultipleDefaults(t *testing.T) {
assert.False(t, ok)
}
func TestManifest_DefaultDaemon_Bad_MultipleNoneDefault(t *testing.T) {
func TestManifest_DefaultDaemon_Bad_MultipleNoneDefault_Good(t *testing.T) {
m := Manifest{
Daemons: map[string]DaemonSpec{
"api": {Binary: "./bin/api"},
@ -156,7 +156,7 @@ func TestManifest_DefaultDaemon_Bad_MultipleNoneDefault(t *testing.T) {
assert.False(t, ok)
}
func TestParse_Good_WithProviderFields(t *testing.T) {
func TestParse_Good_WithProviderFields_Good(t *testing.T) {
raw := `
code: cool-widget
name: Cool Widget Dashboard
@ -204,22 +204,22 @@ func TestManifest_IsProvider_Good(t *testing.T) {
assert.True(t, m.IsProvider())
}
func TestManifest_IsProvider_Bad_NoNamespace(t *testing.T) {
func TestManifest_IsProvider_Bad_NoNamespace_Good(t *testing.T) {
m := Manifest{Binary: "./test"}
assert.False(t, m.IsProvider())
}
func TestManifest_IsProvider_Bad_NoBinary(t *testing.T) {
func TestManifest_IsProvider_Bad_NoBinary_Good(t *testing.T) {
m := Manifest{Namespace: "/api/v1/test"}
assert.False(t, m.IsProvider())
}
func TestManifest_IsProvider_Bad_Empty(t *testing.T) {
func TestManifest_IsProvider_Bad_Empty_Good(t *testing.T) {
m := Manifest{}
assert.False(t, m.IsProvider())
}
func TestManifest_DefaultDaemon_Good_SingleImplicit(t *testing.T) {
func TestManifest_DefaultDaemon_Good_SingleImplicit_Good(t *testing.T) {
m := Manifest{
Daemons: map[string]DaemonSpec{
"server": {

View file

@ -31,7 +31,7 @@ func TestSignAndVerify_Good(t *testing.T) {
assert.True(t, ok)
}
func TestVerify_Bad_Tampered(t *testing.T) {
func TestVerify_Bad_Tampered_Good(t *testing.T) {
pub, priv, _ := ed25519.GenerateKey(nil)
m := &Manifest{Code: "test-app", Version: "1.0.0"}
_ = Sign(m, priv)
@ -43,7 +43,7 @@ func TestVerify_Bad_Tampered(t *testing.T) {
assert.False(t, ok)
}
func TestVerify_Bad_Unsigned(t *testing.T) {
func TestVerify_Bad_Unsigned_Good(t *testing.T) {
pub, _, _ := ed25519.GenerateKey(nil)
m := &Manifest{Code: "test-app"}

View file

@ -38,7 +38,7 @@ func writeCoreJSON(t *testing.T, dir, code, name, version string) {
require.NoError(t, os.WriteFile(filepath.Join(dir, "core.json"), data, 0644))
}
func TestBuildFromDirs_Good_ManifestYAML(t *testing.T) {
func TestBuildFromDirs_Good_ManifestYAML_Good(t *testing.T) {
root := t.TempDir()
modDir := filepath.Join(root, "my-widget")
require.NoError(t, os.MkdirAll(modDir, 0755))
@ -55,7 +55,7 @@ func TestBuildFromDirs_Good_ManifestYAML(t *testing.T) {
assert.Equal(t, IndexVersion, idx.Version)
}
func TestBuildFromDirs_Good_CoreJSON(t *testing.T) {
func TestBuildFromDirs_Good_CoreJSON_Good(t *testing.T) {
root := t.TempDir()
modDir := filepath.Join(root, "compiled-mod")
require.NoError(t, os.MkdirAll(modDir, 0755))
@ -70,7 +70,7 @@ func TestBuildFromDirs_Good_CoreJSON(t *testing.T) {
assert.Equal(t, "Compiled Module", idx.Modules[0].Name)
}
func TestBuildFromDirs_Good_PrefersCompiledOverSource(t *testing.T) {
func TestBuildFromDirs_Good_PrefersCompiledOverSource_Good(t *testing.T) {
root := t.TempDir()
modDir := filepath.Join(root, "dual-mod")
require.NoError(t, os.MkdirAll(modDir, 0755))
@ -86,7 +86,7 @@ func TestBuildFromDirs_Good_PrefersCompiledOverSource(t *testing.T) {
assert.Equal(t, "compiled-code", idx.Modules[0].Code)
}
func TestBuildFromDirs_Good_SkipsNoManifest(t *testing.T) {
func TestBuildFromDirs_Good_SkipsNoManifest_Good(t *testing.T) {
root := t.TempDir()
// Directory with no manifest.
require.NoError(t, os.MkdirAll(filepath.Join(root, "no-manifest"), 0755))
@ -101,7 +101,7 @@ func TestBuildFromDirs_Good_SkipsNoManifest(t *testing.T) {
assert.Len(t, idx.Modules, 1)
}
func TestBuildFromDirs_Good_Deduplicates(t *testing.T) {
func TestBuildFromDirs_Good_Deduplicates_Good(t *testing.T) {
dir1 := t.TempDir()
dir2 := t.TempDir()
@ -120,7 +120,7 @@ func TestBuildFromDirs_Good_Deduplicates(t *testing.T) {
assert.Equal(t, "shared", idx.Modules[0].Code)
}
func TestBuildFromDirs_Good_SortsByCode(t *testing.T) {
func TestBuildFromDirs_Good_SortsByCode_Good(t *testing.T) {
root := t.TempDir()
for _, name := range []string{"charlie", "alpha", "bravo"} {
d := filepath.Join(root, name)
@ -137,7 +137,7 @@ func TestBuildFromDirs_Good_SortsByCode(t *testing.T) {
assert.Equal(t, "charlie", idx.Modules[2].Code)
}
func TestBuildFromDirs_Good_EmptyDir(t *testing.T) {
func TestBuildFromDirs_Good_EmptyDir_Good(t *testing.T) {
root := t.TempDir()
b := &Builder{}
idx, err := b.BuildFromDirs(root)
@ -146,14 +146,14 @@ func TestBuildFromDirs_Good_EmptyDir(t *testing.T) {
assert.Equal(t, IndexVersion, idx.Version)
}
func TestBuildFromDirs_Good_NonexistentDir(t *testing.T) {
func TestBuildFromDirs_Good_NonexistentDir_Good(t *testing.T) {
b := &Builder{}
idx, err := b.BuildFromDirs("/nonexistent/path")
require.NoError(t, err)
assert.Empty(t, idx.Modules)
}
func TestBuildFromDirs_Good_NoRepoURLWithoutConfig(t *testing.T) {
func TestBuildFromDirs_Good_NoRepoURLWithoutConfig_Good(t *testing.T) {
root := t.TempDir()
modDir := filepath.Join(root, "mod")
require.NoError(t, os.MkdirAll(modDir, 0755))
@ -177,7 +177,7 @@ func TestBuildFromManifests_Good(t *testing.T) {
assert.Equal(t, IndexVersion, idx.Version)
}
func TestBuildFromManifests_Good_SkipsNil(t *testing.T) {
func TestBuildFromManifests_Good_SkipsNil_Good(t *testing.T) {
manifests := []*manifest.Manifest{
nil,
{Code: "valid", Name: "Valid"},
@ -188,7 +188,7 @@ func TestBuildFromManifests_Good_SkipsNil(t *testing.T) {
assert.Equal(t, "valid", idx.Modules[0].Code)
}
func TestBuildFromManifests_Good_Deduplicates(t *testing.T) {
func TestBuildFromManifests_Good_Deduplicates_Good(t *testing.T) {
manifests := []*manifest.Manifest{
{Code: "dup", Name: "First"},
{Code: "dup", Name: "Second"},
@ -220,7 +220,7 @@ func TestWriteIndex_Good(t *testing.T) {
assert.Equal(t, "test-mod", parsed.Modules[0].Code)
}
func TestWriteIndex_Good_RoundTrip(t *testing.T) {
func TestWriteIndex_Good_RoundTrip_Good(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "index.json")

View file

@ -58,7 +58,7 @@ binary: ./data-viz
assert.True(t, codes["data-viz"])
}
func TestDiscoverProviders_Good_SkipNonProvider(t *testing.T) {
func TestDiscoverProviders_Good_SkipNonProvider_Good(t *testing.T) {
dir := t.TempDir()
// This has a valid manifest but no namespace/binary — not a provider.
@ -83,7 +83,7 @@ binary: ./real-provider
assert.Equal(t, "real-provider", providers[0].Manifest.Code)
}
func TestDiscoverProviders_Good_SkipNoManifest(t *testing.T) {
func TestDiscoverProviders_Good_SkipNoManifest_Good(t *testing.T) {
dir := t.TempDir()
// Directory with no manifest.
@ -104,7 +104,7 @@ binary: ./good-provider
assert.Equal(t, "good-provider", providers[0].Manifest.Code)
}
func TestDiscoverProviders_Good_SkipInvalidManifest(t *testing.T) {
func TestDiscoverProviders_Good_SkipInvalidManifest_Good(t *testing.T) {
dir := t.TempDir()
// Directory with invalid YAML.
@ -121,7 +121,7 @@ func TestDiscoverProviders_Good_SkipInvalidManifest(t *testing.T) {
assert.Empty(t, providers)
}
func TestDiscoverProviders_Good_EmptyDir(t *testing.T) {
func TestDiscoverProviders_Good_EmptyDir_Good(t *testing.T) {
dir := t.TempDir()
providers, err := DiscoverProviders(dir)
@ -129,13 +129,13 @@ func TestDiscoverProviders_Good_EmptyDir(t *testing.T) {
assert.Empty(t, providers)
}
func TestDiscoverProviders_Good_NonexistentDir(t *testing.T) {
func TestDiscoverProviders_Good_NonexistentDir_Good(t *testing.T) {
providers, err := DiscoverProviders("/tmp/nonexistent-discovery-test-dir")
require.NoError(t, err)
assert.Nil(t, providers)
}
func TestDiscoverProviders_Good_SkipFiles(t *testing.T) {
func TestDiscoverProviders_Good_SkipFiles_Good(t *testing.T) {
dir := t.TempDir()
// Create a regular file (not a directory).
@ -146,7 +146,7 @@ func TestDiscoverProviders_Good_SkipFiles(t *testing.T) {
assert.Empty(t, providers)
}
func TestDiscoverProviders_Good_ProviderDir(t *testing.T) {
func TestDiscoverProviders_Good_ProviderDir_Good(t *testing.T) {
dir := t.TempDir()
createProviderDir(t, dir, "test-prov", `
@ -194,7 +194,7 @@ func TestProviderRegistry_LoadSave_Good(t *testing.T) {
assert.True(t, entry.AutoStart)
}
func TestProviderRegistry_Load_Good_NonexistentFile(t *testing.T) {
func TestProviderRegistry_Load_Good_NonexistentFile_Good(t *testing.T) {
reg, err := LoadProviderRegistry("/tmp/nonexistent-registry-test.yaml")
require.NoError(t, err)
assert.Equal(t, 1, reg.Version)
@ -233,7 +233,7 @@ func TestProviderRegistry_Remove_Good(t *testing.T) {
assert.False(t, ok)
}
func TestProviderRegistry_Get_Bad_NotFound(t *testing.T) {
func TestProviderRegistry_Get_Bad_NotFound_Good(t *testing.T) {
reg := &ProviderRegistryFile{
Version: 1,
Providers: map[string]ProviderRegistryEntry{},

View file

@ -103,7 +103,7 @@ func TestInstall_Good(t *testing.T) {
assert.Contains(t, raw, `"version":"1.0"`)
}
func TestInstall_Good_Signed(t *testing.T) {
func TestInstall_Good_Signed_Good(t *testing.T) {
repo, signKey := createSignedTestRepo(t, "signed-mod", "2.0")
modulesDir := filepath.Join(t.TempDir(), "modules")
@ -124,7 +124,7 @@ func TestInstall_Good_Signed(t *testing.T) {
assert.Contains(t, raw, `"version":"2.0"`)
}
func TestInstall_Bad_AlreadyInstalled(t *testing.T) {
func TestInstall_Bad_AlreadyInstalled_Good(t *testing.T) {
repo := createTestRepo(t, "dup-mod", "1.0")
modulesDir := filepath.Join(t.TempDir(), "modules")
@ -141,7 +141,7 @@ func TestInstall_Bad_AlreadyInstalled(t *testing.T) {
assert.Contains(t, err.Error(), "already installed")
}
func TestInstall_Bad_InvalidSignature(t *testing.T) {
func TestInstall_Bad_InvalidSignature_Good(t *testing.T) {
// Sign with key A, verify with key B
repo, _ := createSignedTestRepo(t, "bad-sig", "1.0")
_, wrongKey := createSignedTestRepo(t, "dummy", "1.0") // different key
@ -165,7 +165,7 @@ func TestInstall_Bad_InvalidSignature(t *testing.T) {
assert.True(t, os.IsNotExist(statErr), "directory should be cleaned up on failure")
}
func TestInstall_Bad_PathTraversalCode(t *testing.T) {
func TestInstall_Bad_PathTraversalCode_Good(t *testing.T) {
repo := createTestRepo(t, "safe-mod", "1.0")
modulesDir := filepath.Join(t.TempDir(), "modules")
@ -211,7 +211,7 @@ func TestRemove_Good(t *testing.T) {
assert.Error(t, err)
}
func TestRemove_Bad_NotInstalled(t *testing.T) {
func TestRemove_Bad_NotInstalled_Good(t *testing.T) {
st, err := store.New(":memory:")
require.NoError(t, err)
defer st.Close()
@ -222,7 +222,7 @@ func TestRemove_Bad_NotInstalled(t *testing.T) {
assert.Contains(t, err.Error(), "not installed")
}
func TestRemove_Bad_PathTraversalCode(t *testing.T) {
func TestRemove_Bad_PathTraversalCode_Good(t *testing.T) {
baseDir := t.TempDir()
modulesDir := filepath.Join(baseDir, "modules")
escapeDir := filepath.Join(baseDir, "escape")
@ -269,7 +269,7 @@ func TestInstalled_Good(t *testing.T) {
assert.True(t, codes["mod-b"])
}
func TestInstalled_Good_Empty(t *testing.T) {
func TestInstalled_Good_Empty_Good(t *testing.T) {
st, err := store.New(":memory:")
require.NoError(t, err)
defer st.Close()
@ -308,7 +308,7 @@ func TestUpdate_Good(t *testing.T) {
assert.Equal(t, "Updated Module", installed[0].Name)
}
func TestUpdate_Bad_PathTraversalCode(t *testing.T) {
func TestUpdate_Bad_PathTraversalCode_Good(t *testing.T) {
modulesDir := filepath.Join(t.TempDir(), "modules")
st, err := store.New(":memory:")

View file

@ -60,7 +60,7 @@ func TestFind_Good(t *testing.T) {
assert.Equal(t, "XMRig", m.Name)
}
func TestFind_Bad_NotFound(t *testing.T) {
func TestFind_Bad_NotFound_Good(t *testing.T) {
idx := &Index{}
_, ok := idx.Find("nope")
assert.False(t, ok)

View file

@ -164,7 +164,7 @@ func TestScmProvider_GetMarketplaceItem_Bad(t *testing.T) {
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestScmProvider_GetMarketplaceItem_Bad_PathTraversal(t *testing.T) {
func TestScmProvider_GetMarketplaceItem_Bad_PathTraversal_Good(t *testing.T) {
idx := &marketplace.Index{Version: 1}
p := scmapi.NewProvider(idx, nil, nil, nil)

View file

@ -32,6 +32,7 @@ func NewInstaller(m io.Medium, registry *Registry) *Installer {
// Install downloads and installs a plugin from GitHub.
// The source format is "org/repo" or "org/repo@version".
// Usage: Install(...)
func (i *Installer) Install(ctx context.Context, source string) error {
org, repo, version, err := ParseSource(source)
if err != nil {
@ -96,6 +97,7 @@ func (i *Installer) Install(ctx context.Context, source string) error {
}
// Update updates a plugin to the latest version.
// Usage: Update(...)
func (i *Installer) Update(ctx context.Context, name string) error {
safeName, pluginDir, err := i.resolvePluginPath(name)
if err != nil {
@ -130,6 +132,7 @@ func (i *Installer) Update(ctx context.Context, name string) error {
}
// Remove uninstalls a plugin by removing its files and registry entry.
// Usage: Remove(...)
func (i *Installer) Remove(name string) error {
safeName, pluginDir, err := i.resolvePluginPath(name)
if err != nil {

View file

@ -25,7 +25,7 @@ func TestNewInstaller_Good(t *testing.T) {
// ── Install error paths ────────────────────────────────────────────
func TestInstall_Bad_InvalidSource(t *testing.T) {
func TestInstall_Bad_InvalidSource_Good(t *testing.T) {
m := io.NewMockMedium()
reg := NewRegistry(m, "/plugins")
inst := NewInstaller(m, reg)
@ -35,7 +35,7 @@ func TestInstall_Bad_InvalidSource(t *testing.T) {
assert.Contains(t, err.Error(), "invalid source")
}
func TestInstall_Bad_AlreadyInstalled(t *testing.T) {
func TestInstall_Bad_AlreadyInstalled_Good(t *testing.T) {
m := io.NewMockMedium()
reg := NewRegistry(m, "/plugins")
_ = reg.Add(&PluginConfig{Name: "my-plugin", Version: "1.0.0"})
@ -69,7 +69,7 @@ func TestRemove_Good(t *testing.T) {
assert.False(t, m.Exists("/plugins/removable"))
}
func TestRemove_Good_DirAlreadyGone(t *testing.T) {
func TestRemove_Good_DirAlreadyGone_Good(t *testing.T) {
m := io.NewMockMedium()
reg := NewRegistry(m, "/plugins")
_ = reg.Add(&PluginConfig{Name: "ghost", Version: "1.0.0"})
@ -83,7 +83,7 @@ func TestRemove_Good_DirAlreadyGone(t *testing.T) {
assert.False(t, ok)
}
func TestRemove_Bad_NotFound(t *testing.T) {
func TestRemove_Bad_NotFound_Good(t *testing.T) {
m := io.NewMockMedium()
reg := NewRegistry(m, "/plugins")
inst := NewInstaller(m, reg)
@ -95,7 +95,7 @@ func TestRemove_Bad_NotFound(t *testing.T) {
// ── Update error paths ─────────────────────────────────────────────
func TestUpdate_Bad_NotFound(t *testing.T) {
func TestUpdate_Bad_NotFound_Good(t *testing.T) {
m := io.NewMockMedium()
reg := NewRegistry(m, "/plugins")
inst := NewInstaller(m, reg)
@ -107,7 +107,7 @@ func TestUpdate_Bad_NotFound(t *testing.T) {
// ── ParseSource ────────────────────────────────────────────────────
func TestParseSource_Good_OrgRepo(t *testing.T) {
func TestParseSource_Good_OrgRepo_Good(t *testing.T) {
org, repo, version, err := ParseSource("host-uk/core-plugin")
assert.NoError(t, err)
assert.Equal(t, "host-uk", org)
@ -115,7 +115,7 @@ func TestParseSource_Good_OrgRepo(t *testing.T) {
assert.Equal(t, "", version)
}
func TestParseSource_Good_OrgRepoVersion(t *testing.T) {
func TestParseSource_Good_OrgRepoVersion_Good(t *testing.T) {
org, repo, version, err := ParseSource("host-uk/core-plugin@v1.0.0")
assert.NoError(t, err)
assert.Equal(t, "host-uk", org)
@ -123,7 +123,7 @@ func TestParseSource_Good_OrgRepoVersion(t *testing.T) {
assert.Equal(t, "v1.0.0", version)
}
func TestParseSource_Good_VersionWithoutPrefix(t *testing.T) {
func TestParseSource_Good_VersionWithoutPrefix_Good(t *testing.T) {
org, repo, version, err := ParseSource("org/repo@1.2.3")
assert.NoError(t, err)
assert.Equal(t, "org", org)
@ -131,37 +131,37 @@ func TestParseSource_Good_VersionWithoutPrefix(t *testing.T) {
assert.Equal(t, "1.2.3", version)
}
func TestParseSource_Bad_Empty(t *testing.T) {
func TestParseSource_Bad_Empty_Good(t *testing.T) {
_, _, _, err := ParseSource("")
assert.Error(t, err)
assert.Contains(t, err.Error(), "source is empty")
}
func TestParseSource_Bad_NoSlash(t *testing.T) {
func TestParseSource_Bad_NoSlash_Good(t *testing.T) {
_, _, _, err := ParseSource("just-a-name")
assert.Error(t, err)
assert.Contains(t, err.Error(), "org/repo")
}
func TestParseSource_Bad_TooManySlashes(t *testing.T) {
func TestParseSource_Bad_TooManySlashes_Good(t *testing.T) {
_, _, _, err := ParseSource("a/b/c")
assert.Error(t, err)
assert.Contains(t, err.Error(), "org/repo")
}
func TestParseSource_Bad_EmptyOrg(t *testing.T) {
func TestParseSource_Bad_EmptyOrg_Good(t *testing.T) {
_, _, _, err := ParseSource("/repo")
assert.Error(t, err)
assert.Contains(t, err.Error(), "org/repo")
}
func TestParseSource_Bad_EmptyRepo(t *testing.T) {
func TestParseSource_Bad_EmptyRepo_Good(t *testing.T) {
_, _, _, err := ParseSource("org/")
assert.Error(t, err)
assert.Contains(t, err.Error(), "org/repo")
}
func TestParseSource_Bad_EmptyVersion(t *testing.T) {
func TestParseSource_Bad_EmptyVersion_Good(t *testing.T) {
_, _, _, err := ParseSource("org/repo@")
assert.Error(t, err)
assert.Contains(t, err.Error(), "version is empty")

View file

@ -25,6 +25,7 @@ func NewLoader(m io.Medium, baseDir string) *Loader {
// Discover finds all plugin directories under baseDir and returns their manifests.
// Directories without a valid plugin.json are silently skipped.
// Usage: Discover(...)
func (l *Loader) Discover() ([]*Manifest, error) {
entries, err := l.medium.List(l.baseDir)
if err != nil {
@ -50,6 +51,7 @@ func (l *Loader) Discover() ([]*Manifest, error) {
}
// LoadPlugin loads a single plugin's manifest by name.
// Usage: LoadPlugin(...)
func (l *Loader) LoadPlugin(name string) (*Manifest, error) {
manifestPath := filepath.Join(l.baseDir, name, "plugin.json")
manifest, err := LoadManifest(l.medium, manifestPath)

View file

@ -45,7 +45,7 @@ func TestLoader_Discover_Good(t *testing.T) {
assert.True(t, names["plugin-b"])
}
func TestLoader_Discover_Good_SkipsInvalidPlugins(t *testing.T) {
func TestLoader_Discover_Good_SkipsInvalidPlugins_Good(t *testing.T) {
m := io.NewMockMedium()
baseDir := "/home/user/.core/plugins"
@ -70,7 +70,7 @@ func TestLoader_Discover_Good_SkipsInvalidPlugins(t *testing.T) {
assert.Equal(t, "good-plugin", manifests[0].Name)
}
func TestLoader_Discover_Good_SkipsFiles(t *testing.T) {
func TestLoader_Discover_Good_SkipsFiles_Good(t *testing.T) {
m := io.NewMockMedium()
baseDir := "/home/user/.core/plugins"
@ -91,7 +91,7 @@ func TestLoader_Discover_Good_SkipsFiles(t *testing.T) {
assert.Equal(t, "real-plugin", manifests[0].Name)
}
func TestLoader_Discover_Good_EmptyDirectory(t *testing.T) {
func TestLoader_Discover_Good_EmptyDirectory_Good(t *testing.T) {
m := io.NewMockMedium()
baseDir := "/home/user/.core/plugins"
m.Dirs[baseDir] = true
@ -122,7 +122,7 @@ func TestLoader_LoadPlugin_Good(t *testing.T) {
assert.Equal(t, "1.0.0", manifest.Version)
}
func TestLoader_LoadPlugin_Bad_NotFound(t *testing.T) {
func TestLoader_LoadPlugin_Bad_NotFound_Good(t *testing.T) {
m := io.NewMockMedium()
loader := NewLoader(m, "/home/user/.core/plugins")
@ -131,7 +131,7 @@ func TestLoader_LoadPlugin_Bad_NotFound(t *testing.T) {
assert.Contains(t, err.Error(), "failed to load plugin")
}
func TestLoader_LoadPlugin_Bad_InvalidManifest(t *testing.T) {
func TestLoader_LoadPlugin_Bad_InvalidManifest_Good(t *testing.T) {
m := io.NewMockMedium()
baseDir := "/home/user/.core/plugins"

View file

@ -38,6 +38,7 @@ func LoadManifest(m io.Medium, path string) (*Manifest, error) {
// Validate checks the manifest for required fields.
// Returns an error if name, version, or entrypoint are missing.
// Usage: Validate(...)
func (m *Manifest) Validate() error {
if m.Name == "" {
return coreerr.E("plugin.Manifest.Validate", "name is required", nil)

View file

@ -32,7 +32,7 @@ func TestLoadManifest_Good(t *testing.T) {
assert.Equal(t, "0.5.0", manifest.MinVersion)
}
func TestLoadManifest_Good_MinimalFields(t *testing.T) {
func TestLoadManifest_Good_MinimalFields_Good(t *testing.T) {
m := io.NewMockMedium()
m.Files["plugin.json"] = `{
"name": "minimal",
@ -49,7 +49,7 @@ func TestLoadManifest_Good_MinimalFields(t *testing.T) {
assert.Empty(t, manifest.MinVersion)
}
func TestLoadManifest_Bad_FileNotFound(t *testing.T) {
func TestLoadManifest_Bad_FileNotFound_Good(t *testing.T) {
m := io.NewMockMedium()
_, err := LoadManifest(m, "nonexistent/plugin.json")
@ -57,7 +57,7 @@ func TestLoadManifest_Bad_FileNotFound(t *testing.T) {
assert.Contains(t, err.Error(), "failed to read manifest")
}
func TestLoadManifest_Bad_InvalidJSON(t *testing.T) {
func TestLoadManifest_Bad_InvalidJSON_Good(t *testing.T) {
m := io.NewMockMedium()
m.Files["plugin.json"] = `{invalid json}`
@ -77,7 +77,7 @@ func TestManifest_Validate_Good(t *testing.T) {
assert.NoError(t, err)
}
func TestManifest_Validate_Bad_MissingName(t *testing.T) {
func TestManifest_Validate_Bad_MissingName_Good(t *testing.T) {
manifest := &Manifest{
Version: "1.0.0",
Entrypoint: "main.go",
@ -88,7 +88,7 @@ func TestManifest_Validate_Bad_MissingName(t *testing.T) {
assert.Contains(t, err.Error(), "name is required")
}
func TestManifest_Validate_Bad_MissingVersion(t *testing.T) {
func TestManifest_Validate_Bad_MissingVersion_Good(t *testing.T) {
manifest := &Manifest{
Name: "test-plugin",
Entrypoint: "main.go",
@ -99,7 +99,7 @@ func TestManifest_Validate_Bad_MissingVersion(t *testing.T) {
assert.Contains(t, err.Error(), "version is required")
}
func TestManifest_Validate_Bad_MissingEntrypoint(t *testing.T) {
func TestManifest_Validate_Bad_MissingEntrypoint_Good(t *testing.T) {
manifest := &Manifest{
Name: "test-plugin",
Version: "1.0.0",

View file

@ -41,16 +41,21 @@ type BasePlugin struct {
}
// Name returns the plugin name.
// Usage: Name(...)
func (p *BasePlugin) Name() string { return p.PluginName }
// Version returns the plugin version.
// Usage: Version(...)
func (p *BasePlugin) Version() string { return p.PluginVersion }
// Init is a no-op default implementation.
// Usage: Init(...)
func (p *BasePlugin) Init(_ context.Context) error { return nil }
// Start is a no-op default implementation.
// Usage: Start(...)
func (p *BasePlugin) Start(_ context.Context) error { return nil }
// Stop is a no-op default implementation.
// Usage: Stop(...)
func (p *BasePlugin) Stop(_ context.Context) error { return nil }

View file

@ -24,7 +24,7 @@ func TestBasePlugin_Good(t *testing.T) {
assert.NoError(t, p.Stop(ctx))
}
func TestBasePlugin_Good_EmptyFields(t *testing.T) {
func TestBasePlugin_Good_EmptyFields_Good(t *testing.T) {
p := &BasePlugin{}
assert.Equal(t, "", p.Name())
@ -36,6 +36,6 @@ func TestBasePlugin_Good_EmptyFields(t *testing.T) {
assert.NoError(t, p.Stop(ctx))
}
func TestBasePlugin_Good_ImplementsPlugin(t *testing.T) {
func TestBasePlugin_Good_ImplementsPlugin_Good(t *testing.T) {
var _ Plugin = &BasePlugin{}
}

View file

@ -32,6 +32,7 @@ func NewRegistry(m io.Medium, basePath string) *Registry {
}
// List returns all installed plugins sorted by name.
// Usage: List(...)
func (r *Registry) List() []*PluginConfig {
result := make([]*PluginConfig, 0, len(r.plugins))
for _, cfg := range r.plugins {
@ -45,12 +46,14 @@ func (r *Registry) List() []*PluginConfig {
// Get returns a plugin by name.
// The second return value indicates whether the plugin was found.
// Usage: Get(...)
func (r *Registry) Get(name string) (*PluginConfig, bool) {
cfg, ok := r.plugins[name]
return cfg, ok
}
// Add registers a plugin in the registry.
// Usage: Add(...)
func (r *Registry) Add(cfg *PluginConfig) error {
if cfg.Name == "" {
return coreerr.E("plugin.Registry.Add", "plugin name is required", nil)
@ -60,6 +63,7 @@ func (r *Registry) Add(cfg *PluginConfig) error {
}
// Remove unregisters a plugin from the registry.
// Usage: Remove(...)
func (r *Registry) Remove(name string) error {
if _, ok := r.plugins[name]; !ok {
return coreerr.E("plugin.Registry.Remove", "plugin not found: "+name, nil)
@ -75,6 +79,7 @@ func (r *Registry) registryPath() string {
// Load reads the plugin registry from disk.
// If the registry file does not exist, the registry starts empty.
// Usage: Load(...)
func (r *Registry) Load() error {
path := r.registryPath()
@ -102,6 +107,7 @@ func (r *Registry) Load() error {
}
// Save writes the plugin registry to disk.
// Usage: Save(...)
func (r *Registry) Save() error {
if err := r.medium.EnsureDir(r.basePath); err != nil {
return coreerr.E("plugin.Registry.Save", "failed to create plugin directory", err)

View file

@ -27,7 +27,7 @@ func TestRegistry_Add_Good(t *testing.T) {
assert.Equal(t, "1.0.0", list[0].Version)
}
func TestRegistry_Add_Bad_EmptyName(t *testing.T) {
func TestRegistry_Add_Bad_EmptyName_Good(t *testing.T) {
m := io.NewMockMedium()
reg := NewRegistry(m, "/home/user/.core/plugins")
@ -68,7 +68,7 @@ func TestRegistry_Get_Good(t *testing.T) {
assert.Equal(t, "2.0.0", cfg.Version)
}
func TestRegistry_Get_Bad_NotFound(t *testing.T) {
func TestRegistry_Get_Bad_NotFound_Good(t *testing.T) {
m := io.NewMockMedium()
reg := NewRegistry(m, "/home/user/.core/plugins")
@ -77,7 +77,7 @@ func TestRegistry_Get_Bad_NotFound(t *testing.T) {
assert.Nil(t, cfg)
}
func TestRegistry_Remove_Bad_NotFound(t *testing.T) {
func TestRegistry_Remove_Bad_NotFound_Good(t *testing.T) {
m := io.NewMockMedium()
reg := NewRegistry(m, "/home/user/.core/plugins")
@ -128,7 +128,7 @@ func TestRegistry_SaveLoad_Good(t *testing.T) {
assert.False(t, b.Enabled)
}
func TestRegistry_Load_Good_EmptyWhenNoFile(t *testing.T) {
func TestRegistry_Load_Good_EmptyWhenNoFile_Good(t *testing.T) {
m := io.NewMockMedium()
reg := NewRegistry(m, "/home/user/.core/plugins")
@ -137,7 +137,7 @@ func TestRegistry_Load_Good_EmptyWhenNoFile(t *testing.T) {
assert.Empty(t, reg.List())
}
func TestRegistry_Load_Bad_InvalidJSON(t *testing.T) {
func TestRegistry_Load_Bad_InvalidJSON_Good(t *testing.T) {
m := io.NewMockMedium()
basePath := "/home/user/.core/plugins"
_ = m.Write(basePath+"/registry.json", "not valid json {{{")
@ -148,7 +148,7 @@ func TestRegistry_Load_Bad_InvalidJSON(t *testing.T) {
assert.Contains(t, err.Error(), "failed to parse registry")
}
func TestRegistry_Load_Good_NullJSON(t *testing.T) {
func TestRegistry_Load_Good_NullJSON_Good(t *testing.T) {
m := io.NewMockMedium()
basePath := "/home/user/.core/plugins"
_ = m.Write(basePath+"/registry.json", "null")
@ -159,7 +159,7 @@ func TestRegistry_Load_Good_NullJSON(t *testing.T) {
assert.Empty(t, reg.List())
}
func TestRegistry_Save_Good_CreatesDir(t *testing.T) {
func TestRegistry_Save_Good_CreatesDir_Good(t *testing.T) {
m := io.NewMockMedium()
basePath := "/home/user/.core/plugins"
reg := NewRegistry(m, basePath)
@ -172,7 +172,7 @@ func TestRegistry_Save_Good_CreatesDir(t *testing.T) {
assert.True(t, m.IsFile(basePath+"/registry.json"))
}
func TestRegistry_List_Good_Sorted(t *testing.T) {
func TestRegistry_List_Good_Sorted_Good(t *testing.T) {
m := io.NewMockMedium()
reg := NewRegistry(m, "/plugins")

View file

@ -49,7 +49,7 @@ func TestGitState_LoadSave_Good(t *testing.T) {
assert.Equal(t, []string{"core-php", "core-tenant"}, loaded.Agents["cladius"].Active)
}
func TestGitState_Load_Good_NoFile(t *testing.T) {
func TestGitState_Load_Good_NoFile_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.EnsureDir("/workspace/.core")
@ -60,7 +60,7 @@ func TestGitState_Load_Good_NoFile(t *testing.T) {
assert.Empty(t, gs.Agents)
}
func TestGitState_Load_Bad_InvalidYAML(t *testing.T) {
func TestGitState_Load_Bad_InvalidYAML_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/workspace/.core/git.yaml", "{{{{not yaml")
@ -109,7 +109,7 @@ func TestGitState_UpdateRepo_Good(t *testing.T) {
assert.Equal(t, 1, r.Behind)
}
func TestGitState_UpdateRepo_Good_Overwrite(t *testing.T) {
func TestGitState_UpdateRepo_Good_Overwrite_Good(t *testing.T) {
gs := NewGitState()
gs.UpdateRepo("core-php", "main", "origin", 1, 0)
gs.UpdateRepo("core-php", "main", "origin", 0, 0)
@ -131,7 +131,7 @@ func TestGitState_Heartbeat_Good(t *testing.T) {
assert.True(t, agent.LastSeen.After(before) || agent.LastSeen.Equal(before))
}
func TestGitState_Heartbeat_Good_Updates(t *testing.T) {
func TestGitState_Heartbeat_Good_Updates_Good(t *testing.T) {
gs := NewGitState()
gs.Heartbeat("cladius", []string{"core-php"})
gs.Heartbeat("cladius", []string{"core-php", "core-tenant"})
@ -157,7 +157,7 @@ func TestGitState_StaleAgents_Good(t *testing.T) {
assert.NotContains(t, stale, "fresh")
}
func TestGitState_StaleAgents_Good_NoneStale(t *testing.T) {
func TestGitState_StaleAgents_Good_NoneStale_Good(t *testing.T) {
gs := NewGitState()
gs.Heartbeat("cladius", []string{"core-php"})
@ -177,7 +177,7 @@ func TestGitState_ActiveAgentsFor_Good(t *testing.T) {
assert.NotContains(t, agents, "athena")
}
func TestGitState_ActiveAgentsFor_Good_IgnoresStale(t *testing.T) {
func TestGitState_ActiveAgentsFor_Good_IgnoresStale_Good(t *testing.T) {
gs := NewGitState()
gs.Agents["gone"] = &AgentState{
LastSeen: time.Now().Add(-20 * time.Minute),
@ -188,7 +188,7 @@ func TestGitState_ActiveAgentsFor_Good_IgnoresStale(t *testing.T) {
assert.Empty(t, agents)
}
func TestGitState_ActiveAgentsFor_Good_NoMatch(t *testing.T) {
func TestGitState_ActiveAgentsFor_Good_NoMatch_Good(t *testing.T) {
gs := NewGitState()
gs.Heartbeat("cladius", []string{"core-php"})
@ -198,18 +198,18 @@ func TestGitState_ActiveAgentsFor_Good_NoMatch(t *testing.T) {
// ── NeedsPull ──────────────────────────────────────────────────────
func TestGitState_NeedsPull_Good_NeverPulled(t *testing.T) {
func TestGitState_NeedsPull_Good_NeverPulled_Good(t *testing.T) {
gs := NewGitState()
assert.True(t, gs.NeedsPull("core-php", 5*time.Minute))
}
func TestGitState_NeedsPull_Good_RecentPull(t *testing.T) {
func TestGitState_NeedsPull_Good_RecentPull_Good(t *testing.T) {
gs := NewGitState()
gs.TouchPull("core-php")
assert.False(t, gs.NeedsPull("core-php", 5*time.Minute))
}
func TestGitState_NeedsPull_Good_StalePull(t *testing.T) {
func TestGitState_NeedsPull_Good_StalePull_Good(t *testing.T) {
gs := NewGitState()
gs.Repos["core-php"] = &RepoGitState{
LastPull: time.Now().Add(-10 * time.Minute),

View file

@ -47,7 +47,7 @@ func TestKBConfig_LoadSave_Good(t *testing.T) {
assert.True(t, loaded.Wiki.Enabled)
}
func TestKBConfig_Load_Good_NoFile(t *testing.T) {
func TestKBConfig_Load_Good_NoFile_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.EnsureDir("/workspace/.core")
@ -56,7 +56,7 @@ func TestKBConfig_Load_Good_NoFile(t *testing.T) {
assert.Equal(t, DefaultKBConfig().Search.Collection, kb.Search.Collection)
}
func TestKBConfig_Load_Good_PartialOverride(t *testing.T) {
func TestKBConfig_Load_Good_PartialOverride_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/workspace/.core/kb.yaml", `
version: 1
@ -75,7 +75,7 @@ search:
assert.Equal(t, "embeddinggemma", kb.Search.EmbedModel)
}
func TestKBConfig_Load_Bad_InvalidYAML(t *testing.T) {
func TestKBConfig_Load_Bad_InvalidYAML_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/workspace/.core/kb.yaml", "{{{{broken")
@ -92,7 +92,7 @@ func TestKBConfig_WikiRepoURL_Good(t *testing.T) {
assert.Equal(t, "ssh://git@forge.lthn.ai:2223/core/go-scm.wiki.git", url)
}
func TestKBConfig_WikiRepoURL_Good_CustomRemote(t *testing.T) {
func TestKBConfig_WikiRepoURL_Good_CustomRemote_Good(t *testing.T) {
kb := &KBConfig{
Wiki: WikiConfig{Remote: "ssh://git@git.example.com/org"},
}

View file

@ -39,7 +39,7 @@ repos:
assert.Equal(t, reg, repo.registry)
}
func TestLoadRegistry_Good_WithDefaults(t *testing.T) {
func TestLoadRegistry_Good_WithDefaults_Good(t *testing.T) {
m := io.NewMockMedium()
yaml := `
version: 1
@ -71,7 +71,7 @@ repos:
assert.Equal(t, "github-actions", admin.CI)
}
func TestLoadRegistry_Good_CustomRepoPath(t *testing.T) {
func TestLoadRegistry_Good_CustomRepoPath_Good(t *testing.T) {
m := io.NewMockMedium()
yaml := `
version: 1
@ -92,7 +92,7 @@ repos:
assert.Equal(t, "/opt/special-repo", repo.Path)
}
func TestLoadRegistry_Good_CIOverride(t *testing.T) {
func TestLoadRegistry_Good_CIOverride_Good(t *testing.T) {
m := io.NewMockMedium()
yaml := `
version: 1
@ -119,14 +119,14 @@ repos:
assert.Equal(t, "custom-ci", b.CI)
}
func TestLoadRegistry_Bad_FileNotFound(t *testing.T) {
func TestLoadRegistry_Bad_FileNotFound_Good(t *testing.T) {
m := io.NewMockMedium()
_, err := LoadRegistry(m, "/nonexistent/repos.yaml")
assert.Error(t, err)
assert.Contains(t, err.Error(), "failed to read")
}
func TestLoadRegistry_Bad_InvalidYAML(t *testing.T) {
func TestLoadRegistry_Bad_InvalidYAML_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/tmp/bad.yaml", "{{{{not yaml at all")
@ -180,7 +180,7 @@ func TestRegistry_Get_Good(t *testing.T) {
assert.Equal(t, "core-php", repo.Name)
}
func TestRegistry_Get_Bad_NotFound(t *testing.T) {
func TestRegistry_Get_Bad_NotFound_Good(t *testing.T) {
reg := newTestRegistry(t)
_, ok := reg.Get("nonexistent")
assert.False(t, ok)
@ -200,7 +200,7 @@ func TestRegistry_ByType_Good(t *testing.T) {
assert.Len(t, products, 1)
}
func TestRegistry_ByType_Good_NoMatch(t *testing.T) {
func TestRegistry_ByType_Good_NoMatch_Good(t *testing.T) {
reg := newTestRegistry(t)
templates := reg.ByType("template")
assert.Empty(t, templates)
@ -242,7 +242,7 @@ func TopologicalOrder(reg *Registry) ([]*Repo, error) {
return reg.TopologicalOrder()
}
func TestTopologicalOrder_Bad_CircularDep(t *testing.T) {
func TestTopologicalOrder_Bad_CircularDep_Good(t *testing.T) {
m := io.NewMockMedium()
yaml := `
version: 1
@ -265,7 +265,7 @@ repos:
assert.Contains(t, err.Error(), "circular dependency")
}
func TestTopologicalOrder_Bad_UnknownDep(t *testing.T) {
func TestTopologicalOrder_Bad_UnknownDep_Good(t *testing.T) {
m := io.NewMockMedium()
yaml := `
version: 1
@ -285,7 +285,7 @@ repos:
assert.Contains(t, err.Error(), "unknown repo")
}
func TestTopologicalOrder_Good_NoDeps(t *testing.T) {
func TestTopologicalOrder_Good_NoDeps_Good(t *testing.T) {
m := io.NewMockMedium()
yaml := `
version: 1
@ -333,7 +333,7 @@ func TestScanDirectory_Good(t *testing.T) {
assert.False(t, ok)
}
func TestScanDirectory_Good_DetectsGitHubOrg(t *testing.T) {
func TestScanDirectory_Good_DetectsGitHubOrg_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.EnsureDir("/workspace/my-repo/.git")
@ -349,7 +349,7 @@ func TestScanDirectory_Good_DetectsGitHubOrg(t *testing.T) {
assert.Equal(t, "host-uk", reg.Org)
}
func TestScanDirectory_Good_DetectsHTTPSOrg(t *testing.T) {
func TestScanDirectory_Good_DetectsHTTPSOrg_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.EnsureDir("/workspace/my-repo/.git")
@ -362,7 +362,7 @@ func TestScanDirectory_Good_DetectsHTTPSOrg(t *testing.T) {
assert.Equal(t, "lethean-io", reg.Org)
}
func TestScanDirectory_Good_EmptyDir(t *testing.T) {
func TestScanDirectory_Good_EmptyDir_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.EnsureDir("/empty")
@ -372,7 +372,7 @@ func TestScanDirectory_Good_EmptyDir(t *testing.T) {
assert.Equal(t, "", reg.Org)
}
func TestScanDirectory_Bad_InvalidDir(t *testing.T) {
func TestScanDirectory_Bad_InvalidDir_Good(t *testing.T) {
m := io.NewMockMedium()
_, err := ScanDirectory(m, "/nonexistent")
assert.Error(t, err)
@ -381,7 +381,7 @@ func TestScanDirectory_Bad_InvalidDir(t *testing.T) {
// ── detectOrg ──────────────────────────────────────────────────────
func TestDetectOrg_Good_SSHRemote(t *testing.T) {
func TestDetectOrg_Good_SSHRemote_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/repo/.git/config", `[remote "origin"]
url = git@github.com:host-uk/core.git
@ -389,7 +389,7 @@ func TestDetectOrg_Good_SSHRemote(t *testing.T) {
assert.Equal(t, "host-uk", detectOrg(m, "/repo"))
}
func TestDetectOrg_Good_HTTPSRemote(t *testing.T) {
func TestDetectOrg_Good_HTTPSRemote_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/repo/.git/config", `[remote "origin"]
url = https://github.com/snider/project.git
@ -397,12 +397,12 @@ func TestDetectOrg_Good_HTTPSRemote(t *testing.T) {
assert.Equal(t, "snider", detectOrg(m, "/repo"))
}
func TestDetectOrg_Bad_NoConfig(t *testing.T) {
func TestDetectOrg_Bad_NoConfig_Good(t *testing.T) {
m := io.NewMockMedium()
assert.Equal(t, "", detectOrg(m, "/nonexistent"))
}
func TestDetectOrg_Bad_NoRemote(t *testing.T) {
func TestDetectOrg_Bad_NoRemote_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/repo/.git/config", `[core]
repositoryformatversion = 0
@ -410,7 +410,7 @@ func TestDetectOrg_Bad_NoRemote(t *testing.T) {
assert.Equal(t, "", detectOrg(m, "/repo"))
}
func TestDetectOrg_Bad_NonGitHubRemote(t *testing.T) {
func TestDetectOrg_Bad_NonGitHubRemote_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/repo/.git/config", `[remote "origin"]
url = ssh://git@forge.lthn.ai:2223/core/go.git
@ -420,13 +420,13 @@ func TestDetectOrg_Bad_NonGitHubRemote(t *testing.T) {
// ── expandPath ─────────────────────────────────────────────────────
func TestExpandPath_Good_Tilde(t *testing.T) {
func TestExpandPath_Good_Tilde_Good(t *testing.T) {
got := expandPath("~/Code/repos")
assert.NotContains(t, got, "~")
assert.Contains(t, got, "Code/repos")
}
func TestExpandPath_Good_NoTilde(t *testing.T) {
func TestExpandPath_Good_NoTilde_Good(t *testing.T) {
assert.Equal(t, "/absolute/path", expandPath("/absolute/path"))
assert.Equal(t, "relative/path", expandPath("relative/path"))
}
@ -473,14 +473,14 @@ func TestRepo_IsGitRepo_Good(t *testing.T) {
// ── getMedium fallback ─────────────────────────────────────────────
func TestGetMedium_Good_FallbackToLocal(t *testing.T) {
func TestGetMedium_Good_FallbackToLocal_Good(t *testing.T) {
repo := &Repo{Name: "orphan", Path: "/tmp/orphan"}
// No registry set — should fall back to io.Local.
m := repo.getMedium()
assert.Equal(t, io.Local, m)
}
func TestGetMedium_Good_NilMediumFallback(t *testing.T) {
func TestGetMedium_Good_NilMediumFallback_Good(t *testing.T) {
reg := &Registry{} // medium is nil.
repo := &Repo{Name: "test", registry: reg}
m := repo.getMedium()

View file

@ -50,7 +50,7 @@ func TestWorkConfig_LoadSave_Good(t *testing.T) {
assert.True(t, loaded.Sync.AutoPull)
}
func TestWorkConfig_Load_Good_NoFile(t *testing.T) {
func TestWorkConfig_Load_Good_NoFile_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.EnsureDir("/workspace/.core")
@ -59,7 +59,7 @@ func TestWorkConfig_Load_Good_NoFile(t *testing.T) {
assert.Equal(t, DefaultWorkConfig().Sync.Interval, wc.Sync.Interval)
}
func TestWorkConfig_Load_Good_PartialOverride(t *testing.T) {
func TestWorkConfig_Load_Good_PartialOverride_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/workspace/.core/work.yaml", `
version: 1
@ -78,7 +78,7 @@ sync:
assert.True(t, wc.Sync.CloneMissing)
}
func TestWorkConfig_Load_Bad_InvalidYAML(t *testing.T) {
func TestWorkConfig_Load_Bad_InvalidYAML_Good(t *testing.T) {
m := io.NewMockMedium()
_ = m.Write("/workspace/.core/work.yaml", "{{{{broken")
@ -96,12 +96,12 @@ func TestWorkConfig_HasTrigger_Good(t *testing.T) {
assert.True(t, wc.HasTrigger("scheduled"))
}
func TestWorkConfig_HasTrigger_Bad_NotFound(t *testing.T) {
func TestWorkConfig_HasTrigger_Bad_NotFound_Good(t *testing.T) {
wc := DefaultWorkConfig()
assert.False(t, wc.HasTrigger("on_deploy"))
}
func TestWorkConfig_HasTrigger_Good_CustomTriggers(t *testing.T) {
func TestWorkConfig_HasTrigger_Good_CustomTriggers_Good(t *testing.T) {
wc := &WorkConfig{
Version: 1,
Triggers: []string{"on_pr", "manual"},