Files
libredesk/internal/ai/embedsource_test.go
Abhinav Raut 76db5aa851 fix help center bugs and share the embedding and article-content code
Review pass over the help center commit. Bug fixes:

- Image-only or video-only articles were rejected on save because the
  editor blanked the content when it had no text.
- An article could be moved into a collection in another help center.
  Slugs are only unique per help center, so two articles could end up
  on the same public URL.
- The article editor fired the "agent is typing" indicator into
  whatever conversation was last open, which the contact could see.
- Unsupported locales like /hc/x/zz-ZZ returned an empty 200 page with
  a canonical URL, so crawlers saw unlimited URLs. They 404 now.
- Article feedback was accepted for paused help centers.
- French pages announced lang="en".
- Editing a help center popped validation errors mid-keystroke.

Shared code that was duplicated:

- Help articles had their own copy of the snippet embedding lifecycle.
  The copy was missing the wait group, the concurrency cap and the
  generation gating, so article embeds were killed at shutdown, all hit
  the provider at once, and a slow embed could overwrite a newer edit or
  bring back a deleted article. Both content types now go through one
  embedSource lifecycle. Fingerprints are unchanged, so nothing
  re-embeds on deploy.
- Callout and collapsible CSS was written twice, once for the editor and
  once for the public page. They now share article-content.css, so the
  editor stays WYSIWYG.
- The excerpt was computed three ways. The page uses the stored one.

Less work at runtime:

- The admin tree no longer sends every article's HTML. The edit sheet
  loads the one article it needs. Publishing an article used to
  re-download the whole help center.
- Embedding reconcile no longer reads every article body every minute.
- The tree view fetched the help center twice on load.

Cleanups: one update-article route instead of two, media privacy comes
from the model type instead of a bool threaded through the handler, a
LinkListField component replaces three copies of the same repeater,
TreeNode renders the article row once, and 12 duplicate or dead i18n
keys are gone.
2026-07-25 15:50:47 +05:30

35 lines
1.7 KiB
Go

package ai
import (
"testing"
)
func TestItemFingerprint(t *testing.T) {
item := embedItem{Title: "Refunds", Content: "We refund within 30 days."}
base := itemFingerprint(item, "https://api.openai.com/v1", "text-embedding-3-small", 1536)
if itemFingerprint(item, "https://api.openai.com/v1", "text-embedding-3-small", 1536) != base {
t.Fatal("fingerprint must be stable for identical inputs")
}
// Every field that makes stored vectors comparable must change the fingerprint, or a reindex is skipped when it shouldn't be.
cases := map[string]string{
"base URL changed": itemFingerprint(item, "https://llm.internal/v1", "text-embedding-3-small", 1536),
"model changed": itemFingerprint(item, "https://api.openai.com/v1", "text-embedding-3-large", 1536),
"dimensions changed": itemFingerprint(item, "https://api.openai.com/v1", "text-embedding-3-small", 3072),
"title changed": itemFingerprint(embedItem{Title: "Returns", Content: item.Content}, "https://api.openai.com/v1", "text-embedding-3-small", 1536),
"content changed": itemFingerprint(embedItem{Title: item.Title, Content: "We refund within 14 days."}, "https://api.openai.com/v1", "text-embedding-3-small", 1536),
}
for name, fp := range cases {
if fp == base {
t.Errorf("%s: fingerprint should differ from the baseline", name)
}
}
// Eligibility and id are lifecycle state, not embedded content; they must not force a reindex.
varied := embedItem{ID: 42, Title: item.Title, Content: item.Content, Fingerprint: "stale", Eligible: true}
if itemFingerprint(varied, "https://api.openai.com/v1", "text-embedding-3-small", 1536) != base {
t.Error("fingerprint must depend only on title, content and provider identity")
}
}