aboutsummaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorDmitry Vyukov <dvyukov@google.com>2026-01-30 20:25:26 +0100
committerDmitry Vyukov <dvyukov@google.com>2026-01-31 16:07:13 +0000
commit3576455960ee88cefa43cad0bdfd1458549569b9 (patch)
treeb0943ccce2feb664e2a30dd2462d99cf13fc4bf7 /pkg
parentafcca7fa917427568d76a8295ff9f1e88824c1fe (diff)
pkg/aflow/flow/patching: use recent commit subjects
Give LLM the recent commit subjects when it generates description, so that it can use the same style. Add infrastrcuture to write end-to-end action tests to test it.
Diffstat (limited to 'pkg')
-rw-r--r--pkg/aflow/flow.go9
-rw-r--r--pkg/aflow/flow/patching/actions.go32
-rw-r--r--pkg/aflow/flow/patching/actions_test.go59
-rw-r--r--pkg/aflow/flow/patching/patching.go7
-rw-r--r--pkg/aflow/func_action.go13
-rw-r--r--pkg/aflow/schema.go9
-rw-r--r--pkg/aflow/test_action.go41
-rw-r--r--pkg/aflow/test_tool.go4
8 files changed, 163 insertions, 11 deletions
diff --git a/pkg/aflow/flow.go b/pkg/aflow/flow.go
index 467e28880..960c034dc 100644
--- a/pkg/aflow/flow.go
+++ b/pkg/aflow/flow.go
@@ -63,14 +63,7 @@ func register[Inputs, Outputs any](typ ai.WorkflowType, description string,
_, err := convertFromMap[Inputs](inputs, false, false)
return err
},
- extractOutputs: func(state map[string]any) map[string]any {
- // Ensure that we actually have all outputs.
- tmp, err := convertFromMap[Outputs](state, false, false)
- if err != nil {
- panic(err)
- }
- return convertToMap(tmp)
- },
+ extractOutputs: extractOutputs[Outputs],
}
for _, flow := range flows {
if flow.Name == "" {
diff --git a/pkg/aflow/flow/patching/actions.go b/pkg/aflow/flow/patching/actions.go
index 2c98e9306..d42c24fa1 100644
--- a/pkg/aflow/flow/patching/actions.go
+++ b/pkg/aflow/flow/patching/actions.go
@@ -4,6 +4,8 @@
package patching
import (
+ "errors"
+ "fmt"
"os/exec"
"path/filepath"
"strings"
@@ -108,3 +110,33 @@ func maintainers(ctx *aflow.Context, args maintainersArgs) (maintainersResult, e
}
return res, nil
}
+
+var getRecentCommits = aflow.NewFuncAction("get-recent-commits", recentCommits)
+
+type recentCommitsArgs struct {
+ KernelSrc string
+ KernelCommit string
+ PatchDiff string
+}
+
+type recentCommitsResult struct {
+ RecentCommits string
+}
+
+func recentCommits(ctx *aflow.Context, args recentCommitsArgs) (recentCommitsResult, error) {
+ var res recentCommitsResult
+ var files []string
+ for _, file := range vcs.ParseGitDiff([]byte(args.PatchDiff)) {
+ files = append(files, file.Name)
+ }
+ if len(files) == 0 {
+ return res, aflow.FlowError(errors.New("patch diff does not contain any modified files"))
+ }
+ gitArgs := append([]string{"log", "--format=%s", "--no-merges", "-n", "20", args.KernelCommit}, files...)
+ output, err := osutil.RunCmd(10*time.Minute, args.KernelSrc, "git", gitArgs...)
+ if err != nil {
+ return res, aflow.FlowError(fmt.Errorf("%w\n%s", err, output))
+ }
+ res.RecentCommits = string(output)
+ return res, nil
+}
diff --git a/pkg/aflow/flow/patching/actions_test.go b/pkg/aflow/flow/patching/actions_test.go
new file mode 100644
index 000000000..c6f3011e7
--- /dev/null
+++ b/pkg/aflow/flow/patching/actions_test.go
@@ -0,0 +1,59 @@
+// Copyright 2026 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package patching
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/google/syzkaller/pkg/aflow"
+)
+
+func TestRecentCommits(t *testing.T) {
+ // To avoid creating a fake git repo, we use the syzkaller repo itself.
+ // On CI we have a shallow git checkout that does not have the commit.
+ if os.Getenv("CI") != "" {
+ t.Skip("skipping on CI because of shallow git checkout")
+ }
+ aflow.TestAction(t, getRecentCommits, recentCommitsArgs{
+ KernelSrc: filepath.FromSlash("../../../.."),
+ KernelCommit: "e01a0ca6c12c9851ea7090f13879255ef82291e7",
+ PatchDiff: `
+diff --git a/dashboard/app/ai.go b/dashboard/app/ai.go
+index d4539113c..1d7401e61 100644
+--- a/dashboard/app/ai.go
++++ b/dashboard/app/ai.go
+@@ -1,2 +1,2 @@
+-// Copyright 2025 syzkaller project authors. All rights reserved.
++// Copyright 2026 syzkaller project authors. All rights reserved.
+ // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+diff --git a/syz-cluster/pkg/fuzzconfig/generate.go b/syz-cluster/pkg/fuzzconfig/generate.go
+index fa7d082e6..74ec57b49 100644
+--- a/syz-cluster/pkg/fuzzconfig/generate.go
++++ b/syz-cluster/pkg/fuzzconfig/generate.go
+@@ -1,2 +1,2 @@
+-// Copyright 2025 syzkaller project authors. All rights reserved.
++// Copyright 2026 syzkaller project authors. All rights reserved.
+ // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+`,
+ }, recentCommitsResult{RecentCommits: `dashboard: run patching ai jobs on custom base commits
+dashboard/app: upload AI-generated patches to gerrit
+dashboard: journal user actions on the ai dashboard
+pkg/aflow/trajectory: add token usage
+dashboard/app: add AI job running status
+dashboard: filter AI jobs by workflows
+syz-cluster: disable some trace calls for non-bpf targets
+pkg/aflow: make LLM model per-agent rather than per-flow
+dashboard/app: show crash report on AI job page
+dashboard/app: improve AI UI
+pkg/aflow: allow to specify model per-flow
+dashboard/app: add race harmfullness label
+dashboard/app: add manual AI job triage
+pkg/aflow/flow/assessment: add UAF moderation workflow
+dashboard/app: add support for AI workflows
+syz-cluster: rewrite fuzz config generation
+`,
+ }, "")
+}
diff --git a/pkg/aflow/flow/patching/patching.go b/pkg/aflow/flow/patching/patching.go
index 1906204ec..f8ee75a31 100644
--- a/pkg/aflow/flow/patching/patching.go
+++ b/pkg/aflow/flow/patching/patching.go
@@ -75,6 +75,7 @@ func createPatchingFlow(name string, summaryWindow int) *aflow.Flow {
MaxIterations: 10,
},
getMaintainers,
+ getRecentCommits,
&aflow.LLMAgent{
Name: "description-generator",
Model: aflow.BestExpensiveModel,
@@ -222,6 +223,12 @@ Additional description of the patch:
{{.PatchExplanation}}
+Here are summaries of recent commits that touched the same files.
+Format the summary line consistently with these, look how prefixes
+are specified, letter capitalization, style, etc.
+
+{{.RecentCommits}}
+
{{if titleIsWarning .BugTitle}}
If the patch removes the WARN_ON macro, refer to the fact that WARN_ON
must not be used for conditions that can legitimately happen, and that pr_err
diff --git a/pkg/aflow/func_action.go b/pkg/aflow/func_action.go
index 4b6fe11f1..bcc9e5592 100644
--- a/pkg/aflow/func_action.go
+++ b/pkg/aflow/func_action.go
@@ -5,8 +5,11 @@ package aflow
import (
"maps"
+ "reflect"
+ "testing"
"github.com/google/syzkaller/pkg/aflow/trajectory"
+ "github.com/stretchr/testify/require"
)
func NewFuncAction[Args, Results any](name string, fn func(*Context, Args) (Results, error)) Action {
@@ -44,3 +47,13 @@ func (a *funcAction[Args, Results]) verify(ctx *verifyContext) {
requireInputs[Args](ctx, a.name)
provideOutputs[Results](ctx, a.name)
}
+
+func (a *funcAction[Args, Results]) testVerify(t *testing.T, ctx *verifyContext, args, results any) (
+ map[string]any, map[string]any, func(map[string]any) map[string]any) {
+ require.Equal(t, reflect.TypeFor[Args](), reflect.TypeOf(args))
+ require.Equal(t, reflect.TypeFor[Results](), reflect.TypeOf(results))
+ provideOutputs[Args](ctx, "args")
+ a.verify(ctx)
+ requireInputs[Results](ctx, "results")
+ return convertToMap(args.(Args)), convertToMap(results.(Results)), extractOutputs[Results]
+}
diff --git a/pkg/aflow/schema.go b/pkg/aflow/schema.go
index 8c96578a7..52924d99a 100644
--- a/pkg/aflow/schema.go
+++ b/pkg/aflow/schema.go
@@ -151,6 +151,15 @@ func setField(field reflect.Value, val, f any, name string, tool bool) error {
val, name, f, field.Type().Name())
}
+func extractOutputs[T any](state map[string]any) map[string]any {
+ // Ensure that we actually have all outputs.
+ tmp, err := convertFromMap[T](state, false, false)
+ if err != nil {
+ panic(err)
+ }
+ return convertToMap(tmp)
+}
+
// foreachField iterates over all public fields of the struct provided in data.
func foreachField(data any) iter.Seq2[string, reflect.Value] {
return func(yield func(string, reflect.Value) bool) {
diff --git a/pkg/aflow/test_action.go b/pkg/aflow/test_action.go
new file mode 100644
index 000000000..0bf49ed69
--- /dev/null
+++ b/pkg/aflow/test_action.go
@@ -0,0 +1,41 @@
+// Copyright 2026 syzkaller project authors. All rights reserved.
+// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
+
+package aflow
+
+import (
+ "testing"
+ "time"
+
+ "github.com/google/syzkaller/pkg/aflow/trajectory"
+ "github.com/stretchr/testify/require"
+)
+
+func TestAction(t *testing.T, a Action, initArgs, wantResults any, wantError string) {
+ type tester interface {
+ testVerify(t *testing.T, ctx *verifyContext, args, results any) (
+ map[string]any, map[string]any, func(map[string]any) map[string]any)
+ }
+ vctx := newVerifyContext()
+ args, results, extractOutputs := a.(tester).testVerify(t, vctx, initArgs, wantResults)
+ require.NoError(t, vctx.finalize())
+ // We don't init all fields, init more, if necessary.
+ ctx := &Context{
+ state: args,
+ onEvent: func(*trajectory.Span) error { return nil },
+ stubContext: stubContext{
+ timeNow: time.Now,
+ },
+ }
+ defer ctx.close()
+ err := a.execute(ctx)
+ gotResults := map[string]any{}
+ gotError := ""
+ if err != nil {
+ gotError = err.Error()
+ } else {
+ gotResults = extractOutputs(ctx.state)
+ }
+ require.Equal(t, wantError, gotError)
+ require.Equal(t, results, gotResults)
+}
diff --git a/pkg/aflow/test_tool.go b/pkg/aflow/test_tool.go
index 4b9cb6a4a..a4755d639 100644
--- a/pkg/aflow/test_tool.go
+++ b/pkg/aflow/test_tool.go
@@ -17,9 +17,7 @@ func TestTool(t *testing.T, tool Tool, initState, initArgs, wantResults any, wan
vctx := newVerifyContext()
state, args, results := tool.(toolTester).checkTestTypes(t, vctx, initState, initArgs, wantResults)
tool.verify(vctx)
- if err := vctx.finalize(); err != nil {
- t.Fatal(err)
- }
+ require.NoError(t, vctx.finalize())
// Just ensure it does not crash.
_ = tool.declaration()
// We don't init all fields, init more, if necessary.