문서 완성도 판정을 여러 런타임 계층에서 반복해 부분 Review가 Reviewer에 도달하지 못하던 실패를 없애기 위해 의미 검증 책임을 Reviewer로 모은다.
1269 lines
29 KiB
Go
1269 lines
29 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)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/* Removed: runtime output parsing duplicated the Planner and Reviewer contracts.
|
|
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")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/* Removed: Plan item inventory is Reviewer-owned semantic analysis.
|
|
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)
|
|
}
|
|
|
|
// Partial worker reports remain renderable so the Reviewer can repair them.
|
|
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() rejected partial %s: %v", name, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|
|
|
|
/* Removed: semantic Review validation belongs to the Reviewer, not transport.
|
|
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)
|
|
}
|
|
}
|