compressArticle assumed a fixed segment layout [model,cpu,mem,gpu,disk,net,psu,support].
Build() skips empty segments, so without GPU the PSU slot shifted to index 5. Step 2 of
compression called compressNetSegment on the PSU value ("2x1kW") which returned "2xNIC".
Replace positional indexing with namedSeg{group,value} tagged segments; compressArticle
now looks up each segment by group name via findSegGroup(), regardless of array length.
Regression test TestBuild_CompressArticle_NoGPU_PSUNotNIC reproduces the exact config
from OPS-2445 (NF5280M6 + 2xPSU, no GPU) that triggered the bug.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
140 lines
5.0 KiB
Go
140 lines
5.0 KiB
Go
package article
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/quoteforge/internal/localdb"
|
|
"git.mchus.pro/mchus/quoteforge/internal/models"
|
|
)
|
|
|
|
func TestBuild_ParsesNetAndPSU(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: 1,
|
|
Source: "estimate",
|
|
Version: "S-2026-02-11-001",
|
|
Name: "test",
|
|
CreatedAt: time.Now(),
|
|
SyncedAt: time.Now(),
|
|
}); err != nil {
|
|
t.Fatalf("save local pricelist: %v", err)
|
|
}
|
|
localPL, err := local.GetLocalPricelistByServerID(1)
|
|
if err != nil {
|
|
t.Fatalf("get local pricelist: %v", err)
|
|
}
|
|
|
|
if err := local.SaveLocalPricelistItems([]localdb.LocalPricelistItem{
|
|
{PricelistID: localPL.ID, LotName: "NIC_2p25G_MCX512A-AC", LotCategory: "NIC", Price: 1},
|
|
{PricelistID: localPL.ID, LotName: "HBA_2pFC32_Gen6", LotCategory: "HBA", Price: 1},
|
|
{PricelistID: localPL.ID, LotName: "PS_1000W_Platinum", LotCategory: "PS", Price: 1},
|
|
}); err != nil {
|
|
t.Fatalf("save local items: %v", err)
|
|
}
|
|
|
|
items := models.ConfigItems{
|
|
{LotName: "NIC_2p25G_MCX512A-AC", Quantity: 1},
|
|
{LotName: "HBA_2pFC32_Gen6", Quantity: 1},
|
|
{LotName: "PS_1000W_Platinum", Quantity: 2},
|
|
}
|
|
result, err := Build(local, items, BuildOptions{
|
|
ServerModel: "DL380GEN11",
|
|
SupportCode: "1yW",
|
|
ServerPricelist: &localPL.ServerID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("build article: %v", err)
|
|
}
|
|
if result.Article == "" {
|
|
t.Fatalf("expected article to be non-empty")
|
|
}
|
|
if contains(result.Article, "UNKNET") || contains(result.Article, "UNKPSU") {
|
|
t.Fatalf("unexpected UNK in article: %s", result.Article)
|
|
}
|
|
}
|
|
|
|
// TestBuild_CompressArticle_NoGPU_PSUNotNIC reproduces the bug where 2 PSUs produced
|
|
// "2xNIC" in the article because compressArticle used hard-coded indices that assumed
|
|
// GPU was always present.
|
|
func TestBuild_CompressArticle_NoGPU_PSUNotNIC(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: 2,
|
|
Source: "estimate",
|
|
Version: "S-2026-05-19-001",
|
|
Name: "test",
|
|
CreatedAt: time.Now(),
|
|
SyncedAt: time.Now(),
|
|
}); err != nil {
|
|
t.Fatalf("save local pricelist: %v", err)
|
|
}
|
|
localPL, err := local.GetLocalPricelistByServerID(2)
|
|
if err != nil {
|
|
t.Fatalf("get local pricelist: %v", err)
|
|
}
|
|
|
|
if err := local.SaveLocalPricelistItems([]localdb.LocalPricelistItem{
|
|
{PricelistID: localPL.ID, LotName: "CPU_INTEL_8358", LotCategory: "CPU", Price: 1},
|
|
{PricelistID: localPL.ID, LotName: "MEM_DDR4_64G_3200", LotCategory: "MEM", Price: 1},
|
|
{PricelistID: localPL.ID, LotName: "SSD_SATA_0.4T", LotCategory: "SSD", Price: 1},
|
|
{PricelistID: localPL.ID, LotName: "SSD_SATA_0.9T", LotCategory: "SSD", Price: 1},
|
|
{PricelistID: localPL.ID, LotName: "HDD_SATA_16T", LotCategory: "HDD", Price: 1},
|
|
{PricelistID: localPL.ID, LotName: "NIC_2p25G_MCX512A", LotCategory: "NIC", Price: 1},
|
|
{PricelistID: localPL.ID, LotName: "HBA_2pFC32_Gen6", LotCategory: "HBA", Price: 1},
|
|
{PricelistID: localPL.ID, LotName: "NIC_4p1G_I350", LotCategory: "NIC", Price: 1},
|
|
{PricelistID: localPL.ID, LotName: "PS_1500W_Platinum", LotCategory: "PS", Price: 1},
|
|
}); err != nil {
|
|
t.Fatalf("save local items: %v", err)
|
|
}
|
|
|
|
// PS_1500W → "2x1.5kW" (7 chars) brings uncompressed article to 81 chars, triggering
|
|
// compressArticle. Before the fix, compressArticle used hard-coded index 5 for NET, but
|
|
// without GPU the PSU sits at index 5, so compressNetSegment("2x1.5kW") returned "2xNIC".
|
|
items := models.ConfigItems{
|
|
{LotName: "CPU_INTEL_8358", Quantity: 2},
|
|
{LotName: "MEM_DDR4_64G_3200", Quantity: 16}, // 1024 GiB = 1T
|
|
{LotName: "SSD_SATA_0.4T", Quantity: 2},
|
|
{LotName: "SSD_SATA_0.9T", Quantity: 4},
|
|
{LotName: "HDD_SATA_16T", Quantity: 6},
|
|
{LotName: "NIC_2p25G_MCX512A", Quantity: 1},
|
|
{LotName: "HBA_2pFC32_Gen6", Quantity: 1},
|
|
{LotName: "NIC_4p1G_I350", Quantity: 1},
|
|
{LotName: "PS_1500W_Platinum", Quantity: 2},
|
|
}
|
|
result, err := Build(local, items, BuildOptions{
|
|
ServerModel: "NF5280M6",
|
|
ServerPricelist: &localPL.ServerID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("build article: %v", err)
|
|
}
|
|
if len([]rune(result.Article)) > 80 {
|
|
t.Fatalf("article too long (%d): %s", len([]rune(result.Article)), result.Article)
|
|
}
|
|
// PSU segment must not be mis-labeled as NIC during compression
|
|
// The correct behaviour: PSU is dropped, NET stays as-is or compressed to HBA/NIC labels
|
|
// Before the fix: article ended with "-2xNIC" (PSU turned into NIC)
|
|
// After the fix: article must not contain a standalone "NIC" that came from PSU wattage
|
|
if strings.HasSuffix(result.Article, "-2xNIC") {
|
|
t.Fatalf("PSU mis-labeled as NIC in article: %s", result.Article)
|
|
}
|
|
t.Logf("article: %s (warnings: %v)", result.Article, result.Warnings)
|
|
}
|
|
|
|
func contains(s, sub string) bool {
|
|
return strings.Contains(s, sub)
|
|
}
|