From 006d7e87c5a470384dd288a5f149f6a6513d7d61 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Thu, 17 Sep 2026 11:56:42 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B0=D1=80=D1=82=D0=B8=D0=BA=D1=83?= =?UTF-8?q?=D0=BB=20=E2=80=94=20=D1=80=D0=B0=D1=81=D0=BF=D0=BE=D0=B7=D0=BD?= =?UTF-8?q?=D0=B0=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20DPU/BlueField=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8F=20(BF3/BF2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DPU-лоты (DPU_BF3_B3220-32G) не подходили под regex parsePortSpeed (рассчитан на NIC/HCA-суффиксы NpMGbE/pFC) и падали в заглушку категории NET с предупреждением. Добавлен parseDPUModel — структурный разбор DPU_{FAMILY}_{MODEL} без каталога вендоров. Co-Authored-By: Claude Sonnet 5 --- internal/article/generator.go | 19 +++++++++++ internal/article/generator_test.go | 53 ++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/internal/article/generator.go b/internal/article/generator.go index f17aea0..f6bbe85 100644 --- a/internal/article/generator.go +++ b/internal/article/generator.go @@ -466,9 +466,28 @@ func parsePortSpeed(lotName string) string { if m := rePortFC.FindStringSubmatch(lotName); len(m) == 3 { return fmt.Sprintf("%spFC%s", m[1], m[2]) } + if model := parseDPUModel(lotName); model != "" { + return model + } return "" } +// parseDPUModel extracts the "{FAMILY}_{MODEL}" tail from a DPU lot_name (shape +// DPU_{FAMILY}_{MODEL}, e.g. "DPU_BF3_B3220-32G" -> "BF3_B3220-32G"). DPU lots have +// no port-speed suffix like NIC/HCA, so they fall through parsePortSpeed's regexes; +// this keeps the family prefix (e.g. BF2 vs BF3) so distinct DPU generations sharing +// a part-number suffix don't collapse into the same article token. Like parseCPUModel +// it does not validate against a vendor catalog, only matches the expected shape. +func parseDPUModel(lotName string) string { + upper := strings.ToUpper(lotName) + const prefix = "DPU_" + if !strings.HasPrefix(upper, prefix) { + return "" + } + rest := strings.TrimSpace(upper[len(prefix):]) + return rest +} + func parseWatts(lotName string) string { if m := reWatts.FindStringSubmatch(lotName); len(m) == 2 { w := atoi(m[1]) diff --git a/internal/article/generator_test.go b/internal/article/generator_test.go index bca7769..8e9d0cc 100644 --- a/internal/article/generator_test.go +++ b/internal/article/generator_test.go @@ -139,6 +139,59 @@ func contains(s, sub string) bool { return strings.Contains(s, sub) } +func TestParsePortSpeed_DPUModel(t *testing.T) { + cases := map[string]string{ + "DPU_BF3_B3220-32G": "BF3_B3220-32G", + "DPU_BF2_B2220Q": "BF2_B2220Q", + } + for in, want := range cases { + if got := parsePortSpeed(in); got != want { + t.Errorf("parsePortSpeed(%q) = %q, want %q", in, got, want) + } + } +} + +// TestBuild_ParsesDPU verifies a DPU lot (e.g. NVIDIA BlueField) resolves to its +// family+model token instead of falling back to the "NET" category placeholder — +// DPU lots don't carry a NIC/HCA-style port-speed suffix, so parsePortSpeed needs +// its own DPU branch (parseDPUModel). +func TestBuild_ParsesDPU(t *testing.T) { + local, err := localdb.New(filepath.Join(t.TempDir(), "local.db")) + if err != nil { + t.Fatalf("init local db: %v", err) + } + t.Cleanup(func() { _ = local.Close() }) + + if err := local.SaveLocalPricelist(&localdb.LocalPricelist{ + ServerID: 10, Source: "estimate", Version: "v1", Name: "t", + IsActive: true, CreatedAt: time.Now(), SyncedAt: time.Now(), + }); err != nil { + t.Fatalf("save pricelist: %v", err) + } + pl, err := local.GetLocalPricelistByServerID(10) + if err != nil { + t.Fatalf("get pricelist: %v", err) + } + if err := local.SaveLocalPricelistItems([]localdb.LocalPricelistItem{ + {PricelistID: pl.ID, LotName: "DPU_BF3_B3220-32G", LotCategory: "DPU", Price: 1}, + }); err != nil { + t.Fatalf("save items: %v", err) + } + + result, err := Build(local, models.ConfigItems{ + {LotName: "DPU_BF3_B3220-32G", Quantity: 1}, + }, BuildOptions{ServerModel: "X1"}) + if err != nil { + t.Fatalf("build: %v", err) + } + if len(result.Warnings) != 0 { + t.Fatalf("expected no warnings, got %v", result.Warnings) + } + if !contains(result.Article, "BF3_B3220-32G") { + t.Fatalf("expected DPU model token in article: %s", result.Article) + } +} + func TestParseGPUModel_VendorLetterSuffix(t *testing.T) { cases := map[string]string{ "GPU_NV_RTX_PRO_6000D_SERVER_84GB_PCIE": "RTX6000D_84GB",