Deklarix itself no longer depends on the Anthropic API — that was a separate API key/billing relationship from Claude Code (used to develop Deklarix), which the user did not intend to take on for the product itself. Consideration (Gegenleistung) is no longer guessed from text — it's a required form field now, since only the submitter actually knows whether a business relationship existed. A keyword-only system can't tell a covertly-paid post from a genuinely organic one; they read identically. What internal/extract *can* still determine reliably and deterministically from the caption: whether a disclosure keyword is present (werbung, anzeige, bezahlte partnerschaft, paid partnership, #ad, #werbung, #anzeige, #sponsored, #sponsoredby, #sponsoredpost — case-insensitive), its exact original-case wording, and whether it sits before the platform's "mehr anzeigen" truncation point (~125 chars Instagram, ~150 TikTok — rough estimates, platforms change these without notice, verify before real customer use). internal/extract's Anthropic HTTP client and tool-use schema are gone (client.go/api.go deleted), replaced by engine.go — a stateless Engine with no network calls. extract.Result/ParsePayload keep the exact same JSON shape as before (gegenleistung/kennzeichnung_vorhanden/ kennzeichnung_wortlaut/kennzeichnung_vor_kuerzung), so internal/store and internal/dossier needed no changes at all — only extract itself, the web form/handler (new consideration field), and main.go (no more ANTHROPIC_API_KEY requirement) changed. Trade-off the user was told and accepted: without an LLM, the system can no longer independently catch undisclosed paid content that carries no recognizable keyword at all — that now rests on the submitter's honesty. Creative or implicit disclosure phrasing outside the keyword list also won't be recognized. Verified against a real running instance with zero API keys configured: register -> check (real rule engine, correctly triggered WK-004 for a disclosure placed 130 characters in, past the Instagram threshold) -> archive -> PDF dossier download, all against real Postgres. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
165 lines
5.5 KiB
Go
165 lines
5.5 KiB
Go
package extract_test
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/netcell-it/deklarix/internal/extract"
|
|
"github.com/netcell-it/deklarix/internal/rules"
|
|
)
|
|
|
|
func TestExtractRejectsEmptyCaption(t *testing.T) {
|
|
e := extract.NewEngine()
|
|
if _, err := e.Extract(context.Background(), extract.Input{Platform: "instagram", Consideration: "bezahlt"}); err == nil {
|
|
t.Fatal("expected error for empty caption, got nil")
|
|
}
|
|
}
|
|
|
|
func TestExtractRejectsMissingConsideration(t *testing.T) {
|
|
e := extract.NewEngine()
|
|
if _, err := e.Extract(context.Background(), extract.Input{Platform: "instagram", Caption: "irgendein Text"}); err == nil {
|
|
t.Fatal("expected error for missing consideration, got nil")
|
|
}
|
|
}
|
|
|
|
func TestExtractRejectsInvalidConsideration(t *testing.T) {
|
|
e := extract.NewEngine()
|
|
_, err := e.Extract(context.Background(), extract.Input{
|
|
Platform: "instagram", Caption: "Text", Consideration: "vielleicht",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for an out-of-enum consideration value, got nil")
|
|
}
|
|
}
|
|
|
|
func TestExtractDetectsDisclosureKeyword(t *testing.T) {
|
|
e := extract.NewEngine()
|
|
result, err := e.Extract(context.Background(), extract.Input{
|
|
Platform: "instagram", Consideration: "bezahlt",
|
|
Caption: "Werbung: Schaut euch dieses tolle Produkt an!",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Extract: %v", err)
|
|
}
|
|
if !result.Facts.DisclosurePresent {
|
|
t.Error("expected DisclosurePresent = true")
|
|
}
|
|
if result.Facts.DisclosureWording != "Werbung" {
|
|
t.Errorf("DisclosureWording = %q, want Werbung (original casing preserved)", result.Facts.DisclosureWording)
|
|
}
|
|
if !result.Facts.DisclosureBeforeCut {
|
|
t.Error("expected DisclosureBeforeCut = true (Werbung is at index 0)")
|
|
}
|
|
}
|
|
|
|
func TestExtractIsCaseInsensitive(t *testing.T) {
|
|
e := extract.NewEngine()
|
|
result, err := e.Extract(context.Background(), extract.Input{
|
|
Platform: "instagram", Consideration: "bezahlt", Caption: "WERBUNG fuer ein Produkt",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Extract: %v", err)
|
|
}
|
|
if !result.Facts.DisclosurePresent {
|
|
t.Fatal("expected case-insensitive match to find the keyword")
|
|
}
|
|
}
|
|
|
|
func TestExtractDetectsDisclosureAfterTruncationCut(t *testing.T) {
|
|
e := extract.NewEngine()
|
|
// 130 Fuellzeichen vor "Werbung" -> jenseits der Instagram-Schwelle (125).
|
|
padding := strings.Repeat("x", 130)
|
|
result, err := e.Extract(context.Background(), extract.Input{
|
|
Platform: "instagram", Consideration: "bezahlt", Caption: padding + " Werbung",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Extract: %v", err)
|
|
}
|
|
if !result.Facts.DisclosurePresent {
|
|
t.Fatal("expected the keyword to still be found even though it's late in the caption")
|
|
}
|
|
if result.Facts.DisclosureBeforeCut {
|
|
t.Error("expected DisclosureBeforeCut = false when the keyword appears after the platform's truncation threshold")
|
|
}
|
|
}
|
|
|
|
func TestExtractNoDisclosureFound(t *testing.T) {
|
|
e := extract.NewEngine()
|
|
result, err := e.Extract(context.Background(), extract.Input{
|
|
Platform: "instagram", Consideration: "keine", Caption: "Ein ganz normaler Tag im Park.",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Extract: %v", err)
|
|
}
|
|
if result.Facts.DisclosurePresent {
|
|
t.Error("expected DisclosurePresent = false when no keyword is present")
|
|
}
|
|
if result.Facts.DisclosureBeforeCut {
|
|
t.Error("expected DisclosureBeforeCut = false when there is no disclosure at all")
|
|
}
|
|
}
|
|
|
|
func TestExtractPassesThroughUnclearConsideration(t *testing.T) {
|
|
e := extract.NewEngine()
|
|
result, err := e.Extract(context.Background(), extract.Input{
|
|
Platform: "instagram", Consideration: "unklar", Caption: "Text ohne klare Angabe",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Extract: %v", err)
|
|
}
|
|
if result.Facts.Consideration != rules.ConsiderationUnclear {
|
|
t.Fatalf("Consideration = %q, want unklar", result.Facts.Consideration)
|
|
}
|
|
}
|
|
|
|
func TestExtractDifferentPlatformThresholds(t *testing.T) {
|
|
e := extract.NewEngine()
|
|
// 140 Fuellzeichen: unter der TikTok-Schwelle (150), aber ueber der
|
|
// Instagram-Schwelle (125) -> gleiche Caption, unterschiedliches Ergebnis.
|
|
padding := strings.Repeat("x", 140)
|
|
caption := padding + " Werbung"
|
|
|
|
igResult, err := e.Extract(context.Background(), extract.Input{Platform: "instagram", Consideration: "bezahlt", Caption: caption})
|
|
if err != nil {
|
|
t.Fatalf("Extract (instagram): %v", err)
|
|
}
|
|
ttResult, err := e.Extract(context.Background(), extract.Input{Platform: "tiktok", Consideration: "bezahlt", Caption: caption})
|
|
if err != nil {
|
|
t.Fatalf("Extract (tiktok): %v", err)
|
|
}
|
|
|
|
if igResult.Facts.DisclosureBeforeCut {
|
|
t.Error("expected DisclosureBeforeCut = false for instagram at this length")
|
|
}
|
|
if !ttResult.Facts.DisclosureBeforeCut {
|
|
t.Error("expected DisclosureBeforeCut = true for tiktok at this length")
|
|
}
|
|
}
|
|
|
|
func TestExtractRawJSONRoundTripsThroughParsePayload(t *testing.T) {
|
|
e := extract.NewEngine()
|
|
result, err := e.Extract(context.Background(), extract.Input{
|
|
Platform: "instagram", Jurisdiction: "DE", Consideration: "sachbezug",
|
|
Caption: "Anzeige: dieses Produkt wurde mir geschenkt",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Extract: %v", err)
|
|
}
|
|
|
|
facts, err := extract.ParsePayload(result.RawJSON, "instagram", "DE")
|
|
if err != nil {
|
|
t.Fatalf("ParsePayload: %v", err)
|
|
}
|
|
if facts != result.Facts {
|
|
t.Fatalf("ParsePayload(Extract().RawJSON) = %+v, want %+v", facts, result.Facts)
|
|
}
|
|
}
|
|
|
|
func TestParsePayloadRejectsInvalidConsideration(t *testing.T) {
|
|
payload := []byte(`{"gegenleistung":"vielleicht","kennzeichnung_vorhanden":false,"kennzeichnung_wortlaut":"","kennzeichnung_vor_kuerzung":false}`)
|
|
if _, err := extract.ParsePayload(payload, "tiktok", "DE"); err == nil {
|
|
t.Fatal("expected error for out-of-enum stored payload, got nil")
|
|
}
|
|
}
|