agent/pkg/agentic/qa_analysis_test.go
Snider 5c942a8928 feat(agent/qa): post-run Poindexter workspace analysis per RFC §7 (#538)
Per RFC §7 Post-Run Analysis: analyseWorkspace() builds 5D Poindexter
points (tool_id, severity_score, file_hash, category_id, frequency),
clusters by distance 0.15, diffs against previous journal entries to
classify New / Resolved / Persistent (≥5 consecutive cycles).

Lands:
* pkg/agentic/qa_analysis.go — analyseWorkspace, DispatchReport,
  findingToPoint, diffFindings, persistentFindings; integrates with
  forge.lthn.ai/Snider/Poindexter (canonical path per memory)
* pkg/agentic/qa.go — wires analysis into runQAWithReport before
  ws.Commit() (sync.go untouched — ws.Commit lives in runQAWithReport
  in this branch)
* journal publication extended so summary text + analysis fields travel
  with the report
* qa_analysis_test.go — TestAnalyseWorkspace_{Good_EmptyFindings,
  Good_FiveClusters,Bad_NilWorkspace,Ugly_PoindexterPanic}; the panic
  test uses a panic-injecting clusterer override and asserts graceful
  recovery
* go.mod — adds forge.lthn.ai/Snider/Poindexter (canonical, NOT
  dappco.re — Poindexter is OG load-bearing math primitive)

Sandbox go test blocked by pre-existing unrelated issues in
commands_forge.go / fetch_loop.go / commands_flow_test.go (out of
allowlist); supervisor catches in clean workspace.

Co-authored-by: Codex <noreply@openai.com>
Closes tasks.lthn.sh/view.php?id=538
2026-04-25 23:27:30 +01:00

137 lines
4.6 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"testing"
"time"
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAnalyseWorkspace_Good_EmptyFindings(t *testing.T) {
root := t.TempDir()
setTestWorkspace(t, root)
subsystem := newPrepWithProcess()
t.Cleanup(subsystem.closeStateStore)
workspaceDir := core.JoinPath(WorkspaceRoot(), "core", "go-io", "task-empty")
workspaceName := WorkspaceName(workspaceDir)
workspace, err := subsystem.stateStoreInstance().NewWorkspace(qaWorkspaceName(workspaceDir))
require.NoError(t, err)
t.Cleanup(workspace.Discard)
report := subsystem.analyseWorkspaceNamed(workspace, workspaceName)
assert.Equal(t, workspaceName, report.Workspace)
assert.Empty(t, report.Findings)
assert.Empty(t, report.Clusters)
assert.Empty(t, report.New)
assert.Empty(t, report.Resolved)
assert.Empty(t, report.Persistent)
assert.Equal(t, 0, report.Summary["clusters"])
assert.Equal(t, "0 findings across 0 clusters; 0 new, 0 resolved, 0 persistent", report.SummaryText)
}
func TestAnalyseWorkspace_Good_FiveClusters(t *testing.T) {
root := t.TempDir()
setTestWorkspace(t, root)
subsystem := newPrepWithProcess()
t.Cleanup(subsystem.closeStateStore)
workspaceDir := core.JoinPath(WorkspaceRoot(), "core", "go-io", "task-five")
workspaceName := WorkspaceName(workspaceDir)
workspace, err := subsystem.stateStoreInstance().NewWorkspace(qaWorkspaceName(workspaceDir))
require.NoError(t, err)
t.Cleanup(workspace.Discard)
repeated := QAFinding{Tool: "gosec", Severity: "error", Category: "security-secret", Code: "G101", File: "secret.go", Line: 10, Message: "hardcoded secret"}
for cycle := 0; cycle < persistentThreshold-1; cycle++ {
publishDispatchReport(subsystem.stateStoreInstance(), workspaceName, DispatchReport{
Workspace: workspaceName,
Findings: []QAFinding{repeated},
GeneratedAt: time.Now().UTC(),
})
}
currentFindings := []QAFinding{
repeated,
{Tool: "gosec", Severity: "error", Category: "security-path", Code: "G304", File: "path.go", Line: 20, Message: "tainted path"},
{Tool: "staticcheck", Severity: "warning", Category: "correctness-regexp", Code: "SA1000", File: "regexp.go", Line: 30, Message: "invalid regexp"},
{Tool: "govet", Severity: "warning", Category: "printf", Code: "printf", File: "printf.go", Line: 40, Message: "printf mismatch"},
{Tool: "revive", Severity: "info", Category: "var-naming", Code: "var-naming", File: "style.go", Line: 50, Message: "bad variable name"},
}
for _, finding := range currentFindings {
require.NoError(t, workspace.Put("finding", findingToMap(finding)))
}
report := subsystem.analyseWorkspaceNamed(workspace, workspaceName)
if assert.Len(t, report.Clusters, 5) {
for _, cluster := range report.Clusters {
assert.Equal(t, 1, cluster.Count)
}
}
assert.Len(t, report.New, 4)
assert.Empty(t, report.Resolved)
assert.Len(t, report.Persistent, 1)
assert.Equal(t, 5, report.Summary["clusters"])
assert.Equal(t, 1, report.Summary["persistent"])
}
func TestAnalyseWorkspace_Bad_NilWorkspace(t *testing.T) {
var subsystem *PrepSubsystem
assert.NotPanics(t, func() {
report := subsystem.analyseWorkspace(nil)
assert.Empty(t, report.Workspace)
assert.Empty(t, report.Findings)
assert.Empty(t, report.Clusters)
assert.Empty(t, report.New)
assert.Empty(t, report.Resolved)
assert.Empty(t, report.Persistent)
assert.Equal(t, "0 findings across 0 clusters; 0 new, 0 resolved, 0 persistent", report.SummaryText)
})
}
func TestAnalyseWorkspace_Ugly_PoindexterPanic(t *testing.T) {
root := t.TempDir()
setTestWorkspace(t, root)
subsystem := newPrepWithProcess()
t.Cleanup(subsystem.closeStateStore)
workspaceDir := core.JoinPath(WorkspaceRoot(), "core", "go-io", "task-panic")
workspaceName := WorkspaceName(workspaceDir)
workspace, err := subsystem.stateStoreInstance().NewWorkspace(qaWorkspaceName(workspaceDir))
require.NoError(t, err)
t.Cleanup(workspace.Discard)
require.NoError(t, workspace.Put("finding", findingToMap(QAFinding{
Tool: "gosec",
Severity: "error",
Category: "security-secret",
Code: "G101",
File: "panic.go",
Line: 10,
Message: "hardcoded secret",
})))
previousClusterer := qaAnalysisClusterer
qaAnalysisClusterer = func([]QAFinding) []DispatchCluster {
panic("poindexter panic")
}
t.Cleanup(func() { qaAnalysisClusterer = previousClusterer })
assert.NotPanics(t, func() {
report := subsystem.analyseWorkspaceNamed(workspace, workspaceName)
if assert.Len(t, report.Clusters, 1) {
assert.Equal(t, 1, report.Clusters[0].Count)
}
assert.Equal(t, 1, report.Summary["clusters"])
})
}