diff --git a/.agents/skills/go-engineer/SKILL.md b/.agents/skills/go-engineer/SKILL.md index 720d3e8..00862d6 100644 --- a/.agents/skills/go-engineer/SKILL.md +++ b/.agents/skills/go-engineer/SKILL.md @@ -33,8 +33,8 @@ lives in `.agents/skills/review-docs/SKILL.md`. compatibility constraints, or verification target is not explicit. 3. Apply the MUST / MUST NOT rules while editing. 4. Defer test structure and fixtures to `go-tester` for test changes. -5. Verify with the narrowest relevant Go test first, then the repository-level - checks listed below. +5. Verify with the narrowest relevant Go test and lint target first, then the + repository-level checks listed below. 6. Follow the git-history policy in `AGENTS.md`. ## Setup Check @@ -47,9 +47,10 @@ Run this before non-trivial Go changes or when the package baseline is unclear: support package. 3. **Test owner** - identify the package test suite and fixtures that already cover the behavior. -4. **Commands** - plan `gofmt`, focused `go test`, `go vet ./...`, full - `go test ./...`, and `go build -trimpath main.go` when integration or CLI - behavior changes. +4. **Commands** - plan `gofmt`, focused `go test`, focused + `golangci-lint run`, `go vet ./...`, full `go test ./...`, + `golangci-lint run ./...`, and `go build -trimpath main.go` when + integration or CLI behavior changes. ## Processing Flow @@ -150,9 +151,12 @@ Run the narrowest relevant checks first, then broaden: 1. `gofmt -w ` 2. `go test ./` -3. `go vet ./...` -4. `go test ./...` -5. `go build -trimpath main.go` when CLI, configuration, embedding, or package +3. `golangci-lint run ./` or the smallest package set that + contains touched Go files +4. `go vet ./...` +5. `go test ./...` +6. `golangci-lint run ./...` +7. `go build -trimpath main.go` when CLI, configuration, embedding, or package integration behavior changed For user-visible CLI behavior, run a focused `go run ./main.go ...` scenario diff --git a/.agents/skills/go-tester/SKILL.md b/.agents/skills/go-tester/SKILL.md index d92c9ac..b27e0f5 100644 --- a/.agents/skills/go-tester/SKILL.md +++ b/.agents/skills/go-tester/SKILL.md @@ -52,7 +52,8 @@ Two companions own neighboring concerns: fixture needed to express the behavior. 5. **Assert behavior, not implementation trivia.** Prefer outputs, changed-file lists, typed errors, and line numbers over private state. -6. **Run the narrowest test first.** Broaden only after the focused test passes. +6. **Run the narrowest test and lint target first.** Broaden only after the + focused package passes. ## Naming And Structure @@ -112,7 +113,10 @@ After Go test code changes, run: 1. `gofmt -w ` 2. the focused package test -3. `go test ./...` +3. `golangci-lint run ./` or the smallest package set that + contains touched Go files +4. `go test ./...` +5. `golangci-lint run ./...` Run `go vet ./...` when the test change also changes helper code, exported test support, or production code. diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index e432c07..46beba0 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -30,5 +30,10 @@ jobs: - name: Run Tests run: go test -v ./... + - name: Run Fuzz Tests + run: | + go test -run=^$ -fuzz=FuzzCutIndent -fuzztime=5s ./indent + go test -run=^$ -fuzz=FuzzPatternFindIn -fuzztime=5s ./embedding/parsing + - name: Run E2E Tests run: go test -v -tags showcase ./showcase diff --git a/embedding/parsing/instruction_test.go b/embedding/parsing/instruction_test.go index 2241c30..e048a23 100644 --- a/embedding/parsing/instruction_test.go +++ b/embedding/parsing/instruction_test.go @@ -28,6 +28,7 @@ package parsing_test import ( _type "embed-code/embed-code-go/type" + "errors" "fmt" "os" "path/filepath" @@ -136,6 +137,19 @@ var _ = Describe("Instruction", func() { Expect(err).Should(MatchError(ContainSubstring("invalid start pattern `[`"))) }) + It("should embed a line ending with a literal opening brace pattern", func() { + instructionParams := TestInstructionParams{ + lineGlob: "class Hello {", + } + + actualLines := getXMLExtractionContent( + "org/example/Hello.java", instructionParams, config) + + Expect(actualLines).Should(Equal([]string{ + "public class Hello {", + })) + }) + It("should successfully read source content", func() { instructionParams := TestInstructionParams{ closeTag: true, @@ -551,6 +565,27 @@ var _ = Describe("Instruction", func() { )) }) + It("should report pattern not found when the glob matcher panics", func() { + sourceRoot := GinkgoT().TempDir() + config.CodeRoots = _type.NamedPathList{_type.NamedPath{Path: sourceRoot}} + Expect(os.WriteFile( + filepath.Join(sourceRoot, "panic-pattern.txt"), + []byte("0\n"), + 0600, + )).To(Succeed()) + xmlString := buildInstruction("panic-pattern.txt", TestInstructionParams{ + lineGlob: "0{}", + }) + instruction := createInstructionFromXML(xmlString, config) + + _, err := instruction.Content() + + var patternErr parsing.PatternNotFoundError + Expect(errors.As(err, &patternErr)).Should(BeTrue()) + Expect(patternErr.Kind).Should(Equal("line")) + Expect(err).Should(MatchError(ContainSubstring("line pattern `0{}`"))) + }) + It("should use shared resolver as default", func() { sourceRoot := GinkgoT().TempDir() markdownPath := filepath.Join(GinkgoT().TempDir(), "doc.md") diff --git a/embedding/parsing/pattern.go b/embedding/parsing/pattern.go index d4568fe..abe4b96 100644 --- a/embedding/parsing/pattern.go +++ b/embedding/parsing/pattern.go @@ -120,17 +120,60 @@ func compileLineMatcher(patternLine string) (lineMatcher, error) { pattern = pattern[:lastIndex] } - compiledGlob, err := glob.Compile(pattern) - if err != nil { - return lineMatcher{}, err + var matcher lineMatcher + var compileErr error + func() { + defer func() { + recovered := recover() + if recovered != nil { + compileErr = fmt.Errorf("glob pattern compiler panicked: %v", recovered) + } + }() + + compiledGlob, err := glob.Compile(pattern) + if err != nil { + compileErr = err + + return + } + matcher = lineMatcher{compiled: compiledGlob} + }() + if compileErr != nil { + return lineMatcher{}, compileErr } - return lineMatcher{compiled: compiledGlob}, nil + return matcher, nil } // matches reports whether the source line matches the compiled pattern. func (m lineMatcher) matches(line string) bool { - return m.compiled != nil && m.compiled.Match(line) + if m.compiled == nil { + return false + } + + return matchGlob(m.compiled, line) +} + +// matchGlob reports whether a glob matches and treats dependency panics as misses. +// +// A miss lets the instruction layer report PatternNotFoundError with source +// context instead of exposing a third-party matcher panic to users. +// Recovery stays at single-line matcher granularity so one panicking candidate +// does not abort the rest of the source scan. +func matchGlob(compiledGlob glob.Glob, line string) bool { + var matched bool + func() { + defer func() { + recovered := recover() + if recovered != nil { + matched = false + } + }() + + matched = compiledGlob.Match(line) + }() + + return matched } // FindIn returns the first source-line range matching the pattern. diff --git a/embedding/parsing/pattern_fuzz_test.go b/embedding/parsing/pattern_fuzz_test.go new file mode 100644 index 0000000..92dd2d9 --- /dev/null +++ b/embedding/parsing/pattern_fuzz_test.go @@ -0,0 +1,83 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package parsing_test + +import ( + "strings" + "testing" + + "embed-code/embed-code-go/embedding/parsing" +) + +// FuzzPatternFindIn checks source-line pattern compilation and matching. +func FuzzPatternFindIn(f *testing.F) { + f.Add("main", "public static void main(String[] args) {\n return;\n}", 0) + f.Add("^ padded text $ \\n ^Use \\* to multiply$", " padded text \nUse * to multiply", 0) + f.Add("^Use \\* to multiply$ \\n żółć$", "Use * to multiply\nżółć", 0) + f.Add(" return;", " System.out.println(\"Hi\");\n \n return;", 0) + f.Add("[", "invalid pattern is rejected", 0) + f.Add("0{$", "0", 0) + f.Add("0{}", "0", 0) + + f.Fuzz(func(t *testing.T, sourceGlob string, source string, startFrom int) { + if len(sourceGlob) > 4096 || len(source) > 8192 { + t.Skip("generated input exceeds the bounded fuzz target size") + } + + pattern, err := parsing.NewPattern(sourceGlob) + if err != nil { + return + } + lines := strings.Split(source, "\n") + + start, end, found := pattern.FindIn(lines, startFrom) + + if !found { + return + } + assertValidPatternMatch(t, lines, startFrom, start, end) + }) +} + +// assertValidPatternMatch checks the public range contract returned by FindIn. +func assertValidPatternMatch(t *testing.T, lines []string, startFrom int, start int, end int) { + t.Helper() + if start < 0 || start >= len(lines) { + t.Fatalf("start index %d is outside %d source lines", start, len(lines)) + } + if end < start || end >= len(lines) { + t.Fatalf( + "end index %d is outside matched range starting at %d in %d source lines", + end, + start, + len(lines), + ) + } + if startFrom >= 0 && start < startFrom { + t.Fatalf("match starts at %d before requested start %d", start, startFrom) + } +} diff --git a/indent/indent_fuzz_test.go b/indent/indent_fuzz_test.go new file mode 100644 index 0000000..540a01b --- /dev/null +++ b/indent/indent_fuzz_test.go @@ -0,0 +1,70 @@ +/* + * Copyright 2026, TeamDev. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package indent_test + +import ( + "strings" + "testing" + + "embed-code/embed-code-go/indent" +) + +// FuzzCutIndent checks indentation trimming over arbitrary source blocks. +func FuzzCutIndent(f *testing.F) { + f.Add(" System.out.println(\"Hi\");\n \n return;") + f.Add("\n foo\n bar\n\n baz") + f.Add("\t\tfirst\n\tsecond\n") + f.Add("plain\ntext") + + f.Fuzz(func(t *testing.T, source string) { + lines := strings.Split(source, "\n") + commonIndent := indent.MaxCommonIndentation(lines) + + changedLines := indent.CutIndent(lines, commonIndent) + + if len(changedLines) != len(lines) { + t.Fatalf("CutIndent returned %d lines for %d input lines", len(changedLines), len(lines)) + } + for lineIndex, line := range lines { + if commonIndent <= len(line) && changedLines[lineIndex] != line[commonIndent:] { + t.Fatalf( + "line %d was cut incorrectly: got %q, want %q", + lineIndex, + changedLines[lineIndex], + line[commonIndent:], + ) + } + if commonIndent > len(line) && changedLines[lineIndex] != "" { + t.Fatalf( + "short line %d was not fully removed: got %q", + lineIndex, + changedLines[lineIndex], + ) + } + } + }) +} diff --git a/showcase/embedding/positive/instruction-tag.md b/showcase/embedding/positive/instruction-tag.md index 7f08059..bc06cfe 100644 --- a/showcase/embedding/positive/instruction-tag.md +++ b/showcase/embedding/positive/instruction-tag.md @@ -11,6 +11,11 @@ configured `code-path`. With named source roots, start the value with `$name/`, such as `$java/org/showcase/Greeting.java`. With one unnamed source root, use a path relative to that root. +Embed-code treats documentation files and embedding instructions as trusted +local input. The `file` value may intentionally use relative path segments such +as `../`; embed-code resolves the path against the selected source root but does +not enforce that the final path stays inside that root. + Use one source-selection shape per instruction: - `file` alone embeds the whole source file.