internal/worker: store CVERecord change history

Change-Id: I68c53b263e6c801d9ec4bdb5b34cfac6da2841eb
Reviewed-on: https://go-review.googlesource.com/c/vuln/+/370837
Trust: Jonathan Amsterdam <jba@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Julie Qiu <julie@golang.org>
diff --git a/internal/worker/store/store.go b/internal/worker/store/store.go
index be99c49..c916a99 100644
--- a/internal/worker/store/store.go
+++ b/internal/worker/store/store.go
@@ -45,6 +45,10 @@
 	// IssueCreatedAt is the time when the issue was created.
 	// Set only after a GitHub issue has been successfully created.
 	IssueCreatedAt time.Time
+
+	// History holds previous states of a CVERecord,
+	// from most to least recent.
+	History []*CVERecordSnapshot
 }
 
 // Validate returns an error if the CVERecord is not valid.
@@ -99,6 +103,24 @@
 	}
 }
 
+// CVERecordSnapshot holds a previous state of a CVERecord.
+// The fields mean the same as those of CVERecord.
+type CVERecordSnapshot struct {
+	CommitHash        string
+	CVEState          string
+	TriageState       TriageState
+	TriageStateReason string
+}
+
+func (r *CVERecord) Snapshot() *CVERecordSnapshot {
+	return &CVERecordSnapshot{
+		CommitHash:        r.CommitHash,
+		CVEState:          r.CVEState,
+		TriageState:       r.TriageState,
+		TriageStateReason: r.TriageStateReason,
+	}
+}
+
 // A CommitUpdateRecord describes a single update operation, which reconciles
 // a commit in the CVE list repo with the DB state.
 type CommitUpdateRecord struct {
diff --git a/internal/worker/update.go b/internal/worker/update.go
index b9724f3..7c85a34 100644
--- a/internal/worker/update.go
+++ b/internal/worker/update.go
@@ -316,10 +316,13 @@
 			mp = result.modulePath
 		}
 		mod.TriageStateReason = fmt.Sprintf("CVE changed; affected module = %q", mp)
-		// TODO(golang/go#49733): keep a history of the previous states and their commits.
 	default:
 		return false, fmt.Errorf("unknown TriageState: %q", old.TriageState)
 	}
+	// If the triage state changed, add the old state to the history at the beginning.
+	if old.TriageState != mod.TriageState {
+		mod.History = append([]*store.CVERecordSnapshot{old.Snapshot()}, mod.History...)
+	}
 	// If we're here, then mod is a modification to the DB.
 	if err := tx.SetCVERecord(&mod); err != nil {
 		return false, err
diff --git a/internal/worker/update_test.go b/internal/worker/update_test.go
index af765ee..d577289 100644
--- a/internal/worker/update_test.go
+++ b/internal/worker/update_test.go
@@ -83,6 +83,9 @@
 	if !m.IssueCreatedAt.IsZero() {
 		panic("unsupported modification")
 	}
+	if m.History != nil {
+		c.History = m.History
+	}
 	return &c
 }
 
@@ -182,10 +185,21 @@
 				}),
 			},
 			want: []*store.CVERecord{
-				rs[0],
+				modify(rs[0], &store.CVERecord{
+					History: []*store.CVERecordSnapshot{{
+						CommitHash:  commitHash,
+						CVEState:    cveschema.StatePublic,
+						TriageState: store.TriageStateNoActionNeeded,
+					}},
+				}),
 				modify(rs[1], &store.CVERecord{
 					Module: clearString,
 					CVE:    clearCVE,
+					History: []*store.CVERecordSnapshot{{
+						CommitHash:  commitHash,
+						CVEState:    cveschema.StateReserved,
+						TriageState: store.TriageStateNeedsIssue,
+					}},
 				}),
 				rs[2],
 				rs[3],
@@ -208,6 +222,11 @@
 				modify(rs[0], &store.CVERecord{
 					TriageState:       store.TriageStateUpdatedSinceIssueCreation,
 					TriageStateReason: `CVE changed; affected module = "golang.org/x/mod"`,
+					History: []*store.CVERecordSnapshot{{
+						CommitHash:  commitHash,
+						CVEState:    cveschema.StatePublic,
+						TriageState: store.TriageStateIssueCreated,
+					}},
 				}),
 				modify(rs[1], &store.CVERecord{
 					TriageState:       store.TriageStateUpdatedSinceIssueCreation,