Compare commits

...
8 Commits
Author SHA1 Message Date
mathiasandClaude Opus 4.8 72cb25111f test(store): address migrations by version, not step count (#8)
CI / Lint / Test / Vet (push) Successful in 12s
CI / Build & Import (push) Successful in 12s
The up/down migration tests stepped a hard-coded number of Steps(-N)/Steps(+N)
down from HEAD and back. The counts assumed a specific latest migration, so
adding one shifted every count by one and unrelated tests (010/011/014) went
red with confusing off-by-one symptoms — a papercut on every new migration.

Drive the schema to an exact version with m.Migrate(version) via two helpers
(headVersion, migrateTo). Each test now steps to just below its target by
version, asserts the down effect, steps up to the target, asserts the up
effect, then restores to the captured HEAD. A migration added on top changes
HEAD but shifts no count, so no test needs editing.

Verified by adding a throwaway migration 017 on top: all four tests stayed
green with zero edits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QbdxXWxLefS5AwLN5eyze
2026-07-02 14:49:02 +02:00
mathiasandClaude Sonnet 4.6 38f222c931 chore: rename Go module path gitea.d-ma.be → git.d-ma.be
CI / Lint / Test / Vet (push) Successful in 11s
CI / Build & Import (push) Successful in 12s
Infra ADR-0004 renamed the Gitea host. Bulk replace across go.mod and
all .go import paths. Build and tests pass unchanged.

Closes #20

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dt6aHEDWRjkK14Voi6HnGh
2026-07-02 14:37:33 +02:00
mathias e042b6d26d chore: add .mcp.json (brain + gitea) — wire tapir for the single-harness workflow
CI / Lint / Test / Vet (push) Successful in 16s
CI / Build & Import (push) Successful in 12s
tapir had no MCP config at all — a hyperguild session here couldn't reach brain
or Gitea, breaking the report-back path before it could start. Matches the
shape now standard post-consolidation (hyperguild#75/#76, infra#178 template
refresh). Prerequisite for tapir#20 being run as the first real workflow test.
2026-07-02 07:00:53 +00:00
mathias c85a32e770 Merge pull request 'LANGUAGE.md: fix LiteLLM gateway never-say (stale endpoints, not piguard)' (#21) from language-md-litellm-fix into main
CI / Lint / Test / Vet (push) Successful in 12s
CI / Build & Import (push) Successful in 10s
2026-06-16 15:45:45 +00:00
mathias b246c0e688 fix(language): LiteLLM gateway never-say — stale endpoints, not piguard
CI / Lint / Test / Vet (pull_request) Successful in 12s
CI / Build & Import (pull_request) Has been skipped
piguard is legitimately in the request path as the reverse proxy; only the
stale endpoint forms (piguard:4000, koala:4000) are wrong. Per brain#6 review
amendment #3 / tapir#18 follow-on.
2026-06-16 15:35:31 +00:00
mathias c7896cb3cc Merge pull request 'LANGUAGE.md pilot: hand-written vocabulary + caveman rubric (Phase 1)' (#19) from language-md-pilot into main
CI / Lint / Test / Vet (push) Successful in 12s
CI / Build & Import (push) Successful in 10s
2026-06-16 15:22:14 +00:00
mathiasandClaude Opus 4.8 b79fb892c8 docs(claude): point to LANGUAGE.md + caveman rubric
CI / Lint / Test / Vet (pull_request) Successful in 13s
CI / Build & Import (pull_request) Has been skipped
Wires the Phase 1 vocabulary pilot into the canonical agent instructions
(tapir#18). CLAUDE.md is canonical here (no .context/ source, no generation
header).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-12 22:54:57 +02:00
mathias c315e3e003 feat(language): add hand-written vocabulary + caveman rubric pilot
Phase 1 of the ubiquitous-language system (tapir#18). Tapir-specific terms
verified against domain/ports code + README/VISION; homelab-core terms (brain
sink, LiteLLM gateway) derived from brain#5 glossary. Tooling-free pilot.
2026-06-12 20:52:23 +00:00
65 changed files with 208 additions and 152 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"mcpServers": {
"brain": {
"type": "http",
"url": "https://brain-mcp.d-ma.be/mcp",
"headers": {
"Authorization": "Bearer ${BRAIN_MCP_TOKEN}"
}
},
"gitea": {
"type": "http",
"url": "https://git-mcp.d-ma.be/mcp",
"headers": {
"Authorization": "Bearer ${GITEA_MCP_TOKEN}"
}
}
}
}
+1
View File
@@ -12,6 +12,7 @@ docs it indexes.
3. `DECISIONS.md` — the ADRs. Decisions are settled here; do not re-litigate without a new ADR. 3. `DECISIONS.md` — the ADRs. Decisions are settled here; do not re-litigate without a new ADR.
4. `docs/architecture/architecture.md`, `docs/data-model.md`, `docs/use-cases/*.feature`. 4. `docs/architecture/architecture.md`, `docs/data-model.md`, `docs/use-cases/*.feature`.
5. `docs/homelab-integration.md` — the concrete endpoints/conventions you'll need. 5. `docs/homelab-integration.md` — the concrete endpoints/conventions you'll need.
6. `LANGUAGE.md` — the project vocabulary. Apply the caveman rubric before destructive operations.
## How to work in this repo ## How to work in this repo
+26
View File
@@ -0,0 +1,26 @@
# Vocabulary (hand-written pilot 2026-06 — will be generated by langgen in Phase 2)
| Term | Means | Never say |
|---|---|---|
| transcript | Captions-first text of a video; the source for summarizing. Shared store keyed by `(provider, video_id)`. | "subtitles file", "the audio" |
| video | Per-user record of a seen video; the unit of work. Not globally deduped. | "the shared video" |
| subscription | A watched channel on a user's connection that Tapir polls for new videos. | "feed", "follow" |
| summary | A video's produced output: summary text + highlights + takeaways + AI provenance. | "transcript" |
| highlight | A notable point pulled from a video (`Summary.Highlights`). | "takeaway" |
| takeaway | An actionable conclusion from a video (`Summary.Takeaways`). | "highlight" |
| sink | A delivery destination for a summary (`store`, `brain`). New one = new adapter. | "the database" |
| AI router | Local-first chain: local Primary → local fallback → external worst-case. | "the API" |
| BYO-AI fallback | User's own external AI key, used only when local fails; opt-in. Without it, content is never sent externally. | "the default AI" |
| brain | Persistent homelab knowledge store; in Tapir, one optional HTTP sink (`brain_ingest`), not the filesystem package. | "the database", "the store" |
| LiteLLM gateway | Local AI gateway (`koala:30401/v1`); the Primary in the AI router. | "piguard:4000", "koala:4000", "the cloud" |
| connection | A connected video account a user authorizes via OAuth; subscriptions hang off it. | "login", "session" |
## Caveman rubric
Before any HIGH/CRITICAL operation, output one line:
`caveman: me <verb> <object>, not <excluded thing>`
Valid iff: (1) only vocabulary terms + plain verbs, (2) a stranger could identify
the exact operation, (3) names one thing explicitly NOT being done.
Examples:
- `caveman: me delete user connection, not the shared transcript`
- `caveman: me send transcript to BYO-AI fallback, not the LiteLLM gateway`
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"strings" "strings"
"testing" "testing"
"gitea.d-ma.be/mathias/tapir/internal/config" "git.d-ma.be/mathias/tapir/internal/config"
) )
// TestChatModelsReuseTheChainLocalFirst: the switcher offers the ADR-022 chain in // TestChatModelsReuseTheChainLocalFirst: the switcher offers the ADR-022 chain in
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"log/slog" "log/slog"
"sync" "sync"
"gitea.d-ma.be/mathias/tapir/internal/runner" "git.d-ma.be/mathias/tapir/internal/runner"
) )
// discoveryRunner runs one user's discovery pass. // discoveryRunner runs one user's discovery pass.
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/runner" "git.d-ma.be/mathias/tapir/internal/runner"
) )
// serialize must guarantee at most one discovery pass runs at a time, so a // serialize must guarantee at most one discovery pass runs at a time, so a
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
// Env var names for the read-only CLI. DSN and user id are never hardcoded — the // Env var names for the read-only CLI. DSN and user id are never hardcoded — the
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
func TestFormatListColumnsAndOrdering(t *testing.T) { func TestFormatListColumnsAndOrdering(t *testing.T) {
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"strings" "strings"
"text/tabwriter" "text/tabwriter"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
// runList prints the user's stored summaries as a table, most recent first. // runList prints the user's stored summaries as a table, most recent first.
+9 -9
View File
@@ -23,15 +23,15 @@ import (
"sync" "sync"
"time" "time"
"gitea.d-ma.be/mathias/tapir/internal/adapters/secrets" "git.d-ma.be/mathias/tapir/internal/adapters/secrets"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/adapters/youtube" "git.d-ma.be/mathias/tapir/internal/adapters/youtube"
"gitea.d-ma.be/mathias/tapir/internal/auth" "git.d-ma.be/mathias/tapir/internal/auth"
"gitea.d-ma.be/mathias/tapir/internal/config" "git.d-ma.be/mathias/tapir/internal/config"
"gitea.d-ma.be/mathias/tapir/internal/metrics" "git.d-ma.be/mathias/tapir/internal/metrics"
"gitea.d-ma.be/mathias/tapir/internal/runner" "git.d-ma.be/mathias/tapir/internal/runner"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
"gitea.d-ma.be/mathias/tapir/internal/web/oidc" "git.d-ma.be/mathias/tapir/internal/web/oidc"
) )
func main() { func main() {
+12 -12
View File
@@ -5,18 +5,18 @@ import (
"fmt" "fmt"
"strings" "strings"
"gitea.d-ma.be/mathias/tapir/internal/adapters/chat" "git.d-ma.be/mathias/tapir/internal/adapters/chat"
"gitea.d-ma.be/mathias/tapir/internal/adapters/llm" "git.d-ma.be/mathias/tapir/internal/adapters/llm"
"gitea.d-ma.be/mathias/tapir/internal/adapters/secrets" "git.d-ma.be/mathias/tapir/internal/adapters/secrets"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/adapters/summarizer" "git.d-ma.be/mathias/tapir/internal/adapters/summarizer"
"gitea.d-ma.be/mathias/tapir/internal/adapters/youtube" "git.d-ma.be/mathias/tapir/internal/adapters/youtube"
"gitea.d-ma.be/mathias/tapir/internal/config" "git.d-ma.be/mathias/tapir/internal/config"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/metrics" "git.d-ma.be/mathias/tapir/internal/metrics"
"gitea.d-ma.be/mathias/tapir/internal/ports" "git.d-ma.be/mathias/tapir/internal/ports"
"gitea.d-ma.be/mathias/tapir/internal/usecase" "git.d-ma.be/mathias/tapir/internal/usecase"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
) )
// videoFetcher adapts the YouTube adapter to web.VideoFetcher for the paste flow // videoFetcher adapts the YouTube adapter to web.VideoFetcher for the paste flow
+1 -1
View File
@@ -3,7 +3,7 @@ package main
import ( import (
"testing" "testing"
"gitea.d-ma.be/mathias/tapir/internal/config" "git.d-ma.be/mathias/tapir/internal/config"
) )
// TestBuildProcessorNilOnIncompleteConfig asserts the queue-only fallback: when a // TestBuildProcessorNilOnIncompleteConfig asserts the queue-only fallback: when a
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"text/tabwriter" "text/tabwriter"
"time" "time"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
// gateThreshold is the Stage-0 gate (VISION/ADR-016): usage in >= 2 distinct // gateThreshold is the Stage-0 gate (VISION/ADR-016): usage in >= 2 distinct
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
var testSince = time.Date(2026, 6, 11, 0, 0, 0, 0, time.UTC) var testSince = time.Date(2026, 6, 11, 0, 0, 0, 0, time.UTC)
+7 -7
View File
@@ -6,13 +6,13 @@ import (
"log/slog" "log/slog"
"time" "time"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/adapters/youtube" "git.d-ma.be/mathias/tapir/internal/adapters/youtube"
"gitea.d-ma.be/mathias/tapir/internal/config" "git.d-ma.be/mathias/tapir/internal/config"
"gitea.d-ma.be/mathias/tapir/internal/ports" "git.d-ma.be/mathias/tapir/internal/ports"
"gitea.d-ma.be/mathias/tapir/internal/runner" "git.d-ma.be/mathias/tapir/internal/runner"
"gitea.d-ma.be/mathias/tapir/internal/usecase" "git.d-ma.be/mathias/tapir/internal/usecase"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
) )
// buildUserRunner constructs a runner.Runner for one user, reusing the same // buildUserRunner constructs a runner.Runner for one user, reusing the same
+2 -2
View File
@@ -11,8 +11,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/runner" "git.d-ma.be/mathias/tapir/internal/runner"
) )
func quietLog() *slog.Logger { func quietLog() *slog.Logger {
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"os" "os"
"strings" "strings"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
// runShow prints the full summary for one video: text, highlights, takeaways, // runShow prints the full summary for one video: text, highlights, takeaways,
+1 -1
View File
@@ -1,4 +1,4 @@
module gitea.d-ma.be/mathias/tapir module git.d-ma.be/mathias/tapir
go 1.26.1 go 1.26.1
+1 -1
View File
@@ -18,7 +18,7 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"gitea.d-ma.be/mathias/tapir/internal/metrics" "git.d-ma.be/mathias/tapir/internal/metrics"
) )
// Completer is the minimal LLM chat surface the Service needs. *llm.Client // Completer is the minimal LLM chat surface the Service needs. *llm.Client
+1 -1
View File
@@ -18,7 +18,7 @@ import (
"path/filepath" "path/filepath"
"sync" "sync"
"gitea.d-ma.be/mathias/tapir/internal/ports" "git.d-ma.be/mathias/tapir/internal/ports"
) )
// FileStore is a SecretStore backed by a single 0600 JSON file mapping opaque // FileStore is a SecretStore backed by a single 0600 JSON file mapping opaque
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"gitea.d-ma.be/mathias/tapir/internal/adapters/secrets" "git.d-ma.be/mathias/tapir/internal/adapters/secrets"
) )
func TestPutThenGet(t *testing.T) { func TestPutThenGet(t *testing.T) {
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
// seedUserRow inserts a bare users row (FK target for a connection) as the // seedUserRow inserts a bare users row (FK target for a connection) as the
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
const ( const (
+48 -37
View File
@@ -3,6 +3,7 @@ package store_test
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"os" "os"
"testing" "testing"
@@ -34,6 +35,30 @@ func fileMigrator(t *testing.T) *migrate.Migrate {
return m return m
} }
// headVersion reports the current (HEAD) schema version so a test can restore
// to it after stepping down, without hard-coding what HEAD is. Adding a
// migration on top changes HEAD but no test that uses this needs editing.
func headVersion(t *testing.T, m *migrate.Migrate) uint {
t.Helper()
v, dirty, err := m.Version()
require.NoError(t, err)
require.False(t, dirty, "schema must not be dirty")
return v
}
// migrateTo drives the schema to an exact version *by version number*, not by
// step count. This is the whole point of the migrate-test design: a migration
// added above the target does not shift any count here, so unrelated tests stay
// green (see issue #8). ErrNoChange (already at that version) is not a failure.
func migrateTo(t *testing.T, m *migrate.Migrate, version uint) {
t.Helper()
err := m.Migrate(version)
if errors.Is(err, migrate.ErrNoChange) {
return
}
require.NoError(t, err)
}
// loginEventsExists reports whether the login_events relation is present. // loginEventsExists reports whether the login_events relation is present.
func loginEventsExists(t *testing.T) bool { func loginEventsExists(t *testing.T) bool {
t.Helper() t.Helper()
@@ -53,25 +78,15 @@ func TestMigration010LoginEventsUpDown(t *testing.T) {
require.True(t, loginEventsExists(t), "login_events must exist at latest migration") require.True(t, loginEventsExists(t), "login_events must exist at latest migration")
m := fileMigrator(t) m := fileMigrator(t)
// 011..016 sit above 010; step them down first so 010 is exercised in isolation. head := headVersion(t, m)
require.NoError(t, m.Steps(-1), "down 016 drops channel_caption_state, login_events intact")
require.True(t, loginEventsExists(t), "016 down leaves login_events intact")
require.NoError(t, m.Steps(-1), "down 015 reshapes transcripts, login_events intact")
require.True(t, loginEventsExists(t), "015 down leaves login_events intact")
require.NoError(t, m.Steps(-1), "down 014 drops channel_title, login_events intact")
require.True(t, loginEventsExists(t), "014 down leaves login_events intact")
require.NoError(t, m.Steps(-1), "down 013 drops channel_errors, login_events intact")
require.True(t, loginEventsExists(t), "013 down leaves login_events intact")
require.NoError(t, m.Steps(-1), "down 012 is a no-op, login_events intact")
require.True(t, loginEventsExists(t), "012 down leaves login_events intact")
require.NoError(t, m.Steps(-1), "down 011 must not touch login_events")
require.True(t, loginEventsExists(t), "011 down leaves login_events intact")
require.NoError(t, m.Steps(-1), "down 010 must drop login_events") migrateTo(t, m, 9) // just below 010 — everything above steps down
require.False(t, loginEventsExists(t), "login_events must be gone after the down migration") require.False(t, loginEventsExists(t), "login_events must be gone after the down migration")
require.NoError(t, m.Steps(7), "up must recreate 010 then re-apply 011..016") migrateTo(t, m, 10) // up 010
require.True(t, loginEventsExists(t), "login_events must be restored after the up migration") require.True(t, loginEventsExists(t), "login_events must be restored after the up migration")
migrateTo(t, m, head) // restore to HEAD for sibling tests
} }
// autoSummarizeDefault reads the users.auto_summarize column default as text // autoSummarizeDefault reads the users.auto_summarize column default as text
@@ -93,21 +108,15 @@ func TestMigration011AutoSummarizeDefaultUpDown(t *testing.T) {
require.Equal(t, "true", autoSummarizeDefault(t), "011 sets the default to TRUE") require.Equal(t, "true", autoSummarizeDefault(t), "011 sets the default to TRUE")
m := fileMigrator(t) m := fileMigrator(t)
require.NoError(t, m.Steps(-1), "down 016 drops channel_caption_state") head := headVersion(t, m)
require.NoError(t, m.Steps(-1), "down 015 reshapes transcripts")
require.NoError(t, m.Steps(-1), "down 014 drops channel_title") migrateTo(t, m, 10) // just below 011 — reverts the column default
require.NoError(t, m.Steps(-1), "down 013 drops channel_errors")
require.NoError(t, m.Steps(-1), "down 012 is a no-op")
require.NoError(t, m.Steps(-1), "down 011 reverts the column default")
require.Equal(t, "false", autoSummarizeDefault(t), "default is FALSE after the down migration") require.Equal(t, "false", autoSummarizeDefault(t), "default is FALSE after the down migration")
require.NoError(t, m.Steps(1), "up 011 re-applies the TRUE default") migrateTo(t, m, 11) // up 011 re-applies the TRUE default
require.Equal(t, "true", autoSummarizeDefault(t)) require.Equal(t, "true", autoSummarizeDefault(t))
require.NoError(t, m.Steps(1), "up 012 runs clean (no FORCE RLS on fresh schema)")
require.NoError(t, m.Steps(1), "up 013 creates channel_errors") migrateTo(t, m, head) // restore to HEAD for sibling tests
require.NoError(t, m.Steps(1), "up 014 recreates channel_title")
require.NoError(t, m.Steps(1), "up 015 reshapes transcripts to shared")
require.NoError(t, m.Steps(1), "up 016 recreates channel_caption_state (HEAD)")
} }
// channelTitleExists reports whether videos.channel_title is present. // channelTitleExists reports whether videos.channel_title is present.
@@ -127,17 +136,15 @@ func TestMigration014VideoChannelTitleUpDown(t *testing.T) {
require.True(t, channelTitleExists(t), "channel_title exists at latest migration") require.True(t, channelTitleExists(t), "channel_title exists at latest migration")
m := fileMigrator(t) m := fileMigrator(t)
require.NoError(t, m.Steps(-1), "down 016 drops channel_caption_state, channel_title intact") head := headVersion(t, m)
require.True(t, channelTitleExists(t), "016 down leaves channel_title intact")
require.NoError(t, m.Steps(-1), "down 015 reshapes transcripts, channel_title intact") migrateTo(t, m, 13) // just below 014 — drops channel_title
require.True(t, channelTitleExists(t), "015 down leaves channel_title intact")
require.NoError(t, m.Steps(-1), "down 014 must drop channel_title")
require.False(t, channelTitleExists(t), "channel_title must be gone after the down migration") require.False(t, channelTitleExists(t), "channel_title must be gone after the down migration")
require.NoError(t, m.Steps(1), "up 014 must recreate channel_title") migrateTo(t, m, 14) // up 014 recreates channel_title
require.True(t, channelTitleExists(t), "channel_title must be restored after the up migration") require.True(t, channelTitleExists(t), "channel_title must be restored after the up migration")
require.NoError(t, m.Steps(1), "up 015 restores the shared transcripts shape")
require.NoError(t, m.Steps(1), "up 016 recreates channel_caption_state (HEAD)") migrateTo(t, m, head) // restore to HEAD for sibling tests
} }
// TestMigration012FixAutoSummarizeRLS proves 012 runs cleanly and flips any // TestMigration012FixAutoSummarizeRLS proves 012 runs cleanly and flips any
@@ -148,7 +155,11 @@ func TestMigration012FixAutoSummarizeRLS(t *testing.T) {
// Round-trip: down 012, then up 012 — must be idempotent. // Round-trip: down 012, then up 012 — must be idempotent.
m := fileMigrator(t) m := fileMigrator(t)
require.NoError(t, m.Steps(-1), "down 012 must not error") head := headVersion(t, m)
require.NoError(t, m.Steps(1), "up 012 must re-apply cleanly")
migrateTo(t, m, 11) // down 012 must not error
migrateTo(t, m, 12) // up 012 must re-apply cleanly
require.Equal(t, "true", autoSummarizeDefault(t), "default still TRUE after 012 re-applied") require.Equal(t, "true", autoSummarizeDefault(t), "default still TRUE after 012 re-applied")
migrateTo(t, m, head) // restore to HEAD for sibling tests
} }
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
// seedVideo inserts a videos row whose id matches a summary's video_id, so the // seedVideo inserts a videos row whose id matches a summary's video_id, so the
+1 -1
View File
@@ -24,7 +24,7 @@ import (
_ "github.com/jackc/pgx/v5/stdlib" // register the "pgx" database/sql driver for migrate _ "github.com/jackc/pgx/v5/stdlib" // register the "pgx" database/sql driver for migrate
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
) )
//go:embed migrations/*.sql //go:embed migrations/*.sql
+3 -3
View File
@@ -11,9 +11,9 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/ports" "git.d-ma.be/mathias/tapir/internal/ports"
) )
// Static check: Store satisfies the Sink port. // Static check: Store satisfies the Sink port.
@@ -8,7 +8,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
// seedBareVideo inserts a videos row with no summary, so the all-videos read and // seedBareVideo inserts a videos row with no summary, so the all-videos read and
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
) )
// GetTranscript returns the shared, stored transcript for a video keyed by the // GetTranscript returns the shared, stored transcript for a video keyed by the
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
func TestSetTranscriptStatus_RoundTrip(t *testing.T) { func TestSetTranscriptStatus_RoundTrip(t *testing.T) {
+3 -3
View File
@@ -6,9 +6,9 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/ports" "git.d-ma.be/mathias/tapir/internal/ports"
) )
// Static check: Store satisfies the shared TranscriptStore port (ADR-021). // Static check: Store satisfies the shared TranscriptStore port (ADR-021).
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
) )
// UpsertVideo persists a video's metadata and returns its durable store id (the // UpsertVideo persists a video's metadata and returns its durable store id (the
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
) )
func ytVideo(userID, provVideoID, title string) domain.Video { func ytVideo(userID, provVideoID, title string) domain.Video {
+2 -2
View File
@@ -18,8 +18,8 @@ import (
"time" "time"
"unicode/utf8" "unicode/utf8"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/metrics" "git.d-ma.be/mathias/tapir/internal/metrics"
) )
// Completer is the minimal LLM chat surface the Summarizer needs. // Completer is the minimal LLM chat surface the Summarizer needs.
@@ -12,9 +12,9 @@ import (
"strings" "strings"
"testing" "testing"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/metrics" "git.d-ma.be/mathias/tapir/internal/metrics"
"gitea.d-ma.be/mathias/tapir/internal/ports" "git.d-ma.be/mathias/tapir/internal/ports"
) )
// TestSummarizerRecordsMetric verifies the summarizer→metrics wiring (ADR-030) // TestSummarizerRecordsMetric verifies the summarizer→metrics wiring (ADR-030)
+2 -2
View File
@@ -12,8 +12,8 @@ import (
"strings" "strings"
"time" "time"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/metrics" "git.d-ma.be/mathias/tapir/internal/metrics"
) )
// defaultPlayerBaseURL is the InnerTube / watch-page host. Overridable via // defaultPlayerBaseURL is the InnerTube / watch-page host. Overridable via
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"net/http" "net/http"
"testing" "testing"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
) )
func TestVideoByID(t *testing.T) { func TestVideoByID(t *testing.T) {
+2 -2
View File
@@ -27,8 +27,8 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/ports" "git.d-ma.be/mathias/tapir/internal/ports"
) )
// defaultBaseURL is the YouTube Data API v3 root. Overridable via Config.BaseURL // defaultBaseURL is the YouTube Data API v3 root. Overridable via Config.BaseURL
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
) )
// --- fakes ------------------------------------------------------------------ // --- fakes ------------------------------------------------------------------
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gitea.d-ma.be/mathias/tapir/internal/auth" "git.d-ma.be/mathias/tapir/internal/auth"
) )
// fakeWriter is a TokenWriter capturing the persisted (ref, value). // fakeWriter is a TokenWriter capturing the persisted (ref, value).
+1 -1
View File
@@ -7,7 +7,7 @@ package ports
import ( import (
"context" "context"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
) )
// VideoSource is a video platform Tapir watches (YouTube, Vimeo). // VideoSource is a video platform Tapir watches (YouTube, Vimeo).
+3 -3
View File
@@ -19,9 +19,9 @@ import (
"slices" "slices"
"time" "time"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/ports" "git.d-ma.be/mathias/tapir/internal/ports"
"gitea.d-ma.be/mathias/tapir/internal/usecase" "git.d-ma.be/mathias/tapir/internal/usecase"
) )
// passCandidate is a video that passed all pre-filters (seen/manual/backoff) // passCandidate is a video that passed all pre-filters (seen/manual/backoff)
+3 -3
View File
@@ -9,9 +9,9 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/runner" "git.d-ma.be/mathias/tapir/internal/runner"
"gitea.d-ma.be/mathias/tapir/internal/usecase" "git.d-ma.be/mathias/tapir/internal/usecase"
) )
const testUser = "11111111-1111-1111-1111-111111111111" const testUser = "11111111-1111-1111-1111-111111111111"
+2 -2
View File
@@ -14,8 +14,8 @@ import (
"fmt" "fmt"
"sync" "sync"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/ports" "git.d-ma.be/mathias/tapir/internal/ports"
) )
// ErrNotImplemented marks scaffold methods awaiting implementation. // ErrNotImplemented marks scaffold methods awaiting implementation.
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context" "context"
"testing" "testing"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
) )
// These tests pin the ADR-021 read-stored-first behaviour at the engine core: // These tests pin the ADR-021 read-stored-first behaviour at the engine core:
+1 -1
View File
@@ -3,7 +3,7 @@ package web
import ( import (
"net/http" "net/http"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
// handleAccount renders the account page: the user's display name, the // handleAccount renders the account page: the user's display name, the
+2 -2
View File
@@ -9,8 +9,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
) )
// fakeSecrets is a SecretRemover that records the refs it was asked to delete, so // fakeSecrets is a SecretRemover that records the refs it was asked to delete, so
+2 -2
View File
@@ -6,8 +6,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"gitea.d-ma.be/mathias/tapir/internal/adapters/chat" "git.d-ma.be/mathias/tapir/internal/adapters/chat"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
// Chatter is the per-video chat backend (ADR-027). *chat.Service satisfies it; // Chatter is the per-video chat backend (ADR-027). *chat.Service satisfies it;
+4 -4
View File
@@ -12,10 +12,10 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/chat" "git.d-ma.be/mathias/tapir/internal/adapters/chat"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
) )
// videoZ is a video id used by the isolation test for a DIFFERENT user's video. // videoZ is a video id used by the isolation test for a DIFFERENT user's video.
+2 -2
View File
@@ -12,8 +12,8 @@ import (
"sync" "sync"
"time" "time"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/auth" "git.d-ma.be/mathias/tapir/internal/auth"
) )
// Connections is the narrow write port the connect flow depends on (Clean // Connections is the narrow write port the connect flow depends on (Clean
+3 -3
View File
@@ -11,9 +11,9 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/auth" "git.d-ma.be/mathias/tapir/internal/auth"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
) )
// fakeWriter is a TokenWriter capturing the persisted (ref, value). // fakeWriter is a TokenWriter capturing the persisted (ref, value).
+2 -2
View File
@@ -11,8 +11,8 @@ import (
"github.com/a-h/templ" "github.com/a-h/templ"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
) )
// Store is the read/write surface the web handlers depend on — a narrow port over // Store is the read/write surface the web handlers depend on — a narrow port over
+3 -3
View File
@@ -16,9 +16,9 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
) )
// dsn points at the in-process Postgres started in TestMain. Handler tests run // dsn points at the in-process Postgres started in TestMain. Handler tests run
+2 -2
View File
@@ -27,8 +27,8 @@ import (
"github.com/coreos/go-oidc/v3/oidc" "github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"gitea.d-ma.be/mathias/tapir/internal/metrics" "git.d-ma.be/mathias/tapir/internal/metrics"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
) )
// Config is the OIDC + session configuration. cmd/tapir maps these from // Config is the OIDC + session configuration. cmd/tapir maps these from
+2 -2
View File
@@ -14,8 +14,8 @@ import (
josev4 "github.com/go-jose/go-jose/v4" josev4 "github.com/go-jose/go-jose/v4"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
"gitea.d-ma.be/mathias/tapir/internal/web/oidc" "git.d-ma.be/mathias/tapir/internal/web/oidc"
) )
const ( const (
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"sync" "sync"
"time" "time"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
) )
// randToken returns a URL-safe 256-bit random string for session IDs, OIDC // randToken returns a URL-safe 256-bit random string for session IDs, OIDC
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
) )
// fakeFetcher is a web.VideoFetcher returning a fixed video (or an error), // fakeFetcher is a web.VideoFetcher returning a fixed video (or an error),
+1 -1
View File
@@ -3,7 +3,7 @@ package web
import ( import (
"bytes" "bytes"
"context" "context"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
"strings" "strings"
"testing" "testing"
) )
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"context" "context"
"sync" "sync"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
) )
// Processor runs the core summarization use case for a single already-discovered // Processor runs the core summarization use case for a single already-discovered
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/jackc/pgx/v5/pgxpool" "github.com/jackc/pgx/v5/pgxpool"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
) )
// fakeProcessor records ProcessVideo calls. With block set it parks until the // fakeProcessor records ProcessVideo calls. With block set it parks until the
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
func renderVideoCard(t *testing.T, r store.SummaryRow) string { func renderVideoCard(t *testing.T, r store.SummaryRow) string {
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"github.com/a-h/templ" "github.com/a-h/templ"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
// youtubeIDRe matches a canonical 11-char YouTube video id (the provider's // youtubeIDRe matches a canonical 11-char YouTube video id (the provider's
+1 -1
View File
@@ -12,7 +12,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store" "git.d-ma.be/mathias/tapir/internal/adapters/store"
) )
// Layout is the shared HTML shell. HTMX drives the progressive interactions // Layout is the shared HTML shell. HTMX drives the progressive interactions
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/web" "git.d-ma.be/mathias/tapir/internal/web"
) )
// fakeAuth is a configurable web.Auth for the landing-page tests: it reports a // fakeAuth is a configurable web.Auth for the landing-page tests: it reports a
+2 -2
View File
@@ -12,8 +12,8 @@ import (
"testing" "testing"
"time" "time"
"gitea.d-ma.be/mathias/tapir/internal/domain" "git.d-ma.be/mathias/tapir/internal/domain"
"gitea.d-ma.be/mathias/tapir/internal/usecase" "git.d-ma.be/mathias/tapir/internal/usecase"
) )
// --- fake adapters --------------------------------------------------------- // --- fake adapters ---------------------------------------------------------