summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaudomator Agent <agent@claudomator.local>2026-03-26 09:28:17 +0000
committerClaudomator Agent <agent@claudomator.local>2026-03-26 09:28:17 +0000
commitf66fde80bf5759faa75d3c294e99abbb75dd2cdf (patch)
tree8e25d577d1916bdf53f052c36e6c49ba2956b199
parenta13ec6aa94550bce5caaee6bc01e690cabb5d4dc (diff)
update createValidationTask to list named acceptance criteria in instructionsstory/acceptance-criteria
Replace raw ValidationJSON in validation task instructions with a formatted per-criterion checklist when acceptance_criteria entries are present. Falls back to the raw JSON blob when AcceptanceCriteria is empty. Adds TestCreateValidationTask_InstructionsIncludeNamedCriteria. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-rw-r--r--internal/executor/executor.go26
-rw-r--r--internal/executor/executor_test.go61
2 files changed, 85 insertions, 2 deletions
diff --git a/internal/executor/executor.go b/internal/executor/executor.go
index 6aef736..6ffedc7 100644
--- a/internal/executor/executor.go
+++ b/internal/executor/executor.go
@@ -565,13 +565,35 @@ func (p *Pool) createValidationTask(ctx context.Context, storyID string) {
return
}
- var spec map[string]interface{}
+ type validationSpec struct {
+ Type string `json:"type"`
+ AcceptanceCriteria []struct {
+ Name string `json:"name"`
+ Verification string `json:"verification"`
+ TestRef string `json:"test_ref,omitempty"`
+ } `json:"acceptance_criteria"`
+ }
+ var spec validationSpec
if err := json.Unmarshal([]byte(story.ValidationJSON), &spec); err != nil {
p.logger.Error("createValidationTask: failed to parse ValidationJSON", "storyID", storyID, "error", err)
return
}
- instructions := fmt.Sprintf("Validate the deployment for story %q.\n\nValidation spec:\n%s", story.Name, story.ValidationJSON)
+ var instructions string
+ if len(spec.AcceptanceCriteria) > 0 {
+ var sb strings.Builder
+ fmt.Fprintf(&sb, "Validate the deployment for story %q.\n\nFor each acceptance criterion, verify it and report PASS or FAIL:\n", story.Name)
+ for _, c := range spec.AcceptanceCriteria {
+ if c.TestRef != "" {
+ fmt.Fprintf(&sb, "- %s: %s [test: %s]\n", c.Name, c.Verification, c.TestRef)
+ } else {
+ fmt.Fprintf(&sb, "- %s: %s\n", c.Name, c.Verification)
+ }
+ }
+ instructions = sb.String()
+ } else {
+ instructions = fmt.Sprintf("Validate the deployment for story %q.\n\nValidation spec:\n%s", story.Name, story.ValidationJSON)
+ }
now := time.Now().UTC()
vtask := &task.Task{
diff --git a/internal/executor/executor_test.go b/internal/executor/executor_test.go
index ad496e7..933f303 100644
--- a/internal/executor/executor_test.go
+++ b/internal/executor/executor_test.go
@@ -2027,6 +2027,67 @@ func TestPool_DependsOn_NoDeadlock(t *testing.T) {
}
}
+func TestCreateValidationTask_InstructionsIncludeNamedCriteria(t *testing.T) {
+ store := testStore(t)
+ runner := &mockRunner{}
+ runners := map[string]Runner{"claude": runner}
+ logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
+ pool := NewPool(2, runners, store, logger)
+
+ now := time.Now().UTC()
+ story := &task.Story{
+ ID: "story-named-criteria-1",
+ Name: "Named Criteria Story",
+ Status: task.StoryDeployed,
+ CreatedAt: now,
+ UpdatedAt: now,
+ ValidationJSON: `{
+ "type": "automated",
+ "acceptance_criteria": [
+ {"name": "API returns 200", "verification": "GET /health returns HTTP 200", "test_ref": "TestHealthEndpoint"},
+ {"name": "DB migration applied", "verification": "table users exists", "test_ref": ""},
+ {"name": "Auth header required", "verification": "requests without token get 401"}
+ ]
+ }`,
+ }
+ if err := store.CreateStory(story); err != nil {
+ t.Fatalf("CreateStory: %v", err)
+ }
+
+ pool.createValidationTask(context.Background(), story.ID)
+
+ // Drain the submitted task from results.
+ select {
+ case <-pool.Results():
+ case <-time.After(5 * time.Second):
+ t.Fatal("timed out waiting for validation task result")
+ }
+
+ tasks, err := store.ListTasksByStory(story.ID)
+ if err != nil {
+ t.Fatalf("ListTasksByStory: %v", err)
+ }
+ if len(tasks) != 1 {
+ t.Fatalf("expected 1 validation task, got %d", len(tasks))
+ }
+
+ instructions := tasks[0].Agent.Instructions
+ // Each criterion name must appear as a formatted list item (not merely in raw JSON).
+ for _, line := range []string{
+ "- API returns 200:",
+ "- DB migration applied:",
+ "- Auth header required:",
+ } {
+ if !strings.Contains(instructions, line) {
+ t.Errorf("instructions missing formatted criterion line %q\n\nfull instructions:\n%s", line, instructions)
+ }
+ }
+ // test_ref should appear for criteria that have one.
+ if !strings.Contains(instructions, "TestHealthEndpoint") {
+ t.Errorf("instructions should include test_ref 'TestHealthEndpoint'\n\nfull instructions:\n%s", instructions)
+ }
+}
+
func TestPool_ValidationTask_Fail_SetsNeedsFix(t *testing.T) {
store := testStore(t)
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))