repos
/ orchard main

orchard

mirror

Every site I host, in one repo, along with the Cloudflare Tunnel and Caddy that front them. It's all Go, Vite, and SQLite, and it runs on a desktop at home with nothing listening on an inbound port.

blogbuncaddycloudflare-tunneldockergogolanghomelabhtml-templatemonorepoself-hostedseosqlitestatic-sitetypstuptime-monitoringviteweb-analytics

3.3 KB · 99 lines · Go Raw History
 1package skills
 2
 3import (
 4	"context"
 5	"errors"
 6	"net/http"
 7	"os"
 8	"strings"
 9	"testing"
10	"time"
11)
12
13// TestSkillsRun exercises each skill against its own upstream. None of these
14// touch the search engine, so they cost nothing against its rate limit, but
15// they are still live calls and stay behind the same flag.
16func TestSkillsRun(t *testing.T) {
17	if os.Getenv("SEARCH_LIVE") == "" {
18		t.Skip("set SEARCH_LIVE=1 to run skills against their live sources")
19	}
20	d := Deps{
21		HTTP: &http.Client{Timeout: 30 * time.Second},
22		UA:   "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
23		Now:  time.Now,
24	}
25	cases := []struct {
26		skill Skill
27		q     string
28		// want is a fragment the answer has to carry, so a skill that returns
29		// a well formed answer about the wrong thing still fails.
30		want string
31	}{
32		{Calculator{}, "what is 30 * 27", "810"},
33		{Convert{}, "30 celsius to fahrenheit", "86.0"},
34		{Convert{}, "how many km in 5 miles", "8.05"},
35		{Convert{}, "100 usd in eur", "EUR"},
36		{Time{}, "what time is it in tokyo", "Tokyo"},
37		{Time{}, "how many days until christmas", "Christmas"},
38		{Weather{}, "what is the weather this weekend", "°F"},
39		{Markets{}, "what is the S&P 500 right now", "S&P 500"},
40		{Odds{}, "what are the odds on the us open", "%"},
41		{Sports{}, "did Liverpool win their last match", "Liverpool"},
42	}
43	for _, c := range cases {
44		name := c.skill.Card().Name + "/" + c.q
45		t.Run(name, func(t *testing.T) {
46			ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
47			defer cancel()
48			res, err := c.skill.Run(ctx, c.q, d)
49			if errors.Is(err, errThrottled) {
50				// Not a code failure. Running this suite repeatedly is itself
51				// enough to get rate limited, which is the thing the cache and
52				// the narrowed sweep exist to avoid in normal use.
53				t.Skipf("%s is throttling, try again later", c.skill.Card().Name)
54			}
55			if err != nil {
56				t.Fatalf("error: %v", err)
57			}
58			if res == nil {
59				t.Fatal("declined the question it is meant to answer")
60			}
61			if !strings.Contains(res.Text, c.want) {
62				t.Errorf("answer does not carry %q:\n%s", c.want, res.Text)
63			}
64			t.Logf("%s in %s\n%s", res.Skill, res.Elapsed, res.Text)
65		})
66	}
67}
68
69// TestSkillsDecline is the other half, and the more important one. A skill
70// handed a question it cannot answer has to return nothing rather than answer
71// about something else, because the router will occasionally send it one.
72func TestSkillsDecline(t *testing.T) {
73	d := Deps{HTTP: &http.Client{Timeout: 15 * time.Second}, Now: time.Now}
74	cases := []struct {
75		skill Skill
76		q     string
77	}{
78		{Calculator{}, "how many calories in a banana"},
79		{Convert{}, "what currency does japan use"},
80		{Convert{}, "why is the euro weak"},
81		{Time{}, "when was the declaration of independence signed"},
82		{Weather{}, "what is the weather in tokyo"},
83		{Markets{}, "should i buy a house"},
84		{Odds{}, "the"},
85		{Odds{}, "how is the us open going"},
86		{Odds{}, "what's the score in the us open"},
87	}
88	for _, c := range cases {
89		t.Run(c.skill.Card().Name+"/"+c.q, func(t *testing.T) {
90			ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
91			defer cancel()
92			res, _ := c.skill.Run(ctx, c.q, d)
93			if res != nil {
94				t.Errorf("answered a question it should have declined:\n%s", res.Text)
95			}
96		})
97	}
98}