iop/packages/go/singlerequesttemplate/template_test.go
toki 31fada5d08 fix(single_request): 템플릿 승인 경계를 닫고 회귀 근거를 채운다
공식 리뷰가 지적한 두 가지 승인 경계 결함을 고친다. `plan_file`/`review_file`은
edge.yaml 디렉터리 기준 상대 경로만 허용하고 절대/빈 경로는 파일 접근 전에 거부한다.
필수 heading과 Review `PASS`는 부분 문자열이 아니라 정확한 단독 줄로 검증하며,
문서화된 placeholder를 제거한 뒤 남는 `{{`/`}}`를 거부해 문법을 닫는다.

리뷰가 존재한다고 기술했지만 실제로는 없던 회귀 근거를 추가한다. admission 시
고정된 effective 템플릿 쌍이 clone과 workspace 재검증을 통과하는지, refresh가
이미 승인된 요청이 아니라 새 요청에만 적용되는지, custom Review 템플릿이 내부
artifact만 바꾸고 caller 최종 출력은 그대로인지를 각각 확정 검증한다.

현재 문서도 실제 동작에 맞춘다. Edge 실행 spec의 낡은 Plan JSON Schema 서술을
direct PlanMD 검증으로 고치고, outer Anthropic 계약과 input spec에 템플릿이
Edge 내부 stage 입력일 뿐 caller 요청/응답 계약을 바꾸지 않음을 명시한다.

Refs: agent-task/single_request_plan_review_templates/PLAN-cloud-G08.md
2026-08-09 09:25:27 +09:00

767 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: "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)
}
}