[gold] /json/v2/search RPC: Exclude digests with optional key disallow_triaging=true from BulkTriageDeltaInfos.

For CLs, this change adds the overhead of looking up the optional keys for each extendedBulkTriageDeltaInfo. I don't expect this to add significant latency to the RPC because it uses the options cache[1], and because the query for cache misses should be fast.

For the primary branch, we have the additional step of fetching the optionIDs for each digest. This is done via a single query against the TraceValues table (inspired in [2]). I expect this query to be pretty fast because the WHERE clause leverages the table's index.

[1] https://skia.googlesource.com/buildbot/+/264f3ee077677f5eddf47808a3f33987322f05a6/golden/go/search/search.go#1627
[2] https://skia.googlesource.com/buildbot/+/264f3ee077677f5eddf47808a3f33987322f05a6/golden/go/search/search.go#1818

Bug: skia:14033
Change-Id: I55765b64308e826b949e87e06ecc9b5fc3297d42
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/658056
Commit-Queue: Leandro Lovisolo <lovisolo@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/golden/go/search/search.go b/golden/go/search/search.go
index 265b291..6602261 100644
--- a/golden/go/search/search.go
+++ b/golden/go/search/search.go
@@ -743,15 +743,20 @@
 		}
 	}
 
-	bulkTriageDeltaInfos := make([]frontend.BulkTriageDeltaInfo, len(extendedBulkTriageDeltaInfos))
-	for i, triageDeltaInfo := range extendedBulkTriageDeltaInfos {
-		bulkTriageDeltaInfos[i] = triageDeltaInfo.BulkTriageDeltaInfo
+	// Populate the optionsIDs fields of each extendedBulkTriageDeltaInfo.
+	if err := s.populateExtendedBulkTriageDeltaInfosOptionsIDs(ctx, extendedBulkTriageDeltaInfos); err != nil {
+		return nil, skerr.Wrap(err)
+	}
+
+	bulkTriageDeltaInfos, err := s.prepareExtendedBulkTriageDeltaInfosForFrontend(ctx, extendedBulkTriageDeltaInfos)
+	if err != nil {
+		return nil, skerr.Wrap(err)
 	}
 
 	return &frontend.SearchResponse{
 		Results:              results,
 		Offset:               q.Offset,
-		Size:                 len(bulkTriageDeltaInfos),
+		Size:                 len(extendedBulkTriageDeltaInfos),
 		BulkTriageDeltaInfos: bulkTriageDeltaInfos,
 		Commits:              commits,
 	}, nil
@@ -1153,8 +1158,10 @@
 type extendedBulkTriageDeltaInfo struct {
 	frontend.BulkTriageDeltaInfo
 
+	traceIDs   []schema.TraceID
 	groupingID schema.GroupingID
 	digest     schema.DigestBytes
+	optionsIDs []schema.OptionsID // Will be set for CL data only.
 }
 
 // getClosestDiffs returns information about the closest triaged digests for each result in the
@@ -1266,8 +1273,10 @@
 				Grouping: grouping,
 				Digest:   types.Digest(hex.EncodeToString(s2.leftDigest)),
 			},
+			traceIDs:   s2.traceIDs,
 			groupingID: s2.groupingID,
 			digest:     s2.leftDigest,
+			optionsIDs: s2.optionsIDs,
 		}
 		if s2.closestDigest != nil {
 			// Apply RGBA Filter here - if the closest digest isn't within range, we remove it.
@@ -2110,6 +2119,78 @@
 	return nil
 }
 
