단일 요청의 계획·작업·리뷰·수리 단계가 정형화된 산출물을 다음 단계로 전달하고, 선택적 selfcheck로 불완전한 dispatcher 결과를 보완하기 위해 반영한다.
1263 lines
28 KiB
Go
1263 lines
28 KiB
Go
package singlerequesttemplate_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"iop/packages/go/singlerequesttemplate"
|
|
)
|
|
|
|
// planTemplateOfSize pads the built-in Plan template with trailing static text
|
|
// so the returned template is exactly size bytes long.
|
|
func planTemplateOfSize(size int) string {
|
|
return singlerequesttemplate.DefaultPlanTemplate + strings.Repeat(" ", size-len(singlerequesttemplate.DefaultPlanTemplate))
|
|
}
|
|
|
|
// reviewTemplateOfSize pads the built-in Review handoff template with trailing
|
|
// static text so the returned template is exactly size bytes long.
|
|
func reviewTemplateOfSize(size int) string {
|
|
return singlerequesttemplate.DefaultReviewTemplate + strings.Repeat(" ", size-len(singlerequesttemplate.DefaultReviewTemplate))
|
|
}
|
|
|
|
func TestValidatePlanTemplate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tmpl string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "default plan template is valid",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "custom plan template with extra static text",
|
|
tmpl: `# Plan
|
|
|
|
Custom header notes.
|
|
|
|
## Goal
|
|
{{goal}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
|
|
Footer notes.
|
|
`,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty template",
|
|
tmpl: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "oversized template 8193 bytes",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate + strings.Repeat(" ", 8193-len(singlerequesttemplate.DefaultPlanTemplate)),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "missing {{goal}}",
|
|
tmpl: `# Plan
|
|
|
|
## Goal
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "duplicate {{goal}}",
|
|
tmpl: `# Plan
|
|
|
|
## Goal
|
|
{{goal}} {{goal}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unknown token",
|
|
tmpl: `# Plan
|
|
|
|
## Goal
|
|
{{goal}} {{foo}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "wrong token order",
|
|
tmpl: `# Plan
|
|
|
|
## Goal
|
|
{{steps}}
|
|
|
|
## Steps
|
|
{{goal}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "missing required heading # Plan",
|
|
tmpl: `## Goal
|
|
{{goal}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "exact 8192 bytes accepted",
|
|
tmpl: planTemplateOfSize(8192),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "decorated heading ### Plan",
|
|
tmpl: `### Plan
|
|
|
|
## Goal
|
|
{{goal}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "heading line carries trailing text",
|
|
tmpl: `# Plan Mismatch
|
|
|
|
## Goal
|
|
{{goal}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "heading embedded inside a prose line",
|
|
tmpl: `# Plan
|
|
|
|
Documented as ## Goal below.
|
|
{{goal}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "duplicate required heading",
|
|
tmpl: `# Plan
|
|
|
|
## Goal
|
|
{{goal}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
|
|
## Goal
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unbalanced opening delimiter residue",
|
|
tmpl: `# Plan
|
|
|
|
## Goal
|
|
{{goal}} {{
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unbalanced closing delimiter residue",
|
|
tmpl: `# Plan
|
|
|
|
## Goal
|
|
{{goal}}}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "nested delimiter around a documented placeholder",
|
|
tmpl: `# Plan
|
|
|
|
## Goal
|
|
{{{{goal}}}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := singlerequesttemplate.ValidatePlanTemplate(tt.tmpl)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ValidatePlanTemplate() err = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParsePlan(t *testing.T) {
|
|
validOutput := `# Plan
|
|
|
|
## Goal
|
|
Fix single-request template handling bug.
|
|
|
|
## Steps
|
|
- [P1] Inspect template file resolution.
|
|
- [P2] Verify template validation logic.
|
|
|
|
## Verification
|
|
- Run go test on singlerequesttemplate package.
|
|
`
|
|
|
|
tests := []struct {
|
|
name string
|
|
tmpl string
|
|
raw string
|
|
maxOutputBytes int
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "valid plan output default template",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: validOutput,
|
|
maxOutputBytes: 1024,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "boundary steps = 2, verif = 1",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: validOutput,
|
|
maxOutputBytes: 1024,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "provider omits final line feed",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: strings.TrimSuffix(validOutput, "\n"),
|
|
maxOutputBytes: 1024,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "provider omits only final line feed after static suffix",
|
|
tmpl: `# Plan
|
|
|
|
## Goal
|
|
{{goal}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
|
|
END
|
|
`,
|
|
raw: `# Plan
|
|
|
|
## Goal
|
|
Fix suffix parsing.
|
|
|
|
## Steps
|
|
- [P1] Keep the static suffix.
|
|
- [P2] Allow the final line feed omission.
|
|
|
|
## Verification
|
|
- Run the parser tests.
|
|
|
|
END`,
|
|
maxOutputBytes: 1024,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "provider omits static suffix text",
|
|
tmpl: `# Plan
|
|
|
|
## Goal
|
|
{{goal}}
|
|
|
|
## Steps
|
|
{{steps}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
|
|
END
|
|
`,
|
|
raw: strings.TrimSuffix(validOutput, "\n"),
|
|
maxOutputBytes: 1024,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "boundary steps = 6, verif = 3",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: `# Plan
|
|
|
|
## Goal
|
|
Implement feature end to end.
|
|
|
|
## Steps
|
|
- [P1] Step one
|
|
- [P2] Step two
|
|
- [P3] Step three
|
|
- [P4] Step four
|
|
- [P5] Step five
|
|
- [P6] Step six
|
|
|
|
## Verification
|
|
- Verify 1
|
|
- Verify 2
|
|
- Verify 3
|
|
`,
|
|
maxOutputBytes: 2048,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "invalid step count = 1 (too few)",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: `# Plan
|
|
|
|
## Goal
|
|
Implement feature.
|
|
|
|
## Steps
|
|
- [P1] Step one
|
|
|
|
## Verification
|
|
- Verify 1
|
|
`,
|
|
maxOutputBytes: 1024,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid step count = 7 (too many)",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: `# Plan
|
|
|
|
## Goal
|
|
Implement feature.
|
|
|
|
## Steps
|
|
- [P1] Step 1
|
|
- [P2] Step 2
|
|
- [P3] Step 3
|
|
- [P4] Step 4
|
|
- [P5] Step 5
|
|
- [P6] Step 6
|
|
- [P7] Step 7
|
|
|
|
## Verification
|
|
- Verify 1
|
|
`,
|
|
maxOutputBytes: 1024,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid verif count = 0 (too few)",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: `# Plan
|
|
|
|
## Goal
|
|
Implement feature.
|
|
|
|
## Steps
|
|
- [P1] Step 1
|
|
- [P2] Step 2
|
|
|
|
## Verification
|
|
|
|
`,
|
|
maxOutputBytes: 1024,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "invalid verif count = 4 (too many)",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: `# Plan
|
|
|
|
## Goal
|
|
Implement feature.
|
|
|
|
## Steps
|
|
- [P1] Step 1
|
|
- [P2] Step 2
|
|
|
|
## Verification
|
|
- Verify 1
|
|
- Verify 2
|
|
- Verify 3
|
|
- Verify 4
|
|
`,
|
|
maxOutputBytes: 1024,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "step missing deterministic P1 id",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: `# Plan
|
|
|
|
## Goal
|
|
Implement feature.
|
|
|
|
## Steps
|
|
- Inspect template file resolution.
|
|
- Verify template validation logic.
|
|
|
|
## Verification
|
|
- Run go test on singlerequesttemplate package.
|
|
`,
|
|
maxOutputBytes: 1024,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "step id out of order P1, P3",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: `# Plan
|
|
|
|
## Goal
|
|
Implement feature.
|
|
|
|
## Steps
|
|
- [P1] Step one
|
|
- [P3] Step three
|
|
|
|
## Verification
|
|
- Verify 1
|
|
`,
|
|
maxOutputBytes: 1024,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "step id duplicate P1, P1",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: `# Plan
|
|
|
|
## Goal
|
|
Implement feature.
|
|
|
|
## Steps
|
|
- [P1] Step one
|
|
- [P1] Step dup
|
|
|
|
## Verification
|
|
- Verify 1
|
|
`,
|
|
maxOutputBytes: 1024,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "multiline goal",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: `# Plan
|
|
|
|
## Goal
|
|
First line of goal.
|
|
Second line of goal.
|
|
|
|
## Steps
|
|
- [P1] Step 1
|
|
- [P2] Step 2
|
|
|
|
## Verification
|
|
- Verify 1
|
|
`,
|
|
maxOutputBytes: 1024,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "altered heading static text",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: `# Plan Mismatch
|
|
|
|
## Goal
|
|
Fix bug.
|
|
|
|
## Steps
|
|
- [P1] Step 1
|
|
- [P2] Step 2
|
|
|
|
## Verification
|
|
- Verify 1
|
|
`,
|
|
maxOutputBytes: 1024,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unresolved token in output",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: `# Plan
|
|
|
|
## Goal
|
|
Fix {{goal}} bug.
|
|
|
|
## Steps
|
|
- [P1] Step 1
|
|
- [P2] Step 2
|
|
|
|
## Verification
|
|
- Verify 1
|
|
`,
|
|
maxOutputBytes: 1024,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "output exceeds maxOutputBytes",
|
|
tmpl: singlerequesttemplate.DefaultPlanTemplate,
|
|
raw: validOutput,
|
|
maxOutputBytes: 10,
|
|
wantErr: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got, err := singlerequesttemplate.ParsePlan(tt.tmpl, tt.raw, tt.maxOutputBytes)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ParsePlan() err = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
if !tt.wantErr && string(got) != tt.raw {
|
|
t.Errorf("ParsePlan() got = %q, want %q", string(got), tt.raw)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRenderPlan(t *testing.T) {
|
|
fields := singlerequesttemplate.PlanFields{
|
|
Goal: " Inspect the target. ",
|
|
Steps: []string{" Step one. ", "\tStep two. "},
|
|
Verification: []string{" Run focused tests. "},
|
|
}
|
|
want := "# Plan\n\n## Goal\nInspect the target.\n\n## Steps\n- [P1] Step one.\n- [P2] Step two.\n\n## Verification\n- Run focused tests.\n"
|
|
got, err := singlerequesttemplate.RenderPlan(singlerequesttemplate.DefaultPlanTemplate, fields, 1024)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != want {
|
|
t.Fatalf("RenderPlan() = %q, want %q", got, want)
|
|
}
|
|
|
|
custom := "# Plan\n\nOperator note.\n\n## Goal\n{{goal}}\n\n## Steps\n{{steps}}\n\n## Verification\n{{verification}}\n\nEND\n"
|
|
wantCustom := "# Plan\n\nOperator note.\n\n## Goal\nInspect the target.\n\n## Steps\n- [P1] Step one.\n- [P2] Step two.\n\n## Verification\n- Run focused tests.\n\nEND\n"
|
|
got, err = singlerequesttemplate.RenderPlan(custom, fields, 1024)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != wantCustom {
|
|
t.Fatalf("custom RenderPlan() = %q, want %q", got, wantCustom)
|
|
}
|
|
}
|
|
|
|
func TestRenderPlanRejectsMalformedFields(t *testing.T) {
|
|
valid := singlerequesttemplate.PlanFields{Goal: "Fix bug.", Steps: []string{"Step 1", "Step 2"}, Verification: []string{"Verify 1"}}
|
|
tests := []struct {
|
|
name string
|
|
fields singlerequesttemplate.PlanFields
|
|
max int
|
|
}{
|
|
{"empty-goal", singlerequesttemplate.PlanFields{Steps: valid.Steps, Verification: valid.Verification}, 1024},
|
|
{"multiline-goal", singlerequesttemplate.PlanFields{Goal: "Line 1\nLine 2", Steps: valid.Steps, Verification: valid.Verification}, 1024},
|
|
{"one-step", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: []string{"Step 1"}, Verification: valid.Verification}, 1024},
|
|
{"seven-steps", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: []string{"S1", "S2", "S3", "S4", "S5", "S6", "S7"}, Verification: valid.Verification}, 1024},
|
|
{"empty-step", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: []string{"Step 1", ""}, Verification: valid.Verification}, 1024},
|
|
{"multiline-step", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: []string{"Step 1\ncontinued", "Step 2"}, Verification: valid.Verification}, 1024},
|
|
{"four-verifications", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: valid.Steps, Verification: []string{"V1", "V2", "V3", "V4"}}, 1024},
|
|
{"empty-verification", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: valid.Steps, Verification: []string{""}}, 1024},
|
|
{"multiline-verification", singlerequesttemplate.PlanFields{Goal: valid.Goal, Steps: valid.Steps, Verification: []string{"Verify 1\ncontinued"}}, 1024},
|
|
{"unresolved-token", singlerequesttemplate.PlanFields{Goal: "Fix {{goal}}.", Steps: valid.Steps, Verification: valid.Verification}, 1024},
|
|
{"zero-max", valid, 0},
|
|
{"output-over-limit", valid, 10},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if _, err := singlerequesttemplate.RenderPlan(singlerequesttemplate.DefaultPlanTemplate, tt.fields, tt.max); err == nil {
|
|
t.Fatal("RenderPlan() succeeded, want error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPlanItemIDs(t *testing.T) {
|
|
rendered, err := singlerequesttemplate.RenderPlan(singlerequesttemplate.DefaultPlanTemplate, singlerequesttemplate.PlanFields{
|
|
Goal: "Goal line.",
|
|
Steps: []string{"Step one.", "Step two.", "Step three."},
|
|
Verification: []string{"Verify."},
|
|
}, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ids, err := singlerequesttemplate.PlanItemIDs(rendered)
|
|
if err != nil || len(ids) != 3 || ids[0] != "P1" || ids[1] != "P2" || ids[2] != "P3" {
|
|
t.Fatalf("ids=%v err=%v", ids, err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
plan string
|
|
wantErr bool
|
|
}{
|
|
{"empty", "", true},
|
|
{"no step ids", "# Plan\n\n## Goal\nGoal.\n\n## Steps\n- Step one\n- Step two\n\n## Verification\n- V\n", true},
|
|
{"gap p1 then p3", "# Plan\n\n## Goal\nGoal.\n\n## Steps\n- [P1] Step one\n- [P3] Step three\n\n## Verification\n- V\n", true},
|
|
{"duplicate p1", "# Plan\n\n## Goal\nGoal.\n\n## Steps\n- [P1] Step one\n- [P1] Step dup\n\n## Verification\n- V\n", true},
|
|
{"starts at p0", "# Plan\n\n## Goal\nGoal.\n\n## Steps\n- [P0] Step zero\n- [P1] Step one\n\n## Verification\n- V\n", true},
|
|
{"descending order", "# Plan\n\n## Goal\nGoal.\n\n## Steps\n- [P2] Step two\n- [P1] Step one\n\n## Verification\n- V\n", true},
|
|
{"valid p1 p2", "# Plan\n\n## Goal\nGoal.\n\n## Steps\n- [P1] Step one\n- [P2] Step two\n\n## Verification\n- V\n", false},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := singlerequesttemplate.PlanItemIDs([]byte(tt.plan))
|
|
if (err != nil) != tt.wantErr {
|
|
t.Fatalf("err=%v wantErr=%v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateReviewTemplate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
tmpl string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "default review handoff template is valid",
|
|
tmpl: singlerequesttemplate.DefaultReviewTemplate,
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty template",
|
|
tmpl: "",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "oversized template 8193 bytes",
|
|
tmpl: reviewTemplateOfSize(8193),
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "exact 8192 bytes accepted",
|
|
tmpl: reviewTemplateOfSize(8192),
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "missing {{deviations}}",
|
|
tmpl: `# Review
|
|
|
|
## Worker Item Status
|
|
{{item_status}}
|
|
|
|
## Worker Changes
|
|
{{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "duplicate {{changes}}",
|
|
tmpl: `# Review
|
|
|
|
## Worker Item Status
|
|
{{item_status}}
|
|
|
|
## Worker Changes
|
|
{{changes}} {{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unknown placeholder",
|
|
tmpl: `# Review
|
|
|
|
## Worker Item Status
|
|
{{item_status}} {{severity}}
|
|
|
|
## Worker Changes
|
|
{{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "wrong placeholder order",
|
|
tmpl: `# Review
|
|
|
|
## Worker Item Status
|
|
{{changes}}
|
|
|
|
## Worker Changes
|
|
{{item_status}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "missing required heading # Review",
|
|
tmpl: `## Worker Item Status
|
|
{{item_status}}
|
|
|
|
## Worker Changes
|
|
{{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "decorated heading ### Review",
|
|
tmpl: `### Review
|
|
|
|
## Worker Item Status
|
|
{{item_status}}
|
|
|
|
## Worker Changes
|
|
{{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "decorated worker heading",
|
|
tmpl: `# Review
|
|
|
|
### Worker Item Status
|
|
{{item_status}}
|
|
|
|
## Worker Changes
|
|
{{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "duplicate worker heading",
|
|
tmpl: `# Review
|
|
|
|
## Worker Item Status
|
|
{{item_status}}
|
|
|
|
## Worker Changes
|
|
{{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
|
|
## Worker Item Status
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unbalanced opening delimiter residue",
|
|
tmpl: `# Review
|
|
|
|
## Worker Item Status
|
|
{{item_status}} {{
|
|
|
|
## Worker Changes
|
|
{{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "unbalanced closing delimiter residue",
|
|
tmpl: `# Review
|
|
|
|
## Worker Item Status
|
|
{{item_status}}}}
|
|
|
|
## Worker Changes
|
|
{{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "reviewer-only Result placeholder rejected",
|
|
tmpl: `# Review
|
|
|
|
## Result
|
|
PASS
|
|
|
|
## Worker Item Status
|
|
{{item_status}}
|
|
|
|
## Worker Changes
|
|
{{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "reviewer-only Summary placeholder rejected",
|
|
tmpl: `# Review
|
|
|
|
## Worker Item Status
|
|
{{item_status}}
|
|
|
|
## Worker Changes
|
|
{{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
|
|
## Summary
|
|
{{summary}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "legacy reviewer grammar fully rejected",
|
|
tmpl: `# Review
|
|
|
|
## Result
|
|
PASS
|
|
|
|
## Checks
|
|
{{checks}}
|
|
|
|
## Verification
|
|
{{verification}}
|
|
|
|
## Summary
|
|
{{summary}}
|
|
`,
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "custom handoff template with extra static text",
|
|
tmpl: `# Review
|
|
|
|
Operator preamble.
|
|
|
|
## Worker Item Status
|
|
{{item_status}}
|
|
|
|
## Worker Changes
|
|
{{changes}}
|
|
|
|
## Worker Verification
|
|
{{verification}}
|
|
|
|
## Deviations
|
|
{{deviations}}
|
|
|
|
Operator footer.
|
|
`,
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := singlerequesttemplate.ValidateReviewTemplate(tt.tmpl)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("ValidateReviewTemplate() err = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRenderReview(t *testing.T) {
|
|
fields := singlerequesttemplate.ReviewFields{
|
|
ItemStatus: "- P1: completed\n- P2: completed",
|
|
Changes: "- Wrote result.txt with the requested value",
|
|
Verification: "- Ran verify and observed success",
|
|
Deviations: "None",
|
|
}
|
|
|
|
got, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatalf("RenderReview() unexpected err = %v", err)
|
|
}
|
|
|
|
want := `# Review
|
|
|
|
## Worker Item Status
|
|
- P1: completed
|
|
- P2: completed
|
|
|
|
## Worker Changes
|
|
- Wrote result.txt with the requested value
|
|
|
|
## Worker Verification
|
|
- Ran verify and observed success
|
|
|
|
## Deviations
|
|
None
|
|
`
|
|
if string(got) != want {
|
|
t.Errorf("RenderReview() got:\n%s\nwant:\n%s", string(got), want)
|
|
}
|
|
|
|
custom := "# Review\n\nOperator preamble.\n\n## Worker Item Status\n{{item_status}}\n\n## Worker Changes\n{{changes}}\n\n## Worker Verification\n{{verification}}\n\n## Deviations\n{{deviations}}\n\nOperator footer.\n"
|
|
wantCustom := `# Review
|
|
|
|
Operator preamble.
|
|
|
|
## Worker Item Status
|
|
- P1: completed
|
|
- P2: completed
|
|
|
|
## Worker Changes
|
|
- Wrote result.txt with the requested value
|
|
|
|
## Worker Verification
|
|
- Ran verify and observed success
|
|
|
|
## Deviations
|
|
None
|
|
|
|
Operator footer.
|
|
`
|
|
got, err = singlerequesttemplate.RenderReview(custom, fields, 4096)
|
|
if err != nil {
|
|
t.Fatalf("custom RenderReview() err=%v", err)
|
|
}
|
|
if string(got) != wantCustom {
|
|
t.Errorf("custom RenderReview() got:\n%s\nwant:\n%s", string(got), wantCustom)
|
|
}
|
|
|
|
// Missing field test: every worker section must be non-empty.
|
|
for name, mutated := range map[string]singlerequesttemplate.ReviewFields{
|
|
"empty-item-status": {ItemStatus: "", Changes: fields.Changes, Verification: fields.Verification, Deviations: fields.Deviations},
|
|
"empty-changes": {ItemStatus: fields.ItemStatus, Changes: "", Verification: fields.Verification, Deviations: fields.Deviations},
|
|
"empty-verification": {ItemStatus: fields.ItemStatus, Changes: fields.Changes, Verification: "", Deviations: fields.Deviations},
|
|
"empty-deviations": {ItemStatus: fields.ItemStatus, Changes: fields.Changes, Verification: fields.Verification, Deviations: ""},
|
|
"whitespace-deviations": {ItemStatus: fields.ItemStatus, Changes: fields.Changes, Verification: fields.Verification, Deviations: " "},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, mutated, 4096); err == nil {
|
|
t.Fatalf("RenderReview() expected error for %s", name)
|
|
}
|
|
})
|
|
}
|
|
|
|
if _, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 0); err == nil {
|
|
t.Fatalf("RenderReview() expected error for zero max")
|
|
}
|
|
if _, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 10); err == nil {
|
|
t.Fatalf("RenderReview() expected error for output over limit")
|
|
}
|
|
}
|
|
|
|
func TestValidateReviewHandoff(t *testing.T) {
|
|
planIDs := []string{"P1", "P2"}
|
|
|
|
baseFields := singlerequesttemplate.ReviewFields{
|
|
ItemStatus: "- P1: completed\n- P2: completed",
|
|
Changes: "- Wrote result.txt",
|
|
Verification: "- Ran verify",
|
|
Deviations: "None",
|
|
}
|
|
valid, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, baseFields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(valid, planIDs); err != nil {
|
|
t.Fatalf("valid handoff rejected: %v", err)
|
|
}
|
|
|
|
t.Run("missing or duplicate sections rejected", func(t *testing.T) {
|
|
missingItemStatus := strings.Replace(string(valid), "## Worker Item Status", "## Renamed", 1)
|
|
if err := singlerequesttemplate.ValidateReviewHandoff([]byte(missingItemStatus), planIDs); err == nil {
|
|
t.Fatalf("expected error for missing Worker Item Status heading")
|
|
}
|
|
dup := string(valid) + "\n## Worker Item Status\n- P1: completed\n"
|
|
if err := singlerequesttemplate.ValidateReviewHandoff([]byte(dup), planIDs); err == nil {
|
|
t.Fatalf("expected error for duplicate Worker Item Status heading")
|
|
}
|
|
})
|
|
|
|
t.Run("empty content rejected", func(t *testing.T) {
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(nil, planIDs); err == nil {
|
|
t.Fatalf("expected error for empty content")
|
|
}
|
|
})
|
|
|
|
t.Run("unknown plan id rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P1: completed\n- P9: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for unknown plan id P9")
|
|
}
|
|
})
|
|
|
|
t.Run("missing plan id rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P1: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for missing plan id P2")
|
|
}
|
|
})
|
|
|
|
t.Run("duplicate plan id rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P1: completed\n- P1: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for duplicate plan id P1")
|
|
}
|
|
})
|
|
|
|
t.Run("non-completed status rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P1: completed\n- P2: skipped"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for non-completed P2")
|
|
}
|
|
})
|
|
|
|
t.Run("out-of-order plan ids rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P2: completed\n- P1: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for out-of-order plan ids")
|
|
}
|
|
})
|
|
|
|
t.Run("extra status line rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P1: completed\n- P2: completed\n- P3: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for extra plan id P3")
|
|
}
|
|
})
|
|
|
|
t.Run("empty plan id list rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P1: completed\n- P2: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, nil); err == nil {
|
|
t.Fatalf("expected error for empty plan id list against non-empty status")
|
|
}
|
|
})
|
|
|
|
t.Run("prose injected between status lines rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P1: completed\nThis is a note.\n- P2: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for prose injected between status lines")
|
|
}
|
|
})
|
|
|
|
t.Run("malformed bullet missing completed status rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P1: incomplete\n- P2: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for malformed bullet missing completed status")
|
|
}
|
|
})
|
|
|
|
t.Run("blank line in status section rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P1: completed\n\n- P2: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for blank line in status section")
|
|
}
|
|
})
|
|
|
|
t.Run("out-of-order status lines rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P2: completed\n- P1: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for out-of-order status lines")
|
|
}
|
|
})
|
|
|
|
t.Run("malformed bullet with extra text rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "- P1: completed extra text\n- P2: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for malformed bullet with extra text")
|
|
}
|
|
})
|
|
|
|
t.Run("decorated heading variant in status rejected", func(t *testing.T) {
|
|
fields := baseFields
|
|
fields.ItemStatus = "## P1: completed\n- P2: completed"
|
|
rendered, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 4096)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := singlerequesttemplate.ValidateReviewHandoff(rendered, planIDs); err == nil {
|
|
t.Fatalf("expected error for decorated heading variant in status")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestDigest(t *testing.T) {
|
|
d1 := singlerequesttemplate.Digest(singlerequesttemplate.DefaultPlanTemplate)
|
|
d2 := singlerequesttemplate.Digest(singlerequesttemplate.DefaultPlanTemplate)
|
|
d3 := singlerequesttemplate.Digest("other content")
|
|
|
|
if d1 == "" {
|
|
t.Errorf("Digest() returned empty string")
|
|
}
|
|
if d1 != d2 {
|
|
t.Errorf("Digest() not deterministic: %q != %q", d1, d2)
|
|
}
|
|
if d1 == d3 {
|
|
t.Errorf("Digest() collision: %q == %q", d1, d3)
|
|
}
|
|
}
|