269 lines
7.5 KiB
Go
269 lines
7.5 KiB
Go
//go:build llm_script
|
||
|
||
package main
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
func TestParseTasksParsesEvidenceFields(t *testing.T) {
|
||
md := `# Tasks
|
||
|
||
### T-1 ✅ Example
|
||
- **verification**:
|
||
- mode: ` + "`test_pass`" + `
|
||
- command: ` + "`echo ok`" + `
|
||
- expected_evidence: ` + "`ok`" + `
|
||
- evidence_grade: ` + "`runtime-verified`" + `
|
||
- task_type: ` + "`code`" + `
|
||
- timeout_seconds: 15
|
||
`
|
||
|
||
tmpFile, err := os.CreateTemp(t.TempDir(), "tasks-*.md")
|
||
if err != nil {
|
||
t.Fatalf("create temp file: %v", err)
|
||
}
|
||
defer tmpFile.Close()
|
||
|
||
if _, err := tmpFile.WriteString(md); err != nil {
|
||
t.Fatalf("write temp file: %v", err)
|
||
}
|
||
if _, err := tmpFile.Seek(0, 0); err != nil {
|
||
t.Fatalf("seek temp file: %v", err)
|
||
}
|
||
|
||
tasks := parseTasks(tmpFile)
|
||
if len(tasks) != 1 {
|
||
t.Fatalf("expected 1 task, got %d", len(tasks))
|
||
}
|
||
|
||
got := tasks[0].Verification
|
||
if got.Mode != "test_pass" {
|
||
t.Fatalf("expected mode test_pass, got %q", got.Mode)
|
||
}
|
||
if got.Command != "echo ok" {
|
||
t.Fatalf("expected command echo ok, got %q", got.Command)
|
||
}
|
||
if got.ExpectedEvidence != "ok" {
|
||
t.Fatalf("expected evidence ok, got %q", got.ExpectedEvidence)
|
||
}
|
||
if got.EvidenceGrade != "runtime-verified" {
|
||
t.Fatalf("expected evidence grade runtime-verified, got %q", got.EvidenceGrade)
|
||
}
|
||
if got.TaskType != "code" {
|
||
t.Fatalf("expected task type code, got %q", got.TaskType)
|
||
}
|
||
if got.TimeoutSeconds != 15 {
|
||
t.Fatalf("expected timeout 15, got %d", got.TimeoutSeconds)
|
||
}
|
||
}
|
||
|
||
func TestVerifyTaskRejectsSemanticOnlyForCodeTask(t *testing.T) {
|
||
task := taskEntry{
|
||
ID: "T-1",
|
||
Name: "semantic code task",
|
||
Verification: Verification{
|
||
Mode: "semantic",
|
||
Command: "echo ok",
|
||
TaskType: "code",
|
||
EvidenceGrade: "doc-claimed",
|
||
},
|
||
HasVerification: true,
|
||
}
|
||
|
||
result := verifyTask(task, true)
|
||
if result.Verified {
|
||
t.Fatalf("expected semantic-only code task to fail")
|
||
}
|
||
if !strings.Contains(result.Reason, "semantic-only") {
|
||
t.Fatalf("expected semantic-only rejection reason, got %q", result.Reason)
|
||
}
|
||
}
|
||
|
||
func TestVerifyTaskDefaultsEvidenceGradeFromMode(t *testing.T) {
|
||
task := taskEntry{
|
||
ID: "T-2",
|
||
Name: "artifact task",
|
||
Verification: Verification{
|
||
Mode: "artifact_present",
|
||
Command: "echo exists",
|
||
ExpectedEvidence: "exists",
|
||
},
|
||
HasVerification: true,
|
||
}
|
||
|
||
result := verifyTask(task, true)
|
||
if !result.Verified {
|
||
t.Fatalf("expected dry-run artifact task to pass, got reason %q", result.Reason)
|
||
}
|
||
if result.EvidenceGrade != "artifact-present" {
|
||
t.Fatalf("expected default evidence grade artifact-present, got %q", result.EvidenceGrade)
|
||
}
|
||
}
|
||
|
||
func TestResolveTasksPathDoesNotImplicitlyFallbackToGlobal(t *testing.T) {
|
||
root := t.TempDir()
|
||
projectDir := filepath.Join(root, "project")
|
||
globalDir := filepath.Join(root, "workspace")
|
||
scriptDir := filepath.Join(projectDir, "scripts")
|
||
if err := os.MkdirAll(projectDir, 0o755); err != nil {
|
||
t.Fatalf("mkdir project dir: %v", err)
|
||
}
|
||
if err := os.MkdirAll(globalDir, 0o755); err != nil {
|
||
t.Fatalf("mkdir global dir: %v", err)
|
||
}
|
||
if err := os.MkdirAll(scriptDir, 0o755); err != nil {
|
||
t.Fatalf("mkdir script dir: %v", err)
|
||
}
|
||
|
||
projectTasks := filepath.Join(projectDir, "TASKS.md")
|
||
globalTasks := filepath.Join(globalDir, "TASKS.md")
|
||
if err := os.WriteFile(projectTasks, []byte("# project"), 0o644); err != nil {
|
||
t.Fatalf("write project tasks: %v", err)
|
||
}
|
||
if err := os.WriteFile(globalTasks, []byte("# global"), 0o644); err != nil {
|
||
t.Fatalf("write global tasks: %v", err)
|
||
}
|
||
|
||
got := resolveTasksPathWithContext("", "", filepath.Join(root, "outside"), scriptDir, globalTasks)
|
||
if got != projectTasks {
|
||
t.Fatalf("expected project tasks path, got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestResolveTasksPathAllowsExplicitGlobalPath(t *testing.T) {
|
||
root := t.TempDir()
|
||
projectDir := filepath.Join(root, "project")
|
||
globalDir := filepath.Join(root, "workspace")
|
||
scriptDir := filepath.Join(projectDir, "scripts")
|
||
if err := os.MkdirAll(projectDir, 0o755); err != nil {
|
||
t.Fatalf("mkdir project dir: %v", err)
|
||
}
|
||
if err := os.MkdirAll(globalDir, 0o755); err != nil {
|
||
t.Fatalf("mkdir global dir: %v", err)
|
||
}
|
||
if err := os.MkdirAll(scriptDir, 0o755); err != nil {
|
||
t.Fatalf("mkdir script dir: %v", err)
|
||
}
|
||
|
||
projectTasks := filepath.Join(projectDir, "TASKS.md")
|
||
globalTasks := filepath.Join(globalDir, "TASKS.md")
|
||
if err := os.WriteFile(projectTasks, []byte("# project"), 0o644); err != nil {
|
||
t.Fatalf("write project tasks: %v", err)
|
||
}
|
||
if err := os.WriteFile(globalTasks, []byte("# global"), 0o644); err != nil {
|
||
t.Fatalf("write global tasks: %v", err)
|
||
}
|
||
|
||
got := resolveTasksPathWithContext(globalTasks, "", filepath.Join(root, "outside"), scriptDir, globalTasks)
|
||
if got != globalTasks {
|
||
t.Fatalf("expected explicit global tasks path, got %q", got)
|
||
}
|
||
}
|
||
|
||
func TestVerifyTaskCapturesFailureSummaries(t *testing.T) {
|
||
task := taskEntry{
|
||
ID: "T-3",
|
||
Name: "failing task",
|
||
Verification: Verification{
|
||
Mode: "test_pass",
|
||
Command: "echo standard-output && echo standard-error 1>&2 && exit 1",
|
||
ExpectedEvidence: "unused",
|
||
TaskType: "automation",
|
||
},
|
||
HasVerification: true,
|
||
}
|
||
|
||
result := verifyTask(task, false)
|
||
if result.Verified {
|
||
t.Fatalf("expected failing task to fail")
|
||
}
|
||
if !strings.Contains(result.StdoutSummary, "standard-output") {
|
||
t.Fatalf("expected stdout summary to contain command output, got %q", result.StdoutSummary)
|
||
}
|
||
if !strings.Contains(result.StderrSummary, "standard-error") {
|
||
t.Fatalf("expected stderr summary to contain command error, got %q", result.StderrSummary)
|
||
}
|
||
}
|
||
|
||
func TestParseTasksParsesNormalizedStatus(t *testing.T) {
|
||
md := `# Tasks
|
||
|
||
### T-1 ✅ Done task
|
||
- **状态**:✅ 完成(2026-05-11)
|
||
- **verification**:
|
||
- mode: ` + "`test_pass`" + `
|
||
- command: ` + "`echo ok`" + `
|
||
- expected_evidence: ` + "`ok`" + `
|
||
|
||
### T-2 🔶 Planned task
|
||
- **状态**:🔶 待启动
|
||
- **verification**:
|
||
- mode: ` + "`test_pass`" + `
|
||
- command: ` + "`echo ok`" + `
|
||
- expected_evidence: ` + "`ok`" + `
|
||
|
||
### T-3 ⏸️ Paused task
|
||
- **状态**:⏸️ 待规划
|
||
- **verification**:
|
||
- mode: ` + "`test_pass`" + `
|
||
- command: ` + "`echo ok`" + `
|
||
- expected_evidence: ` + "`ok`" + `
|
||
`
|
||
|
||
tmpFile, err := os.CreateTemp(t.TempDir(), "tasks-status-*.md")
|
||
if err != nil {
|
||
t.Fatalf("create temp file: %v", err)
|
||
}
|
||
defer tmpFile.Close()
|
||
|
||
if _, err := tmpFile.WriteString(md); err != nil {
|
||
t.Fatalf("write temp file: %v", err)
|
||
}
|
||
if _, err := tmpFile.Seek(0, 0); err != nil {
|
||
t.Fatalf("seek temp file: %v", err)
|
||
}
|
||
|
||
tasks := parseTasks(tmpFile)
|
||
if len(tasks) != 3 {
|
||
t.Fatalf("expected 3 tasks, got %d", len(tasks))
|
||
}
|
||
|
||
if tasks[0].Status != "completed" {
|
||
t.Fatalf("expected first task status completed, got %q", tasks[0].Status)
|
||
}
|
||
if tasks[1].Status != "planned" {
|
||
t.Fatalf("expected second task status planned, got %q", tasks[1].Status)
|
||
}
|
||
if tasks[2].Status != "paused" {
|
||
t.Fatalf("expected third task status paused, got %q", tasks[2].Status)
|
||
}
|
||
}
|
||
|
||
func TestFilterTasksByStatus(t *testing.T) {
|
||
tasks := []taskEntry{
|
||
{ID: "T-1", Status: "completed"},
|
||
{ID: "T-2", Status: "planned"},
|
||
{ID: "T-3", Status: "in_progress"},
|
||
}
|
||
|
||
completed, err := filterTasksByStatus(tasks, "completed")
|
||
if err != nil {
|
||
t.Fatalf("unexpected error: %v", err)
|
||
}
|
||
if len(completed) != 1 || completed[0].ID != "T-1" {
|
||
t.Fatalf("expected only completed task, got %#v", completed)
|
||
}
|
||
|
||
all, err := filterTasksByStatus(tasks, "all")
|
||
if err != nil {
|
||
t.Fatalf("unexpected error for all: %v", err)
|
||
}
|
||
if len(all) != 3 {
|
||
t.Fatalf("expected all 3 tasks, got %d", len(all))
|
||
}
|
||
}
|