+type traceDigestKey struct {
+	traceID schema.MD5Hash
+	digest  schema.MD5Hash
+}
+
+// populateExtendedBulkTriageDeltaInfosOptionsIDs populates the optionsIDs field of the given
+// extendedBulkTriageDeltaInfos.
+func (s *Impl) populateExtendedBulkTriageDeltaInfosOptionsIDs(ctx context.Context, triageDeltaInfos []extendedBulkTriageDeltaInfo) error {
+	// Map triageDeltaInfos by trace ID and digest for faster querying, and gather all trace IDs.
+	var allTraceIDs []schema.TraceID
+	triageDeltaInfosByTraceAndDigest := map[traceDigestKey]*extendedBulkTriageDeltaInfo{}
+	for i := range triageDeltaInfos {
+		allTraceIDs = append(allTraceIDs, triageDeltaInfos[i].traceIDs...)
+		for _, traceID := range triageDeltaInfos[i].traceIDs {
+			key := traceDigestKey{
+				traceID: sql.AsMD5Hash(traceID),
+				digest:  sql.AsMD5Hash(triageDeltaInfos[i].digest),
+			}
+			triageDeltaInfosByTraceAndDigest[key] = &triageDeltaInfos[i]
+		}
+	}
+
+	const statement = `SELECT trace_id, digest, options_id FROM TraceValues
+	WHERE trace_id = ANY($1) AND commit_id >= $2`
+	rows, err := s.db.Query(ctx, statement, allTraceIDs, getFirstCommitID(ctx))
+	if err != nil {
+		return skerr.Wrap(err)
+	}
+	defer rows.Close()
+
+	var traceID schema.TraceID
+	var digest schema.DigestBytes
+	var optionsID schema.OptionsID
+	for rows.Next() {
+		if err := rows.Scan(&traceID, &digest, &optionsID); err != nil {
+			return skerr.Wrap(err)
+		}
+		key := traceDigestKey{
+			traceID: sql.AsMD5Hash(traceID),
+			digest:  sql.AsMD5Hash(digest),
+		}
+		if triageDeltaInfo, ok := triageDeltaInfosByTraceAndDigest[key]; ok {
+			triageDeltaInfo.optionsIDs = append(triageDeltaInfo.optionsIDs, optionsID)
+		}
+	}
+	return nil
+}
+
+// prepareExtendedBulkTriageDeltaInfosForFrontend turns extendedBulkTriageDeltaInfo structs into
+// frontend.BulkTriageDeltaInfo structs, and filters out those with disallow_triaging=true.
+func (s *Impl) prepareExtendedBulkTriageDeltaInfosForFrontend(ctx context.Context, extendedBulkTriageDeltaInfos []extendedBulkTriageDeltaInfo) ([]frontend.BulkTriageDeltaInfo, error) {
+	// The frontend expects a non-null array.
+	bulkTriageDeltaInfos := []frontend.BulkTriageDeltaInfo{}
+	for _, triageDeltaInfo := range extendedBulkTriageDeltaInfos {
+		disallowTriaging := false
+		for _, optionsID := range triageDeltaInfo.optionsIDs {
+			options, err := s.expandOptionsToParams(ctx, optionsID)
+			if err != nil {
+				return nil, skerr.Wrap(err)
+			}
+			if options["disallow_triaging"] == "true" {
+				disallowTriaging = true
+				break
+			}
+		}
+		if !disallowTriaging {
+			bulkTriageDeltaInfos = append(bulkTriageDeltaInfos, triageDeltaInfo.BulkTriageDeltaInfo)
+		}
+	}
+	return bulkTriageDeltaInfos, nil
+}
+
 // expandGrouping returns the params associated with the grouping id. It will use the cache - if
 // there is a cache miss, it will look it up, add it to the cache and return it.
 func (s *Impl) expandGrouping(ctx context.Context, groupingID schema.MD5Hash) (paramtools.Params, error) {
@@ -2233,15 +2314,15 @@
 		}
 	}
 
-	bulkTriageDeltaInfos := make([]frontend.BulkTriageDeltaInfo, len(extendedBulkTriageDeltaInfos))
-	for i, triageDeltaInfo := range extendedBulkTriageDeltaInfos {
-		bulkTriageDeltaInfos[i] = triageDeltaInfo.BulkTriageDeltaInfo
+	bulkTriageDeltaInfos, err := s.prepareExtendedBulkTriageDeltaInfosForFrontend(ctx, extendedBulkTriageDeltaInfos)
+	if err != nil {
+		return nil, skerr.Wrap(err)
 	}
 
 	return &frontend.SearchResponse{
 		Results:              results,
 		Offset:               getQuery(ctx).Offset,
-		Size:                 len(bulkTriageDeltaInfos),
+		Size:                 len(extendedBulkTriageDeltaInfos),
 		BulkTriageDeltaInfos: bulkTriageDeltaInfos,
 		Commits:              commits,
 	}, nil
diff --git a/golden/go/search/search_test.go b/golden/go/search/search_test.go
index daae996..d1bd220 100644
--- a/golden/go/search/search_test.go
+++ b/golden/go/search/search_test.go
@@ -1311,20 +1311,12 @@
 					types.CorpusField:     dks.CornersCorpus,
 					types.PrimaryKeyField: dks.TriangleTest,
 				},
