package gateway import ( "strings" "testing" ) // Per-key AUTO chain UI contract. // // The per-key editor deliberately REUSES the global lane canvas instead of // shipping a second, simpler editor. That is a design choice with teeth: a // reduced copy would lack drag ordering and per-slot quota/period/hours, and // the two would drift apart silently. These checks pin the sharing, because // the only symptom of un-sharing is "the per-key editor got worse" — no error, // just a smaller feature set that nobody notices until they use it. func TestUIPerKeyAutoWiring(t *testing.T) { src := uiSource(t) // The admin key list must offer the editor, otherwise the whole feature is // unreachable from the UI. Assert the *call site* inside keyCanvasHtml, // not just that the functions exist: removing the button leaves every // definition intact, and that is exactly the regression to catch. kcStart := strings.Index(src, "function keyCanvasHtml") if kcStart < 0 { t.Fatal("keyCanvasHtml is gone — cannot check the AUTO chain button") } kcEnd := strings.Index(src[kcStart:], "\n }") if kcEnd < 0 { t.Fatal("could not delimit keyCanvasHtml") } if !strings.Contains(src[kcStart:kcStart+kcEnd], "openKeyAutoModal(") { t.Fatal("per-key AUTO chain button is missing from the key card header") } for _, fn := range []string{ "function openKeyAutoModal", "function keyAutoInherit", "function keyAutoSave", "function keyAutoClose", } { if !strings.Contains(src, fn) { t.Fatalf("UI is missing %s — the editor cannot function", fn) } } } // The per-key editor must render the SAME canvas the global editor uses. // If this regresses, the per-key chain silently loses drag ordering and // per-slot quota/period/hours while still looking like it works. func TestUIPerKeyAutoReusesGlobalCanvas(t *testing.T) { src := uiSource(t) // One canvas implementation, two mount ids. if !strings.Contains(src, "function renderSortEditor(") { t.Fatal("no shared canvas renderer — the per-key editor must reuse renderSortEditor") } if !strings.Contains(src, `canvasId: "scr-canvas"`) { t.Fatal("the global editor no longer mounts the shared canvas") } if !strings.Contains(src, `canvasId: "key-auto-canvas"`) { t.Fatal("the per-key editor does not mount the shared canvas") } // Canvas lookup must consider both ids, or drag/drop silently no-ops on // whichever surface is not #scr-canvas. if !strings.Contains(src, "function sortCanvasEl(") || !strings.Contains(src, `getElementById("key-auto-canvas")`) { t.Fatal("sortCanvasEl must resolve both canvas ids") } // The rules<->lanes conversion is shared too, so the per-key editor sends // exactly the fields the server understands (quota/period/hours included). for _, fn := range []string{"function buildLanes(", "function lanesToRules("} { if !strings.Contains(src, fn) { t.Fatalf("%s must be shared between the global and per-key editors", fn) } } } // Saving must route through persistAuto's scope switch, not a hand-rolled // request, or the two editors will serialize rules differently. func TestUIPerKeyAutoSavesThroughScope(t *testing.T) { src := uiSource(t) if !strings.Contains(src, "sortState.scope = key") { t.Fatal("openKeyAutoModal must set sortState.scope so the save targets the key") } if !strings.Contains(src, "sortState.scope = null") { t.Fatal("closing the modal must clear sortState.scope, or the GLOBAL editor would save to this key") } if !strings.Contains(src, "if (sortState.scope)") { t.Fatal("persistAuto must branch on sortState.scope") } if !strings.Contains(src, `"/auto"`) { t.Fatal("per-key editor must call the /api/keys/{key}/auto endpoint") } // An empty lane list is how "inherit the global chain" is expressed. if !strings.Contains(src, "JSON.stringify({ auto: lanesToRules(sortState.lanes) })") { t.Fatal("keyAutoSave must PUT lanesToRules(sortState.lanes) verbatim") } } // Both languages need the strings, otherwise a non-Chinese admin sees raw // i18n keys in the modal. func TestUIPerKeyAutoHasBothLanguages(t *testing.T) { src := uiSource(t) for _, k := range []string{ "kAutoChainTitle", "kAutoChainHint", "kAutoChainInherit", "kAutoChainEmpty", "kAutoChainGlobal", "kAutoChainBad", } { n := strings.Count(src, k+`: "`) if n < 2 { t.Fatalf("i18n key %s defined %d time(s), want both zh and en", k, n) } } } // The statistics dashboard must print the window it actually covers. Calendar // periods are not nested — "this week" starts on its Monday and therefore // reaches into the previous month early in a month, making its total LARGER // than "this month". That is correct arithmetic, but without the dates on // screen it reads as a bug, and it has been reported as one. func TestUIStatsShowsPeriodWindow(t *testing.T) { src := uiSource(t) if !strings.Contains(src, `id="period-range"`) { t.Fatal("the stats dashboard has no element for the period window range") } if n := strings.Count(src, "periodRange"); n < 2 { t.Fatalf("i18n key periodRange appears %d time(s), want both zh and en", n) } }