iop/packages/go/singlerequesttemplate/template_test.go
toki 39fa1da55b fix(benchmark): 원샷 비교 실행 실패를 해소한다
호출기별 격리 쓰기 계약과 Gemini ingress, 단일 요청 stage 처리를 맞춰 실제 9-cell 비교가 생성물을 남길 수 있게 한다.
2026-08-12 14:37:17 +09:00

824 lines
12 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 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
- Inspect template file resolution.
- 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
- Keep the static suffix.
- 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
- Step one
- Step two
- Step three
- Step four
- Step five
- 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
- 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
- Step 1
- Step 2
- Step 3
- Step 4
- Step 5
- Step 6
- 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
- Step 1
- Step 2
## Verification
`,
maxOutputBytes: 1024,
wantErr: true,
},
{
name: "invalid verif count = 4 (too many)",
tmpl: singlerequesttemplate.DefaultPlanTemplate,
raw: `# Plan
## Goal
Implement feature.
## Steps
- Step 1
- Step 2
## Verification
- Verify 1
- Verify 2
- Verify 3
- Verify 4
`,
maxOutputBytes: 1024,
wantErr: true,
},
{
name: "multiline goal",
tmpl: singlerequesttemplate.DefaultPlanTemplate,
raw: `# Plan
## Goal
First line of goal.
Second line of goal.
## Steps
- Step 1
- Step 2
## Verification
- Verify 1
`,
maxOutputBytes: 1024,
wantErr: true,
},
{
name: "altered heading static text",
tmpl: singlerequesttemplate.DefaultPlanTemplate,
raw: `# Plan Mismatch
## Goal
Fix bug.
## Steps
- Step 1
- Step 2
## Verification
- Verify 1
`,
maxOutputBytes: 1024,
wantErr: true,
},
{
name: "unresolved token in output",
tmpl: singlerequesttemplate.DefaultPlanTemplate,
raw: `# Plan
## Goal
Fix {{goal}} bug.
## Steps
- Step 1
- 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 TestValidateReviewTemplate(t *testing.T) {
tests := []struct {
name string
tmpl string
wantErr bool
}{
{
name: "default review template is valid",
tmpl: singlerequesttemplate.DefaultReviewTemplate,
wantErr: false,
},
{
name: "empty template",
tmpl: "",
wantErr: true,
},
{
name: "missing PASS",
tmpl: `# Review
## Result
FAIL
## Checks
{{checks}}
## Verification
{{verification}}
## Summary
{{summary}}
`,
wantErr: true,
},
{
name: "wrong order",
tmpl: `# Review
## Result
PASS
## Verification
{{verification}}
## Checks
{{checks}}
## Summary
{{summary}}
`,
wantErr: true,
},
{
name: "oversized template 8193 bytes",
tmpl: reviewTemplateOfSize(8193),
wantErr: true,
},
{
name: "exact 8192 bytes accepted",
tmpl: reviewTemplateOfSize(8192),
wantErr: false,
},
{
name: "NOTPASS does not satisfy the PASS result line",
tmpl: `# Review
## Result
NOTPASS
## Checks
{{checks}}
## Verification
{{verification}}
## Summary
{{summary}}
`,
wantErr: true,
},
{
name: "PASS embedded in a prose line",
tmpl: `# Review
## Result
Result: PASS
## Checks
{{checks}}
## Verification
{{verification}}
## Summary
{{summary}}
`,
wantErr: true,
},
{
name: "decorated heading ### Review",
tmpl: `### Review
## Result
PASS
## Checks
{{checks}}
## Verification
{{verification}}
## Summary
{{summary}}
`,
wantErr: true,
},
{
name: "duplicate PASS result line",
tmpl: `# Review
## Result
PASS
PASS
## Checks
{{checks}}
## Verification
{{verification}}
## Summary
{{summary}}
`,
wantErr: true,
},
{
name: "missing {{summary}}",
tmpl: `# Review
## Result
PASS
## Checks
{{checks}}
## Verification
{{verification}}
## Summary
`,
wantErr: true,
},
{
name: "duplicate {{checks}}",
tmpl: `# Review
## Result
PASS
## Checks
{{checks}} {{checks}}
## Verification
{{verification}}
## Summary
{{summary}}
`,
wantErr: true,
},
{
name: "unknown placeholder",
tmpl: `# Review
## Result
PASS
## Checks
{{checks}} {{severity}}
## Verification
{{verification}}
## Summary
{{summary}}
`,
wantErr: true,
},
{
name: "unbalanced delimiter residue",
tmpl: `# Review
## Result
PASS
## Checks
{{checks}}
## Verification
{{verification}}
## Summary
{{summary}} }}
`,
wantErr: true,
},
{
name: "custom review template with extra static text",
tmpl: `# Review
Operator preamble.
## Result
PASS
## Checks
{{checks}}
## Verification
{{verification}}
## Summary
{{summary}}
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{
Checks: "- Checked file permissions\n- Verified build pass",
Verification: "- Executed unit test suite",
Summary: "All requirements met successfully.",
}
got, err := singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, fields, 1024)
if err != nil {
t.Fatalf("RenderReview() unexpected err = %v", err)
}
want := `# Review
## Result
PASS
## Checks
- Checked file permissions
- Verified build pass
## Verification
- Executed unit test suite
## Summary
All requirements met successfully.
`
if string(got) != want {
t.Errorf("RenderReview() got:\n%s\nwant:\n%s", string(got), want)
}
// Missing field test
badFields := fields
badFields.Summary = ""
_, err = singlerequesttemplate.RenderReview(singlerequesttemplate.DefaultReviewTemplate, badFields, 1024)
if err == nil {
t.Errorf("RenderReview() expected error for empty Summary, got nil")
}
}
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)
}
}