-				Digest:                     dks.DigestB01Pos,
-				LabelBefore:                expectations.Positive,
-				ClosestDiffLabel:           frontend.ClosestDiffLabelPositive,
-				InCurrentSearchResultsPage: true,
-			}, {
-				Grouping: paramtools.Params{
-					types.CorpusField:     dks.CornersCorpus,
-					types.PrimaryKeyField: dks.TriangleTest,
-				},
 				Digest:                     dks.DigestB02Pos,
 				LabelBefore:                expectations.Positive,
 				ClosestDiffLabel:           frontend.ClosestDiffLabelPositive,
 				InCurrentSearchResultsPage: false,
 			},
+			// dks.DigestB01Pos is excluded because it has optional key disallow_triaging=true.
 		},
 	}, res)
 }
@@ -3206,6 +3198,384 @@
 	}, res)
 }
 
+func TestSearch_DisallowTriagingOnPrimaryBranch_DigestExcludedFromBulkTriageInfos(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	db := useKitchenSinkData(ctx, t)
+
+	s := New(db, 100)
+	require.NoError(t, s.StartCacheProcess(ctx, time.Minute, 100))
+	res, err := s.Search(ctx, &query.Search{
+		IncludePositiveDigests:  true,
+		IncludeNegativeDigests:  true,
+		IncludeUntriagedDigests: true,
+		IncludeIgnoredTraces:    true,
+		Sort:                    query.SortDescending,
+		TraceValues: paramtools.ParamSet{
+			types.CorpusField: []string{dks.CornersCorpus},
+			dks.OSKey:         []string{dks.AndroidOS},
+			dks.DeviceKey:     []string{dks.TaimenDevice},
+			dks.ColorModeKey:  []string{dks.RGBColorMode},
+		},
+		RGBAMinFilter: 0,
+		RGBAMaxFilter: 255,
+	})
+	require.NoError(t, err)
+
+	assert.Equal(t, &frontend.SearchResponse{
+		Results: []*frontend.SearchResult{{
+			Digest: dks.DigestA09Neg,
+			Test:   dks.SquareTest,
+			Status: expectations.Negative,
+			ParamSet: paramtools.ParamSet{
+				dks.ColorModeKey:      []string{dks.RGBColorMode},
+				types.CorpusField:     []string{dks.CornersCorpus},
+				dks.DeviceKey:         []string{dks.TaimenDevice},
+				dks.OSKey:             []string{dks.AndroidOS},
+				types.PrimaryKeyField: []string{dks.SquareTest},
+				"ext":                 []string{"png"},
+			},
+			TriageHistory: []frontend.TriageHistory{
+				{
+					User: dks.UserFour,
+					TS:   time.Date(2020, time.December, 11, 13, 0, 0, 0, time.UTC),
+				},
+			},
+			TraceGroup: frontend.TraceGroup{
+				Traces: []frontend.Trace{{
+					ID:            "0e87221433a6de545e32d846fd7c3e6c",
+					DigestIndices: []int{-1, -1, -1, -1, -1, -1, 0, 0, 1, 0},
+					Params: paramtools.Params{
+						dks.ColorModeKey:      dks.RGBColorMode,
+						types.CorpusField:     dks.CornersCorpus,
+						dks.DeviceKey:         dks.TaimenDevice,
+						dks.OSKey:             dks.AndroidOS,
+						types.PrimaryKeyField: dks.SquareTest,
+						"ext":                 "png",
+					},
+				}},
+				Digests: []frontend.DigestStatus{
+					{Digest: dks.DigestA09Neg, Status: expectations.Negative},
+					{Digest: dks.DigestA01Pos, Status: expectations.Positive},
+				},
+				TotalDigests: 2,
+			},
+			RefDiffs: map[frontend.RefClosest]*frontend.SRDiffDigest{
+				frontend.PositiveRef: {
+					CombinedMetric: 10, QueryMetric: 10, PixelDiffPercent: 100, NumDiffPixels: 64,
+					MaxRGBADiffs: [4]int{255, 255, 255, 255},
+					DimDiffer:    false,
+					Digest:       dks.DigestA01Pos,
+					Status:       expectations.Positive,
+					ParamSet: paramtools.ParamSet{
+						dks.ColorModeKey:             []string{dks.RGBColorMode},
+						types.CorpusField:            []string{dks.CornersCorpus},
+						dks.DeviceKey:                []string{dks.QuadroDevice, dks.IPadDevice, dks.IPhoneDevice, dks.TaimenDevice, dks.WalleyeDevice},
+						dks.OSKey:                    []string{dks.AndroidOS, dks.Windows10dot2OS, dks.Windows10dot3OS, dks.IOS},
+						types.PrimaryKeyField:        []string{dks.SquareTest},
+						"ext":                        []string{"png"},
+						"image_matching_algorithm":   []string{"fuzzy"},
+						"fuzzy_max_different_pixels": []string{"2"},
+					},
+				},
+				frontend.NegativeRef: nil,
+			},
+			ClosestRef: frontend.PositiveRef,
+		}, {
+			Digest: dks.DigestB01Pos,
+			Test:   dks.TriangleTest,
+			Status: expectations.Positive,
+			ParamSet: paramtools.ParamSet{
+				dks.ColorModeKey:           []string{dks.RGBColorMode},
+				types.CorpusField:          []string{dks.CornersCorpus},
+				dks.DeviceKey:              []string{dks.TaimenDevice},
+				dks.OSKey:                  []string{dks.AndroidOS},
+				types.PrimaryKeyField:      []string{dks.TriangleTest},
+				"ext":                      []string{"png"},
+				"image_matching_algorithm": []string{"positive_if_only_image"},
+				"disallow_triaging":        []string{"true"},
+			},
+			TriageHistory: []frontend.TriageHistory{
+				{
+					User: dks.UserOne,
+					TS:   time.Date(2020, time.June, 7, 8, 9, 43, 0, time.UTC),
+				},
+			},
+			TraceGroup: frontend.TraceGroup{
+				Traces: []frontend.Trace{{
+					ID:            "1a16cbc8805378f0a6ef654a035d86c4",
+					DigestIndices: []int{-1, -1, -1, -1, -1, -1, 0, 0, 0, 0},
+					Params: paramtools.Params{
+						dks.ColorModeKey:           dks.RGBColorMode,
+						types.CorpusField:          dks.CornersCorpus,
+						dks.DeviceKey:              dks.TaimenDevice,
+						dks.OSKey:                  dks.AndroidOS,
+						types.PrimaryKeyField:      dks.TriangleTest,
+						"ext":                      "png",
+						"image_matching_algorithm": "positive_if_only_image",
+						"disallow_triaging":        "true",
+					},
+				}},
+				Digests: []frontend.DigestStatus{
+					{Digest: dks.DigestB01Pos, Status: expectations.Positive},
+				},
+				TotalDigests: 1,
+			},
+			RefDiffs: map[frontend.RefClosest]*frontend.SRDiffDigest{
+				frontend.PositiveRef: {
+					CombinedMetric: 1.9362538, QueryMetric: 1.9362538, PixelDiffPercent: 43.75, NumDiffPixels: 28,
+					MaxRGBADiffs: [4]int{11, 5, 42, 0},
+					DimDiffer:    false,
+					Digest:       dks.DigestB02Pos,
+					Status:       expectations.Positive,
+					ParamSet: paramtools.ParamSet{
+						dks.ColorModeKey:      []string{dks.GreyColorMode},
+						types.CorpusField:     []string{dks.CornersCorpus},
+						dks.DeviceKey:         []string{dks.QuadroDevice, dks.IPadDevice, dks.IPhoneDevice, dks.WalleyeDevice},
+						dks.OSKey:             []string{dks.AndroidOS, dks.Windows10dot2OS, dks.Windows10dot3OS, dks.IOS},
+						types.PrimaryKeyField: []string{dks.TriangleTest},
+						"ext":                 []string{"png"},
+					},
+				},
+				frontend.NegativeRef: {
+					CombinedMetric: 2.9445405, QueryMetric: 2.9445405, PixelDiffPercent: 10.9375, NumDiffPixels: 7,
+					MaxRGBADiffs: [4]int{250, 244, 197, 51},
+					DimDiffer:    false,
+					Digest:       dks.DigestB03Neg,
+					Status:       expectations.Negative,
+					ParamSet: paramtools.ParamSet{
+						dks.ColorModeKey:      []string{dks.RGBColorMode},
+						types.CorpusField:     []string{dks.CornersCorpus},
+						dks.DeviceKey:         []string{dks.IPadDevice, dks.IPhoneDevice},
+						dks.OSKey:             []string{dks.IOS},
+						types.PrimaryKeyField: []string{dks.TriangleTest},
+						"ext":                 []string{"png"},
+					},
+				},
+			},
+			ClosestRef: frontend.PositiveRef,
+		}, {
+			Digest: dks.DigestA01Pos,
+			Test:   dks.SquareTest,
+			Status: expectations.Positive,
+			ParamSet: paramtools.ParamSet{
+				dks.ColorModeKey:      []string{dks.RGBColorMode},
+				types.CorpusField:     []string{dks.CornersCorpus},
+				dks.DeviceKey:         []string{dks.TaimenDevice},
+				dks.OSKey:             []string{dks.AndroidOS},
+				types.PrimaryKeyField: []string{dks.SquareTest},
+				"ext":                 []string{"png"},
+			},
+			TriageHistory: []frontend.TriageHistory{
+				{
+					User: dks.UserOne,
+					TS:   time.Date(2020, time.June, 7, 8, 23, 8, 0, time.UTC),
+				},
+			},
+			TraceGroup: frontend.TraceGroup{
+				Traces: []frontend.Trace{{
+					ID:            "0e87221433a6de545e32d846fd7c3e6c",
+					DigestIndices: []int{-1, -1, -1, -1, -1, -1, 1, 1, 0, 1},
+					Params: paramtools.Params{
+						dks.ColorModeKey:      dks.RGBColorMode,
+						types.CorpusField:     dks.CornersCorpus,
+						dks.DeviceKey:         dks.TaimenDevice,
+						dks.OSKey:             dks.AndroidOS,
+						types.PrimaryKeyField: dks.SquareTest,
+						"ext":                 "png",
+					},
+				}},
+				Digests: []frontend.DigestStatus{
+					{Digest: dks.DigestA01Pos, Status: expectations.Positive},
+					{Digest: dks.DigestA09Neg, Status: expectations.Negative},
+				},
+				TotalDigests: 2,
+			},
+			RefDiffs: map[frontend.RefClosest]*frontend.SRDiffDigest{
+				frontend.PositiveRef: {
+					CombinedMetric: 0.15655607, QueryMetric: 0.15655607, PixelDiffPercent: 3.125, NumDiffPixels: 2,
+					MaxRGBADiffs: [4]int{4, 0, 0, 0},
+					DimDiffer:    false,
+					Digest:       dks.DigestA08Pos,
+					Status:       expectations.Positive,
+					ParamSet: paramtools.ParamSet{
+						dks.ColorModeKey:             []string{dks.RGBColorMode},
+						types.CorpusField:            []string{dks.CornersCorpus},
+						dks.DeviceKey:                []string{dks.WalleyeDevice},
+						dks.OSKey:                    []string{dks.AndroidOS},
+						types.PrimaryKeyField:        []string{dks.SquareTest},
+						"ext":                        []string{"png"},
+						"image_matching_algorithm":   []string{"fuzzy"},
+						"fuzzy_max_different_pixels": []string{"2"},
+					},
+				},
+				frontend.NegativeRef: {
+					CombinedMetric: 10, QueryMetric: 10, PixelDiffPercent: 100, NumDiffPixels: 64,
+					MaxRGBADiffs: [4]int{255, 255, 255, 255},
+					DimDiffer:    false,
+					Digest:       dks.DigestA09Neg,
+					Status:       expectations.Negative,
+					ParamSet: paramtools.ParamSet{
+						dks.ColorModeKey:      []string{dks.RGBColorMode},
+						types.CorpusField:     []string{dks.CornersCorpus},
+						dks.DeviceKey:         []string{dks.TaimenDevice},
+						dks.OSKey:             []string{dks.AndroidOS},
+						types.PrimaryKeyField: []string{dks.SquareTest},
+						"ext":                 []string{"png"},
+					},
+				},
+			},
+			ClosestRef: frontend.PositiveRef,
+		}},
+		Offset:  0,
+		Size:    3,
+		Commits: kitchenSinkCommits,
+		BulkTriageDeltaInfos: []frontend.BulkTriageDeltaInfo{
+			{
+				Grouping: paramtools.Params{
+					"name":        "square",
+					"source_type": "corners",
+				},
+				Digest:                     dks.DigestA01Pos,
+				LabelBefore:                expectations.Positive,
+				ClosestDiffLabel:           frontend.ClosestDiffLabelPositive,
+				InCurrentSearchResultsPage: true,
+			}, {
+				Grouping: paramtools.Params{
+					"name":        "square",
+					"source_type": "corners",
+				},
+				Digest:                     dks.DigestA09Neg,
+				LabelBefore:                expectations.Negative,
+				ClosestDiffLabel:           frontend.ClosestDiffLabelPositive,
+				InCurrentSearchResultsPage: true,
+			},
+			// dks.DigestB01Pos is excluded because it has optional key disallow_triaging=true.
+		},
+	}, res)
+}
+
+func TestSearch_DisallowTriagingOnCL_DigestExcludedFromBulkTriageInfos(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	db := useKitchenSinkData(ctx, t)
+
+	s := New(db, 100)
+	s.SetReviewSystemTemplates(map[string]string{
+		dks.GerritCRS:         "http://example.com/public/%s",
+		dks.GerritInternalCRS: "http://example.com/internal/%s",
+	})
+	require.NoError(t, s.StartCacheProcess(ctx, time.Minute, 100))
+	res, err := s.Search(ctx, &query.Search{
+		IncludePositiveDigests:  true,
+		IncludeNegativeDigests:  true,
+		IncludeUntriagedDigests: true,
+		Sort:                    query.SortDescending,
+		IncludeIgnoredTraces:    false,
+		TraceValues: paramtools.ParamSet{
+			types.CorpusField: []string{dks.CornersCorpus},
+		},
+		RGBAMinFilter:                  0,
+		RGBAMaxFilter:                  255,
+		ChangelistID:                   dks.ChangelistIDWithDisallowTriagingTest,
+		CodeReviewSystemID:             dks.GerritCRS,
+		Patchsets:                      []int64{1},
+		IncludeDigestsProducedOnMaster: false,
+	})
+	require.NoError(t, err)
+	clCommits := append([]frontend.Commit{}, kitchenSinkCommits...)
+	clCommits = append(clCommits, frontend.Commit{
+		CommitTime:    time.Date(2020, time.December, 12, 16, 0, 0, 0, time.UTC).Unix(),
+		Hash:          dks.ChangelistIDWithDisallowTriagingTest,
+		Author:        dks.UserOne,
+		Subject:       "add test with disallow triaging",
+		ChangelistURL: "http://example.com/public/CLdisallowtriaging",
+	})
+
+	assert.Equal(t, &frontend.SearchResponse{
+		Results: []*frontend.SearchResult{{
+			Digest: dks.DigestB05Pos_CL,
+			Test:   dks.TriangleTest,
+			Status: expectations.Positive,
+			ParamSet: paramtools.ParamSet{
+				dks.ColorModeKey:           []string{dks.GreyColorMode},
+				types.CorpusField:          []string{dks.CornersCorpus},
+				dks.DeviceKey:              []string{dks.TaimenDevice},
+				dks.OSKey:                  []string{dks.AndroidOS},
+				types.PrimaryKeyField:      []string{dks.TriangleTest},
+				"ext":                      []string{"png"},
+				"disallow_triaging":        []string{"true"},
+				"image_matching_algorithm": []string{"positive_if_only_image"},
+			},
+			TriageHistory: []frontend.TriageHistory{
+				{
+					User: dks.UserOne,
+					TS:   time.Date(2020, time.December, 12, 17, 0, 0, 0, time.UTC),
+				},
+			},
+			TraceGroup: frontend.TraceGroup{
+				Traces: []frontend.Trace{{
+					ID:            "668120894a9cfd4d028dbed05c245838",
+					DigestIndices: []int{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0},
+					Params: paramtools.Params{
+						dks.ColorModeKey:           dks.GreyColorMode,
+						types.CorpusField:          dks.CornersCorpus,
+						dks.DeviceKey:              dks.TaimenDevice,
+						dks.OSKey:                  dks.AndroidOS,
+						types.PrimaryKeyField:      dks.TriangleTest,
+						"ext":                      "png",
+						"disallow_triaging":        "true",
+						"image_matching_algorithm": "positive_if_only_image",
+					},
+				}},
+				Digests: []frontend.DigestStatus{
+					{Digest: dks.DigestB05Pos_CL, Status: expectations.Positive},
+				},
+				TotalDigests: 1,
+			},
+			RefDiffs: map[frontend.RefClosest]*frontend.SRDiffDigest{
+				frontend.PositiveRef: {
+					CombinedMetric: 0.73570776, QueryMetric: 0.73570776, PixelDiffPercent: 9.375, NumDiffPixels: 6,
+					MaxRGBADiffs: [4]int{17, 17, 17, 0},
+					DimDiffer:    false,
+					Digest:       dks.DigestB02Pos,
+					Status:       expectations.Positive,
+					ParamSet: paramtools.ParamSet{
+						dks.ColorModeKey:      []string{dks.GreyColorMode},
+						types.CorpusField:     []string{dks.CornersCorpus},
+						dks.DeviceKey:         []string{dks.QuadroDevice, dks.IPadDevice, dks.IPhoneDevice, dks.WalleyeDevice},
+						dks.OSKey:             []string{dks.AndroidOS, dks.Windows10dot2OS, dks.Windows10dot3OS, dks.IOS},
+						types.PrimaryKeyField: []string{dks.TriangleTest},
+						"ext":                 []string{"png"},
+					},
+				},
+				frontend.NegativeRef: {
+					CombinedMetric: 6.2521544, QueryMetric: 6.2521544, PixelDiffPercent: 53.125, NumDiffPixels: 34,
+					MaxRGBADiffs: [4]int{233, 227, 180, 51},
+					DimDiffer:    false,
+					Digest:       dks.DigestB03Neg,
+					Status:       expectations.Negative,
+					ParamSet: paramtools.ParamSet{
+						dks.ColorModeKey:      []string{dks.RGBColorMode},
+						types.CorpusField:     []string{dks.CornersCorpus},
+						dks.DeviceKey:         []string{dks.IPadDevice, dks.IPhoneDevice},
+						dks.OSKey:             []string{dks.IOS},
+						types.PrimaryKeyField: []string{dks.TriangleTest},
+						"ext":                 []string{"png"},
+					},
+				},
+			},
+			ClosestRef: frontend.PositiveRef,
+		}},
+		Offset:               0,
+		Size:                 1,
+		Commits:              clCommits,
+		BulkTriageDeltaInfos: []frontend.BulkTriageDeltaInfo{
+			// dks.DigestB05Pos_CL is excluded because it has optional key disallow_triaging=true.
+		},
+	}, res)
+}
+
 func TestSearch_CLAndPatchsetWithMultipleDatapointsOnSameTrace_ReturnsAllDatapoints(t *testing.T) {
 
 	ctx, cancel := context.WithCancel(context.Background())
@@ -3791,16 +4161,8 @@
 				LabelBefore:                expectations.Positive,
 				ClosestDiffLabel:           frontend.ClosestDiffLabelPositive,
 				InCurrentSearchResultsPage: true,
-			}, {
-				Grouping: paramtools.Params{
-					types.CorpusField:     dks.CornersCorpus,
-					types.PrimaryKeyField: dks.TriangleTest,
-				},
-				Digest:                     dks.DigestB01Pos,
-				LabelBefore:                expectations.Untriaged,
-				ClosestDiffLabel:           frontend.ClosestDiffLabelPositive,
-				InCurrentSearchResultsPage: true,
 			},
+			// dks.DigestB01Pos is excluded because it has optional key disallow_triaging=true.
 		},
 	}, res)
 }