Port the remaining consistency guards from cmautobuy's planExistingMatchedInnerCodeItems into assignExistingBoundItems: - Reject the whole multi-piece group if any candidate's raw ProductSpec, sku or variationSku differs from the lowest-ID candidate. Candidates are matched via NormalizeSpecKey, so raw values can legitimately differ even when normalized keys agree; auto-assigning across genuinely different items must be blocked. - Re-verify each candidate against stallMatches when record.Stall is non-empty, since the no-SKU fallback path in matchEvidence can hand back candidates that were never stall-checked. - Reject candidates with a non-positive or duplicate detail ID. Added one regression test per guard plus a happy-path test confirming legitimate multi-piece assignment still succeeds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTDbDcwbDw1TSAcE6wfh2F
383 lines
17 KiB
Go
383 lines
17 KiB
Go
package sybinnercode
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"go-admin/app/goauto/models"
|
|
"go-admin/app/goauto/sybclient"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type fakeMatchReader struct {
|
|
rows map[string][]sybclient.StockRow
|
|
stocks map[int64]sybclient.StockDetail
|
|
listCalls, detailCalls int
|
|
}
|
|
|
|
func (f *fakeMatchReader) ListByOrderNumber(_ context.Context, order string) ([]sybclient.StockRow, error) {
|
|
f.listCalls++
|
|
return f.rows[order], nil
|
|
}
|
|
func (f *fakeMatchReader) DetailListByStock(_ context.Context, ids []int64) ([]sybclient.StockDetail, error) {
|
|
f.detailCalls++
|
|
result := make([]sybclient.StockDetail, 0, len(ids))
|
|
for _, id := range ids {
|
|
if stock, ok := f.stocks[id]; ok {
|
|
result = append(result, stock)
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func createMatchJob(t *testing.T, db *gorm.DB, records []models.SYBInnerCodeRecord) string {
|
|
t.Helper()
|
|
for i := range records {
|
|
if err := db.Create(&records[i]).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
id := uuid.NewString()
|
|
ids := make([]uint64, 0, len(records))
|
|
for _, record := range records {
|
|
ids = append(ids, record.ID)
|
|
}
|
|
raw, _ := json.Marshal(ids)
|
|
job := models.SYBInnerCodeMatchJob{ID: id, WorkKey: "test:" + id, BusinessDate: "2026-08-28", RecordIDsJSON: string(raw), Status: "pending", Total: len(records)}
|
|
if err := db.Create(&job).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
func TestRunMatchJobOnlyProcessesScopedRecords(t *testing.T) {
|
|
db := testDB(t)
|
|
selected := matchRecord("ORDER-1", "SKU-1", "A#1", "IC-1")
|
|
jobID := createMatchJob(t, db, []models.SYBInnerCodeRecord{selected})
|
|
outside := matchRecord("ORDER-2", "SKU-2", "B#2", "IC-2")
|
|
if err := db.Create(&outside).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reader := &fakeMatchReader{rows: map[string][]sybclient.StockRow{"ORDER-1": {{ID: 10}}}, stocks: map[int64]sybclient.StockDetail{10: {ID: 10, Details: []sybclient.DetailItem{detail(20, "黑色,L", 1, "SKU-1", "A#1", "")}}}}
|
|
if err := RunMatchJob(context.Background(), db, reader, jobID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var untouched models.SYBInnerCodeRecord
|
|
if err := db.First(&untouched, outside.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if untouched.Status != models.SYBInnerCodePending {
|
|
t.Fatalf("out-of-scope record changed to %s", untouched.Status)
|
|
}
|
|
}
|
|
func detail(id int64, spec string, qty int, sku, variation, code string) sybclient.DetailItem {
|
|
return sybclient.DetailItem{ID: id, ProductSpec: spec, ProductQty: qty, Raw: map[string]any{"sku": sku, "variationSku": variation, "innerExpCode": code, "purchasePlatform": "", "purchaseCode": ""}}
|
|
}
|
|
|
|
func matchRecord(order, sku, stall, code string) models.SYBInnerCodeRecord {
|
|
return models.SYBInnerCodeRecord{BusinessDate: "2026-08-28", OrderNumber: order, Stall: stall, SpecKey: "黑色,L", SpecRaw: "黑色,L", SourceSKURaw: sku, Status: models.SYBInnerCodePending, CreatedBy: 1, ImportRequestID: uuid.NewString(), Items: []models.SYBInnerCodeItem{{BusinessDate: "2026-08-28", Code: code, Ordinal: 1, SourceRow: 2}}}
|
|
}
|
|
|
|
func TestRunMatchJobPlansMultiItemWithoutAnyWriteSurface(t *testing.T) {
|
|
db := testDB(t)
|
|
record := models.SYBInnerCodeRecord{BusinessDate: "2026-08-28", OrderNumber: "ORDER-1", Stall: "A#1", SpecKey: "黑色,L", SpecRaw: "黑色,L", SourceSKURaw: "SKU-1", Status: models.SYBInnerCodePending, CreatedBy: 1, ImportRequestID: uuid.NewString(), Items: []models.SYBInnerCodeItem{{BusinessDate: "2026-08-28", Code: "IC-1", Ordinal: 1, SourceRow: 2}, {BusinessDate: "2026-08-28", Code: "IC-2", Ordinal: 2, SourceRow: 3}}}
|
|
jobID := createMatchJob(t, db, []models.SYBInnerCodeRecord{record})
|
|
reader := &fakeMatchReader{rows: map[string][]sybclient.StockRow{"ORDER-1": {{ID: 10, Code: "ORDER-1"}}}, stocks: map[int64]sybclient.StockDetail{10: {ID: 10, Code: "ORDER-1", Details: []sybclient.DetailItem{detail(20, "黑色,L", 2, "SKU-1", "A#1", "")}}}}
|
|
if err := RunMatchJob(context.Background(), db, reader, jobID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var saved models.SYBInnerCodeRecord
|
|
if err := db.Preload("Items").First(&saved).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if saved.Status != models.SYBInnerCodeReady {
|
|
t.Fatalf("status=%s message=%s", saved.Status, saved.ResultMessage)
|
|
}
|
|
var plan models.SYBInnerCodePlan
|
|
if err := db.First(&plan, "record_id = ?", saved.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if plan.DetailID != 20 || plan.PlaceholderCount != 1 || plan.EvidenceHash == "" {
|
|
t.Fatalf("plan=%+v", plan)
|
|
}
|
|
if reader.listCalls != 1 || reader.detailCalls != 1 {
|
|
t.Fatalf("calls list=%d detail=%d", reader.listCalls, reader.detailCalls)
|
|
}
|
|
if err := RunMatchJob(context.Background(), db, reader, jobID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reader.listCalls != 1 {
|
|
t.Fatalf("completed job reran")
|
|
}
|
|
}
|
|
|
|
func TestRunMatchJobRejectsSKUAndStallConflict(t *testing.T) {
|
|
db := testDB(t)
|
|
record := models.SYBInnerCodeRecord{BusinessDate: "2026-08-28", OrderNumber: "ORDER-1", Stall: "B#2", SpecKey: "黑色,L", SpecRaw: "黑色,L", SourceSKURaw: "SKU-A", Status: models.SYBInnerCodePending, CreatedBy: 1, ImportRequestID: uuid.NewString(), Items: []models.SYBInnerCodeItem{{BusinessDate: "2026-08-28", Code: "IC-1", Ordinal: 1, SourceRow: 2}}}
|
|
jobID := createMatchJob(t, db, []models.SYBInnerCodeRecord{record})
|
|
reader := &fakeMatchReader{rows: map[string][]sybclient.StockRow{"ORDER-1": {{ID: 10, Code: "ORDER-1"}}}, stocks: map[int64]sybclient.StockDetail{10: {ID: 10, Details: []sybclient.DetailItem{detail(20, "黑色,L", 1, "SKU-A", "A#1", ""), detail(21, "黑色,L", 1, "SKU-B", "B#2", "")}}}}
|
|
if err := RunMatchJob(context.Background(), db, reader, jobID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var saved models.SYBInnerCodeRecord
|
|
db.First(&saved)
|
|
if saved.Status != models.SYBInnerCodeSkipped || saved.ResultMessage != "原始 SKU 与档口货号冲突,不能自动选择" {
|
|
t.Fatalf("record=%+v", saved)
|
|
}
|
|
}
|
|
|
|
func TestRunMatchJobReservesDifferentDetailsForRecordsInOneBatch(t *testing.T) {
|
|
db := testDB(t)
|
|
base := func(order, sku, stall, code string) models.SYBInnerCodeRecord {
|
|
return models.SYBInnerCodeRecord{BusinessDate: "2026-08-28", OrderNumber: order, Stall: stall, SpecKey: "黑色,L", SpecRaw: "黑色,L", SourceSKURaw: sku, Status: models.SYBInnerCodePending, CreatedBy: 1, ImportRequestID: uuid.NewString(), Items: []models.SYBInnerCodeItem{{BusinessDate: "2026-08-28", Code: code, Ordinal: 1, SourceRow: 2}}}
|
|
}
|
|
jobID := createMatchJob(t, db, []models.SYBInnerCodeRecord{base("ORDER-1", "SKU-A", "A#1", "IC-A"), base("ORDER-1", "SKU-B", "B#2", "IC-B")})
|
|
stock := sybclient.StockDetail{ID: 10, Details: []sybclient.DetailItem{detail(20, "黑色,L", 1, "SKU-A", "A#1", ""), detail(21, "黑色,L", 1, "SKU-B", "B#2", "")}}
|
|
reader := &fakeMatchReader{rows: map[string][]sybclient.StockRow{"ORDER-1": {{ID: 10, Code: "ORDER-1"}}}, stocks: map[int64]sybclient.StockDetail{10: stock}}
|
|
if err := RunMatchJob(context.Background(), db, reader, jobID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var plans []models.SYBInnerCodePlan
|
|
db.Order("record_id").Find(&plans)
|
|
if len(plans) != 2 || plans[0].DetailID == plans[1].DetailID {
|
|
t.Fatalf("plans=%+v", plans)
|
|
}
|
|
}
|
|
|
|
// --- #259/#273/#289 stall-matching regression tests (ported from cmautobuy) ---
|
|
|
|
func TestStallMatchesIgnoresWeightLikeNumberAsArticle(t *testing.T) {
|
|
// #273: "50公斤" must not be treated as if the article were the bare number 50.
|
|
item := detail(1, "50公斤,黑色", 1, "SKU-X", "", "")
|
|
if stallMatches("档口甲#50", item) {
|
|
t.Fatalf("weight-like text must not match numeric article 50")
|
|
}
|
|
}
|
|
|
|
func TestStallMatchesRejectsSubstringInsideLongCode(t *testing.T) {
|
|
// #273: a long code with internal digits (PDD256437) must not spuriously
|
|
// match a short numeric article (256) via substring containment.
|
|
item := detail(1, "黑色,L", 1, "PDD256437", "档口甲", "")
|
|
if stallMatches("档口甲#256", item) {
|
|
t.Fatalf("long code must not match numeric article 256 via substring")
|
|
}
|
|
}
|
|
|
|
func TestStallMatchesLeadingZeroEquivalenceRequiresStallName(t *testing.T) {
|
|
// #259/#273: "067" and "67" are equivalent articles only when the stall
|
|
// name also matches; a different stall name must not match.
|
|
sameStall := detail(1, "黑色,L", 1, "档口甲-067", "", "")
|
|
if !stallMatches("档口甲#67", sameStall) {
|
|
t.Fatalf("067 should be treated as equivalent to 67 when stall name matches")
|
|
}
|
|
differentStall := detail(2, "黑色,L", 1, "档口乙-067", "", "")
|
|
if stallMatches("档口甲#67", differentStall) {
|
|
t.Fatalf("067 must not match 67 when the stall name differs")
|
|
}
|
|
}
|
|
|
|
func TestStallMatchesNonNumericArticleRequiresExactToken(t *testing.T) {
|
|
item := detail(1, "黑色,L", 1, "ABC12", "", "")
|
|
if stallMatches("档口甲#AB", item) {
|
|
t.Fatalf("non-numeric article must require an exact token match, not substring")
|
|
}
|
|
exact := detail(2, "黑色,L", 1, "AB", "", "")
|
|
if !stallMatches("档口甲#AB", exact) {
|
|
t.Fatalf("exact non-numeric token should match")
|
|
}
|
|
}
|
|
|
|
func TestStallMatchesHandlesHashInsideStallName(t *testing.T) {
|
|
// Splits on the LAST '#' so a stall name that itself contains '#' still
|
|
// yields the correct article.
|
|
item := detail(1, "黑色,L", 1, "档口#甲-67", "", "")
|
|
if !stallMatches("档口#甲#67", item) {
|
|
t.Fatalf("stall name containing '#' should still resolve article via last '#'")
|
|
}
|
|
}
|
|
|
|
func TestStallMatchesProductSpecPrefixMatchesArticle(t *testing.T) {
|
|
// Non-numeric article: ProductSpec prefix match does not additionally
|
|
// require the stall name to appear in sku/variationSku.
|
|
item := detail(1, "AB 黑色,L", 1, "", "", "")
|
|
if !stallMatches("档口甲#AB", item) {
|
|
t.Fatalf("ProductSpec starting with the article should match")
|
|
}
|
|
notPrefix := detail(2, "黑色,ABL", 1, "", "", "")
|
|
if stallMatches("档口甲#AB", notPrefix) {
|
|
t.Fatalf("article appearing mid-spec (not as prefix) must not match")
|
|
}
|
|
}
|
|
|
|
func TestStallMatchesEmptyArticleFallsBackToStallName(t *testing.T) {
|
|
item := detail(1, "黑色,L", 1, "档口甲专柜", "", "")
|
|
if !stallMatches("档口甲#", item) {
|
|
t.Fatalf("empty article should fall back to stall-name containment")
|
|
}
|
|
}
|
|
|
|
// --- #289: existing-binding-preserving multi-piece assignment ---
|
|
|
|
func TestAssignExistingBoundItemsPreservesOutOfOrderBindings(t *testing.T) {
|
|
// Two single-piece candidates already carry codes, but the previously
|
|
// bound code (IC-2) sits on the LOWER-ID detail while the target order
|
|
// expects it second. A naive ID-order/index assignment would strip the
|
|
// existing correct binding from detail 20 and try to overwrite it.
|
|
d20 := detail(20, "黑色,L", 1, "SKU-1", "A#1", "IC-2")
|
|
d21 := detail(21, "黑色,L", 1, "SKU-1", "A#1", "")
|
|
assigned, reason := assignExistingBoundItems("A#1", []string{"IC-1", "IC-2"}, []sybclient.DetailItem{d20, d21})
|
|
if reason != "" {
|
|
t.Fatalf("unexpected reason: %s", reason)
|
|
}
|
|
if assigned[1].ID != 20 {
|
|
t.Fatalf("expected detail 20 (already bound to IC-2) preserved at index 1, got %+v", assigned[1])
|
|
}
|
|
if assigned[0].ID != 21 {
|
|
t.Fatalf("expected the blank detail 21 filled in at index 0, got %+v", assigned[0])
|
|
}
|
|
}
|
|
|
|
func TestAssignExistingBoundItemsRejectsForeignCode(t *testing.T) {
|
|
d20 := detail(20, "黑色,L", 1, "SKU-1", "A#1", "IC-OTHER")
|
|
d21 := detail(21, "黑色,L", 1, "SKU-1", "A#1", "")
|
|
_, reason := assignExistingBoundItems("A#1", []string{"IC-1", "IC-2"}, []sybclient.DetailItem{d20, d21})
|
|
if reason == "" {
|
|
t.Fatalf("expected rejection for detail already holding a non-target code")
|
|
}
|
|
}
|
|
|
|
func TestAssignExistingBoundItemsRejectsInconsistentIdentity(t *testing.T) {
|
|
// Candidates can share a NormalizeSpecKey-normalized spec while their raw
|
|
// ProductSpec/sku/variationSku differ; auto-assignment across genuinely
|
|
// different items must be rejected.
|
|
d20 := detail(20, "黑色, L", 1, "SKU-1", "A#1", "")
|
|
d21 := detail(21, "黑色,L", 1, "SKU-2", "A#1", "")
|
|
_, reason := assignExistingBoundItems("A#1", []string{"IC-1", "IC-2"}, []sybclient.DetailItem{d20, d21})
|
|
if reason != "重复候选的规格或 SKU 身份不一致,不能自动逐件分配" {
|
|
t.Fatalf("expected identity-mismatch rejection, got %q", reason)
|
|
}
|
|
}
|
|
|
|
func TestAssignExistingBoundItemsRejectsCandidateFailingStallCheck(t *testing.T) {
|
|
// The no-SKU fallback path in matchEvidence can hand back candidates that
|
|
// were never checked against the stall at all. Both candidates share an
|
|
// identical identity (so the identity guard passes) but neither one's
|
|
// sku/spec actually satisfies the record's stall/article requirement.
|
|
d20 := detail(20, "黑色,L", 1, "SKU-1", "ZZZ", "")
|
|
d21 := detail(21, "黑色,L", 1, "SKU-1", "ZZZ", "")
|
|
_, reason := assignExistingBoundItems("甲档口#88", []string{"IC-1", "IC-2"}, []sybclient.DetailItem{d20, d21})
|
|
if reason != "重复候选的档口及货号不一致,不能自动逐件分配" {
|
|
t.Fatalf("expected stall-mismatch rejection, got %q", reason)
|
|
}
|
|
}
|
|
|
|
func TestAssignExistingBoundItemsRejectsInvalidOrDuplicateDetailID(t *testing.T) {
|
|
invalidID := detail(0, "黑色,L", 1, "SKU-1", "A#1", "")
|
|
valid := detail(21, "黑色,L", 1, "SKU-1", "A#1", "")
|
|
if _, reason := assignExistingBoundItems("A#1", []string{"IC-1", "IC-2"}, []sybclient.DetailItem{invalidID, valid}); reason == "" {
|
|
t.Fatalf("expected rejection for non-positive detail ID")
|
|
}
|
|
dup1 := detail(20, "黑色,L", 1, "SKU-1", "A#1", "")
|
|
dup2 := detail(20, "黑色,L", 1, "SKU-1", "A#1", "")
|
|
if _, reason := assignExistingBoundItems("A#1", []string{"IC-1", "IC-2"}, []sybclient.DetailItem{dup1, dup2}); reason == "" {
|
|
t.Fatalf("expected rejection for duplicate detail IDs")
|
|
}
|
|
}
|
|
|
|
func TestAssignExistingBoundItemsHappyPathStillAssignsWithGuards(t *testing.T) {
|
|
// Both candidates share identical raw spec/sku/variationSku and both
|
|
// individually satisfy the stall check; the guards must not block the
|
|
// legitimate happy path.
|
|
d20 := detail(20, "黑色,L", 1, "SKU-1", "A#1", "IC-2")
|
|
d21 := detail(21, "黑色,L", 1, "SKU-1", "A#1", "")
|
|
assigned, reason := assignExistingBoundItems("A#1", []string{"IC-1", "IC-2"}, []sybclient.DetailItem{d20, d21})
|
|
if reason != "" {
|
|
t.Fatalf("unexpected reason: %s", reason)
|
|
}
|
|
if assigned[0].ID != 21 || assigned[1].ID != 20 {
|
|
t.Fatalf("assigned=%+v", assigned)
|
|
}
|
|
}
|
|
|
|
func TestRunMatchJobPreservesExistingBindingWhenCandidateOrderDiffers(t *testing.T) {
|
|
// End-to-end regression for #289: N=2 single-piece candidates already
|
|
// carrying one previously bound code, with the bound detail's ID not
|
|
// matching sequential/ID order relative to the target codes. The plan
|
|
// must leave the already-correct binding untouched and only place the
|
|
// missing code onto the still-blank detail.
|
|
db := testDB(t)
|
|
// SourceSKURaw is intentionally left blank: with the same source SKU on
|
|
// both candidate rows the source-SKU path would reject as "duplicate"
|
|
// before ever reaching the stall-based multi-item assignment being
|
|
// regression-tested here.
|
|
record := models.SYBInnerCodeRecord{BusinessDate: "2026-08-28", OrderNumber: "ORDER-1", Stall: "A#1", SpecKey: "黑色,L", SpecRaw: "黑色,L", Status: models.SYBInnerCodePending, CreatedBy: 1, ImportRequestID: uuid.NewString(), Items: []models.SYBInnerCodeItem{{BusinessDate: "2026-08-28", Code: "IC-1", Ordinal: 1, SourceRow: 2}, {BusinessDate: "2026-08-28", Code: "IC-2", Ordinal: 2, SourceRow: 3}}}
|
|
records := []models.SYBInnerCodeRecord{record}
|
|
jobID := createMatchJob(t, db, records)
|
|
recordID := records[0].ID
|
|
// Detail 20 (lower ID) already carries IC-2 (bound out of sequence);
|
|
// detail 21 (higher ID) is still blank and should receive IC-1.
|
|
stock := sybclient.StockDetail{ID: 10, Details: []sybclient.DetailItem{
|
|
detail(20, "黑色,L", 1, "SKU-1", "A#1", "IC-2"),
|
|
detail(21, "黑色,L", 1, "SKU-1", "A#1", ""),
|
|
}}
|
|
reader := &fakeMatchReader{rows: map[string][]sybclient.StockRow{"ORDER-1": {{ID: 10, Code: "ORDER-1"}}}, stocks: map[int64]sybclient.StockDetail{10: stock}}
|
|
if err := RunMatchJob(context.Background(), db, reader, jobID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var plan models.SYBInnerCodePlan
|
|
if err := db.First(&plan, "record_id = ?", recordID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var items []plannedRemoteItem
|
|
if err := json.Unmarshal([]byte(plan.RemoteItemsJSON), &items); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(items) != 2 {
|
|
t.Fatalf("items=%+v", items)
|
|
}
|
|
byCode := map[string]plannedRemoteItem{}
|
|
for _, item := range items {
|
|
byCode[item.Code] = item
|
|
}
|
|
if byCode["IC-2"].DetailID != 20 {
|
|
t.Fatalf("existing binding for IC-2 must stay on detail 20, got %+v", byCode["IC-2"])
|
|
}
|
|
if byCode["IC-1"].DetailID != 21 {
|
|
t.Fatalf("missing IC-1 should be assigned to the blank detail 21, got %+v", byCode["IC-1"])
|
|
}
|
|
}
|
|
|
|
type captureStarter struct {
|
|
jobID string
|
|
committed bool
|
|
}
|
|
|
|
func (c *captureStarter) Start(_ context.Context, db *gorm.DB, jobID string) error {
|
|
c.jobID = jobID
|
|
var count int64
|
|
db.Model(&models.SYBInnerCodeRecord{}).Count(&count)
|
|
c.committed = count > 0
|
|
return nil
|
|
}
|
|
func TestImportCreatesOneMatchJobAndStartsAfterCommit(t *testing.T) {
|
|
db := testDB(t)
|
|
starter := &captureStarter{}
|
|
service := NewService(db).WithMatchEnqueuer(starter)
|
|
rows := [][]any{{"2026-08-28", "ORDER-1", "店铺", "A#1", "SKU-1", "黑色,L", "IC-01", 1}}
|
|
result, err := importRows(t, service, uuid.NewString(), rows)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.MatchJobID == "" || starter.jobID != result.MatchJobID || !starter.committed {
|
|
t.Fatalf("result=%+v starter=%+v", result, starter)
|
|
}
|
|
var count int64
|
|
db.Model(&models.SYBInnerCodeMatchJob{}).Count(&count)
|
|
if count != 1 {
|
|
t.Fatalf("jobs=%d", count)
|
|
}
|
|
}
|