diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2026-01-30 20:25:26 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2026-01-31 16:07:13 +0000 |
| commit | 3576455960ee88cefa43cad0bdfd1458549569b9 (patch) | |
| tree | b0943ccce2feb664e2a30dd2462d99cf13fc4bf7 /pkg | |
| parent | afcca7fa917427568d76a8295ff9f1e88824c1fe (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.go | 9 | ||||
| -rw-r--r-- | pkg/aflow/flow/patching/actions.go | 32 | ||||
| -rw-r--r-- | pkg/aflow/flow/patching/actions_test.go | 59 | ||||
| -rw-r--r-- | pkg/aflow/flow/patching/patching.go | 7 | ||||
| -rw-r--r-- | pkg/aflow/func_action.go | 13 | ||||
| -rw-r--r-- | pkg/aflow/schema.go | 9 | ||||
| -rw-r--r-- | pkg/aflow/test_action.go | 41 | ||||
| -rw-r--r-- | pkg/aflow/test_tool.go | 4 |
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. |
