aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2026-02-26 16:31:29 +0000
committerDmitry Vyukov <dvyukov@google.com>2026-03-05 16:16:40 +0000
commit140f2ff8e33f036094db3a1eb146120e2fda90d8 (patch)
treeab5455b4820c925790d85b4d41b876faf0d23395
parentd8e5dc287038d84868b039b985a6182b3ad4da25 (diff)
dashboard: automatically create AI repro jobs
If no reproducer has been found for 2 weeks since the moment of the first crash, create a corresponding AI job.
-rw-r--r--dashboard/app/ai.go15
-rw-r--r--dashboard/app/ai_test.go39
2 files changed, 51 insertions, 3 deletions
diff --git a/dashboard/app/ai.go b/dashboard/app/ai.go
index c5dba6922..68719b723 100644
--- a/dashboard/app/ai.go
+++ b/dashboard/app/ai.go
@@ -589,7 +589,7 @@ func aiBugWorkflows(ctx context.Context, bug *Bug) ([]*uiWorkflow, error) {
if err != nil {
return nil, err
}
- applicable := workflowsForBug(bug, true)
+ applicable := workflowsForBug(ctx, bug, true)
var result []*uiWorkflow
for _, flow := range workflows {
// Also check that the workflow is active on some syz-agent's.
@@ -670,6 +670,12 @@ func bugJobCreate(ctx context.Context, workflow string, typ ai.WorkflowType, bug
})
}
+const (
+ // In how many days after a bug is reported we create the first AI repro job
+ // (provided no reproducer has been found in the meanwhile).
+ aiReproTriggerDays = 14
+)
+
// autoCreateAIJobs incrementally creates AI jobs for existing bugs, returns if any new jobs were created.
//
// The idea is as follows. We have a predicate (workflowsForBug) which says what workflows need to be
@@ -721,7 +727,7 @@ func autoCreateAIJobs(ctx context.Context) (bool, error) {
}
func autoCreateAIJob(ctx context.Context, bug *Bug, bugKey *db.Key) (bool, error) {
- workflows := workflowsForBug(bug, false)
+ workflows := workflowsForBug(ctx, bug, false)
if len(workflows) == 0 {
return false, nil
}
@@ -770,7 +776,7 @@ func autoCreateAIJob(ctx context.Context, bug *Bug, bugKey *db.Key) (bool, error
return len(workflows) != 0, nil
}
-func workflowsForBug(bug *Bug, manual bool) map[ai.WorkflowType]bool {
+func workflowsForBug(ctx context.Context, bug *Bug, manual bool) map[ai.WorkflowType]bool {
workflows := make(map[ai.WorkflowType]bool)
typ := crash.TitleToType(bug.Title)
// UAF bugs stuck in last but one reporting.
@@ -782,6 +788,9 @@ func workflowsForBug(bug *Bug, manual bool) map[ai.WorkflowType]bool {
if typ == crash.KCSANDataRace {
workflows[ai.WorkflowAssessmentKCSAN] = true
}
+ if bug.ReproLevel == dashapi.ReproLevelNone && timeSince(ctx, bug.FirstTime) > aiReproTriggerDays*24*time.Hour {
+ workflows[ai.WorkflowRepro] = true
+ }
if manual {
// Types we don't create automatically yet, but can be created manually.
if typ.IsUAF() {
diff --git a/dashboard/app/ai_test.go b/dashboard/app/ai_test.go
index c99491781..e3cd108f0 100644
--- a/dashboard/app/ai_test.go
+++ b/dashboard/app/ai_test.go
@@ -530,3 +530,42 @@ func TestAIJobAutoCreate(t *testing.T) {
pollResp7, _ := c.agentClient.AIJobPoll(pollReq)
require.Equal(t, pollResp7.ID, "")
}
+
+func TestAIRepro(t *testing.T) {
+ c := NewSpannerCtx(t)
+ defer c.Close()
+
+ build := testBuild(1)
+ c.aiClient.UploadBuild(build)
+ crash := testCrash(build, 1)
+ c.aiClient.ReportCrash(crash)
+ c.aiClient.pollEmailExtID()
+
+ pollReq := &dashapi.AIJobPollReq{
+ CodeRevision: prog.GitRevision,
+ Workflows: []dashapi.AIWorkflow{
+ {Type: ai.WorkflowRepro, Name: string(ai.WorkflowRepro)},
+ },
+ }
+ // No job should be created initially.
+ pollResp0, _ := c.agentClient.AIJobPoll(pollReq)
+ require.Equal(t, pollResp0.ID, "")
+
+ c.advanceTime(15 * 24 * time.Hour)
+ pollResp2, _ := c.agentClient.AIJobPoll(pollReq)
+ require.NotEqual(t, pollResp2.ID, "")
+
+ c.agentClient.AIJobDone(&dashapi.AIJobDoneReq{
+ ID: pollResp2.ID,
+ })
+
+ // A second bug with a repro.
+ crash2 := testCrashWithRepro(build, 2)
+ c.aiClient.ReportCrash(crash2)
+ c.aiClient.pollEmailExtID()
+
+ c.advanceTime(15 * 24 * time.Hour)
+ // No AI job should be created.
+ pollResp4, _ := c.agentClient.AIJobPoll(pollReq)
+ require.Equal(t, pollResp4.ID, "")
+}