build source

This commit is contained in:
build 2026-04-16 04:16:36 +00:00
commit ee1fec43ed
4171 changed files with 1351288 additions and 0 deletions

BIN
internal/model/._model.go Normal file

Binary file not shown.

Binary file not shown.

456
internal/model/model.go Normal file
View file

@ -0,0 +1,456 @@
package model
import (
"fmt"
"time"
"k8s.io/apimachinery/pkg/api/resource"
)
// User represents a tenant who can create and manage dev pods.
type User struct {
ID string `json:"id"`
CreatedAt time.Time `json:"created_at"`
Quota Quota `json:"quota"`
}
// Quota defines resource limits for a user.
type Quota struct {
MaxConcurrentPods int `json:"max_concurrent_pods"`
MaxCPUPerPod int `json:"max_cpu_per_pod"`
MaxRAMGBPerPod int `json:"max_ram_gb_per_pod"`
MonthlyPodHours int `json:"monthly_pod_hours"`
MonthlyAIRequests int `json:"monthly_ai_requests"`
}
// DefaultQuota returns the default quota for new users.
func DefaultQuota() Quota {
return Quota{
MaxConcurrentPods: 3,
MaxCPUPerPod: 8,
MaxRAMGBPerPod: 16,
MonthlyPodHours: 500,
MonthlyAIRequests: 10000,
}
}
// ApplyOverrides applies non-nil fields from an UpdateQuotasRequest onto q.
func (q *Quota) ApplyOverrides(o UpdateQuotasRequest) {
if o.MaxConcurrentPods != nil {
q.MaxConcurrentPods = *o.MaxConcurrentPods
}
if o.MaxCPUPerPod != nil {
q.MaxCPUPerPod = *o.MaxCPUPerPod
}
if o.MaxRAMGBPerPod != nil {
q.MaxRAMGBPerPod = *o.MaxRAMGBPerPod
}
if o.MonthlyPodHours != nil {
q.MonthlyPodHours = *o.MonthlyPodHours
}
if o.MonthlyAIRequests != nil {
q.MonthlyAIRequests = *o.MonthlyAIRequests
}
}
// Validate checks that all quota values are positive.
func (q *Quota) Validate() error {
if q.MaxConcurrentPods < 1 {
return fmt.Errorf("max_concurrent_pods must be at least 1")
}
if q.MaxCPUPerPod < 1 {
return fmt.Errorf("max_cpu_per_pod must be at least 1")
}
if q.MaxRAMGBPerPod < 1 {
return fmt.Errorf("max_ram_gb_per_pod must be at least 1")
}
if q.MonthlyPodHours < 1 {
return fmt.Errorf("monthly_pod_hours must be at least 1")
}
if q.MonthlyAIRequests < 0 {
return fmt.Errorf("monthly_ai_requests must be non-negative")
}
return nil
}
// APIKey represents an authentication key for API access.
type APIKey struct {
KeyHash string `json:"-"`
UserID string `json:"user_id"`
Role string `json:"role"`
CreatedAt time.Time `json:"created_at"`
LastUsedAt time.Time `json:"last_used_at,omitempty"`
}
// Role constants for API keys.
const (
RoleAdmin = "admin"
RoleUser = "user"
)
// APIKeyPrefix is the prefix for generated API keys.
const APIKeyPrefix = "dpk_"
// Pod represents a running dev pod.
type Pod struct {
User string `json:"user"`
Name string `json:"name"`
Tools string `json:"tools"`
CPUReq string `json:"cpu_req"`
CPULimit string `json:"cpu_limit"`
MemReq string `json:"mem_req"`
MemLimit string `json:"mem_limit"`
Task string `json:"task,omitempty"`
Status string `json:"status,omitempty"`
Age string `json:"age,omitempty"`
CPUUsage string `json:"cpu_usage,omitempty"`
MemUsage string `json:"mem_usage,omitempty"`
URL string `json:"url,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
}
// UsageRecord tracks resource usage events.
type UsageRecord struct {
ID int64 `json:"id"`
UserID string `json:"user_id"`
PodName string `json:"pod_name"`
EventType string `json:"event_type"`
Value float64 `json:"value"`
RecordedAt time.Time `json:"recorded_at"`
}
// Event type constants for usage records.
const (
EventPodStart = "pod_start"
EventPodStop = "pod_stop"
EventCPUSample = "cpu_sample"
EventMemSample = "mem_sample"
EventAIRequest = "ai_request"
)
// UsageSummary holds aggregated usage for a billing period.
type UsageSummary struct {
PodHours float64 `json:"pod_hours"`
CPUHours float64 `json:"cpu_hours"`
AIRequests int64 `json:"ai_requests"`
BudgetUsedPct float64 `json:"budget_used_pct"`
}
// DailyUsage holds usage data for a single day.
type DailyUsage struct {
Date string `json:"date"`
PodHours float64 `json:"pod_hours"`
CPUHours float64 `json:"cpu_hours"`
AIRequests int64 `json:"ai_requests"`
}
// UserUsageSummary holds usage data for a single user.
type UserUsageSummary struct {
UserID string `json:"user_id"`
Usage UsageSummary `json:"usage"`
}
// CreatePodRequest is the request body for creating a pod.
type CreatePodRequest struct {
User string `json:"user"`
Pod string `json:"pod"`
Tools string `json:"tools"`
CPUReq string `json:"cpu_req"`
CPULimit string `json:"cpu_limit"`
MemReq string `json:"mem_req"`
MemLimit string `json:"mem_limit"`
Task string `json:"task"`
TailscaleKey string `json:"tailscale_key,omitempty"`
}
// Validate checks that a CreatePodRequest has required fields and valid values.
func (r *CreatePodRequest) Validate() error {
if r.User == "" {
return fmt.Errorf("user is required")
}
if r.Pod == "" {
return fmt.Errorf("pod name is required")
}
if !isValidName(r.User) {
return fmt.Errorf("user must be lowercase alphanumeric with hyphens, 1-63 chars")
}
if len(NamespaceName(r.User)) > 63 {
return fmt.Errorf("user name too long: derived namespace %q exceeds 63-char k8s limit (max user name: 59 chars)", NamespaceName(r.User))
}
if !isValidName(r.Pod) {
return fmt.Errorf("pod name must be lowercase alphanumeric with hyphens, 1-63 chars")
}
if len(ServiceName(r.Pod)) > 63 {
return fmt.Errorf("pod name too long: derived service name %q exceeds 63-char k8s limit (max pod name: 51 chars)", ServiceName(r.Pod))
}
for _, pair := range []struct{ field, value string }{
{"cpu_req", r.CPUReq},
{"cpu_limit", r.CPULimit},
{"mem_req", r.MemReq},
{"mem_limit", r.MemLimit},
} {
if pair.value != "" {
if _, err := resource.ParseQuantity(pair.value); err != nil {
return fmt.Errorf("%s is not a valid resource quantity: %s", pair.field, pair.value)
}
}
}
return nil
}
// CreateUserRequest is the request body for creating a user.
// Quotas uses pointer fields so that zero values (e.g. monthly_ai_requests: 0)
// can be distinguished from omitted fields.
type CreateUserRequest struct {
User string `json:"user"`
Quotas *UpdateQuotasRequest `json:"quotas,omitempty"`
}
// Validate checks that a CreateUserRequest has required fields.
func (r *CreateUserRequest) Validate() error {
if r.User == "" {
return fmt.Errorf("user is required")
}
if !isValidName(r.User) {
return fmt.Errorf("user must be lowercase alphanumeric with hyphens, 1-63 chars")
}
if len(NamespaceName(r.User)) > 63 {
return fmt.Errorf("user name too long: derived namespace %q exceeds 63-char k8s limit (max user name: 59 chars)", NamespaceName(r.User))
}
return nil
}
// UpdateQuotasRequest is the request body for updating user quotas.
type UpdateQuotasRequest struct {
MaxConcurrentPods *int `json:"max_concurrent_pods,omitempty"`
MaxCPUPerPod *int `json:"max_cpu_per_pod,omitempty"`
MaxRAMGBPerPod *int `json:"max_ram_gb_per_pod,omitempty"`
MonthlyPodHours *int `json:"monthly_pod_hours,omitempty"`
MonthlyAIRequests *int `json:"monthly_ai_requests,omitempty"`
}
// UpdatePodRequest is the request body for updating pod resources (delete + recreate).
type UpdatePodRequest struct {
Tools *string `json:"tools,omitempty"`
CPUReq *string `json:"cpu_req,omitempty"`
CPULimit *string `json:"cpu_limit,omitempty"`
MemReq *string `json:"mem_req,omitempty"`
MemLimit *string `json:"mem_limit,omitempty"`
Task *string `json:"task,omitempty"`
}
// Validate checks that resource quantities in UpdatePodRequest are valid.
func (r *UpdatePodRequest) Validate() error {
for _, pair := range []struct{ field string; value *string }{
{"cpu_req", r.CPUReq},
{"cpu_limit", r.CPULimit},
{"mem_req", r.MemReq},
{"mem_limit", r.MemLimit},
} {
if pair.value != nil {
if _, err := resource.ParseQuantity(*pair.value); err != nil {
return fmt.Errorf("%s is not a valid resource quantity: %s", pair.field, *pair.value)
}
}
}
return nil
}
// Validate checks that quota values are positive when provided.
func (r *UpdateQuotasRequest) Validate() error {
if r.MaxConcurrentPods != nil && *r.MaxConcurrentPods < 1 {
return fmt.Errorf("max_concurrent_pods must be at least 1")
}
if r.MaxCPUPerPod != nil && *r.MaxCPUPerPod < 1 {
return fmt.Errorf("max_cpu_per_pod must be at least 1")
}
if r.MaxRAMGBPerPod != nil && *r.MaxRAMGBPerPod < 1 {
return fmt.Errorf("max_ram_gb_per_pod must be at least 1")
}
if r.MonthlyPodHours != nil && *r.MonthlyPodHours < 1 {
return fmt.Errorf("monthly_pod_hours must be at least 1")
}
if r.MonthlyAIRequests != nil && *r.MonthlyAIRequests < 0 {
return fmt.Errorf("monthly_ai_requests must be non-negative")
}
return nil
}
// isValidName checks if a string is a valid k8s-compatible name:
// lowercase alphanumeric and hyphens, 1-63 chars, must start and end with alphanumeric.
func isValidName(name string) bool {
if len(name) == 0 || len(name) > 63 {
return false
}
for i, c := range name {
isAlphaNum := (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9')
isHyphen := c == '-'
if !isAlphaNum && !isHyphen {
return false
}
if (i == 0 || i == len(name)-1) && isHyphen {
return false
}
}
return true
}
// NamespaceName returns the k8s namespace for a user.
func NamespaceName(user string) string {
return "dev-" + user
}
// PodFullName returns the full k8s pod name.
func PodFullName(podName string) string {
return "dev-pod-" + podName
}
// ServiceName returns the k8s service name for a pod.
func ServiceName(podName string) string {
return "dev-pod-" + podName + "-svc"
}
// PodURL returns the external URL for a dev pod.
func PodURL(domain, user, podName string) string {
return fmt.Sprintf("https://%s/@%s/%s/", domain, user, podName)
}
// ClusterStatus holds overall cluster status including per-node details.
type ClusterStatus struct {
Nodes []NodeStatus `json:"nodes"`
Total ResourceSummary `json:"total"`
}
// NodeStatus describes a single cluster node's capacity and usage.
type NodeStatus struct {
Name string `json:"name"`
Status string `json:"status"`
CPUCapacity string `json:"cpu_capacity"`
CPUAllocatable string `json:"cpu_allocatable"`
MemCapacity string `json:"mem_capacity"`
MemAllocatable string `json:"mem_allocatable"`
CPUUsage string `json:"cpu_usage,omitempty"`
MemUsage string `json:"mem_usage,omitempty"`
}
// ResourceSummary holds aggregated resource totals for the cluster.
type ResourceSummary struct {
CPUCapacity string `json:"cpu_capacity"`
CPUAllocatable string `json:"cpu_allocatable"`
MemCapacity string `json:"mem_capacity"`
MemAllocatable string `json:"mem_allocatable"`
}
// CacheStat describes a cache service PVC in the dev-infra namespace.
type CacheStat struct {
Name string `json:"name"`
PVCName string `json:"pvc_name"`
Capacity string `json:"capacity"`
Status string `json:"status"`
}
// RunnerStatus represents the lifecycle state of a runner.
type RunnerStatus string
const (
RunnerStatusReceived RunnerStatus = "received"
RunnerStatusPodCreating RunnerStatus = "pod_creating"
RunnerStatusRunnerRegistered RunnerStatus = "runner_registered"
RunnerStatusJobClaimed RunnerStatus = "job_claimed"
RunnerStatusCompleted RunnerStatus = "completed"
RunnerStatusFailed RunnerStatus = "failed"
RunnerStatusCleanupPending RunnerStatus = "cleanup_pending"
)
var validRunnerTransitions = map[RunnerStatus][]RunnerStatus{
RunnerStatusReceived: {RunnerStatusPodCreating, RunnerStatusFailed},
RunnerStatusPodCreating: {RunnerStatusRunnerRegistered, RunnerStatusFailed},
RunnerStatusRunnerRegistered: {RunnerStatusJobClaimed, RunnerStatusFailed},
RunnerStatusJobClaimed: {RunnerStatusCompleted, RunnerStatusFailed},
RunnerStatusCompleted: {RunnerStatusCleanupPending},
RunnerStatusFailed: {RunnerStatusCleanupPending},
}
// CanTransitionTo checks if a status transition is valid.
func (s RunnerStatus) CanTransitionTo(next RunnerStatus) bool {
for _, valid := range validRunnerTransitions[s] {
if valid == next {
return true
}
}
return false
}
// IsTerminal returns true if the runner is in a terminal state.
func (s RunnerStatus) IsTerminal() bool {
return s == RunnerStatusCompleted || s == RunnerStatusFailed
}
// Runner represents an ephemeral builder pod managed by the API.
type Runner struct {
ID string `json:"id"`
User string `json:"user"`
RepoURL string `json:"repo_url"`
Branch string `json:"branch"`
Tools string `json:"tools"`
Task string `json:"task"`
Status RunnerStatus `json:"status"`
ForgejoRunnerID string `json:"forgejo_runner_id,omitempty"`
WebhookDeliveryID string `json:"webhook_delivery_id,omitempty"`
PodName string `json:"pod_name,omitempty"`
CPUReq string `json:"cpu_req"`
MemReq string `json:"mem_req"`
CreatedAt time.Time `json:"created_at"`
ClaimedAt *time.Time `json:"claimed_at,omitempty"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
}
// CreateRunnerRequest is the request body for creating a runner.
type CreateRunnerRequest struct {
User string `json:"user"`
Repo string `json:"repo"`
Branch string `json:"branch"`
Tools string `json:"tools"`
Task string `json:"task"`
CPUReq string `json:"cpu_req"`
MemReq string `json:"mem_req"`
WebhookDeliveryID string `json:"webhook_delivery_id"`
}
// Validate checks that a CreateRunnerRequest has required fields.
func (r *CreateRunnerRequest) Validate() error {
if r.User == "" {
return fmt.Errorf("user is required")
}
if r.Repo == "" {
return fmt.Errorf("repo is required")
}
if !isValidName(r.User) {
return fmt.Errorf("user must be lowercase alphanumeric with hyphens, 1-63 chars")
}
return nil
}
// UpdateRunnerStatusRequest is the request body for updating runner status.
type UpdateRunnerStatusRequest struct {
Status RunnerStatus `json:"status"`
ForgejoRunnerID string `json:"forgejo_runner_id,omitempty"`
}
// Validate checks the status update request.
func (r *UpdateRunnerStatusRequest) Validate() error {
switch r.Status {
case RunnerStatusPodCreating, RunnerStatusRunnerRegistered, RunnerStatusJobClaimed,
RunnerStatusCompleted, RunnerStatusFailed, RunnerStatusCleanupPending:
return nil
default:
return fmt.Errorf("invalid status: %s", r.Status)
}
}
// RunnerPodName returns the k8s pod name for a runner.
func RunnerPodName(runnerID string) string {
return "dev-pod-" + runnerID
}

View file

@ -0,0 +1,303 @@
package model
import (
"strings"
"testing"
)
func intPtr(v int) *int { return &v }
func TestCreatePodRequest_Validate(t *testing.T) {
tests := []struct {
name string
req CreatePodRequest
wantErr bool
errMsg string
}{
{
name: "valid request",
req: CreatePodRequest{User: "alice", Pod: "main", Tools: "go,rust"},
wantErr: false,
},
{
name: "minimal valid request",
req: CreatePodRequest{User: "bob", Pod: "dev1"},
wantErr: false,
},
{
name: "missing user",
req: CreatePodRequest{Pod: "main"},
wantErr: true,
errMsg: "user is required",
},
{
name: "missing pod name",
req: CreatePodRequest{User: "alice"},
wantErr: true,
errMsg: "pod name is required",
},
{
name: "invalid user - uppercase",
req: CreatePodRequest{User: "Alice", Pod: "main"},
wantErr: true,
errMsg: "user must be lowercase alphanumeric with hyphens",
},
{
name: "invalid user - starts with hyphen",
req: CreatePodRequest{User: "-alice", Pod: "main"},
wantErr: true,
errMsg: "user must be lowercase alphanumeric with hyphens",
},
{
name: "invalid user - ends with hyphen",
req: CreatePodRequest{User: "alice-", Pod: "main"},
wantErr: true,
errMsg: "user must be lowercase alphanumeric with hyphens",
},
{
name: "invalid user - special chars",
req: CreatePodRequest{User: "al!ce", Pod: "main"},
wantErr: true,
errMsg: "user must be lowercase alphanumeric with hyphens",
},
{
name: "invalid pod name - spaces",
req: CreatePodRequest{User: "alice", Pod: "my pod"},
wantErr: true,
errMsg: "pod name must be lowercase alphanumeric with hyphens",
},
{
name: "valid user with hyphens",
req: CreatePodRequest{User: "alice-dev", Pod: "my-pod"},
wantErr: false,
},
{
name: "user name too long for namespace",
req: CreatePodRequest{User: strings.Repeat("a", 60), Pod: "main"},
wantErr: true,
errMsg: "user name too long",
},
{
name: "pod name too long for service",
req: CreatePodRequest{User: "alice", Pod: strings.Repeat("a", 52)},
wantErr: true,
errMsg: "pod name too long",
},
{
name: "max valid user name (59 chars)",
req: CreatePodRequest{User: strings.Repeat("a", 59), Pod: "main"},
wantErr: false,
},
{
name: "max valid pod name (51 chars)",
req: CreatePodRequest{User: "alice", Pod: strings.Repeat("a", 51)},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.req.Validate()
if tt.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
if tt.errMsg != "" && !strings.HasPrefix(err.Error(), tt.errMsg) {
t.Errorf("error = %q, want prefix %q", err.Error(), tt.errMsg)
}
} else {
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
})
}
}
func TestCreateUserRequest_Validate(t *testing.T) {
tests := []struct {
name string
req CreateUserRequest
wantErr bool
}{
{
name: "valid request",
req: CreateUserRequest{User: "alice"},
wantErr: false,
},
{
name: "valid with quotas",
req: CreateUserRequest{User: "bob", Quotas: &UpdateQuotasRequest{MaxConcurrentPods: intPtr(5)}},
wantErr: false,
},
{
name: "missing user",
req: CreateUserRequest{},
wantErr: true,
},
{
name: "invalid user name",
req: CreateUserRequest{User: "ALICE"},
wantErr: true,
},
{
name: "user name too long for namespace",
req: CreateUserRequest{User: strings.Repeat("a", 60)},
wantErr: true,
},
{
name: "max valid user name (59 chars)",
req: CreateUserRequest{User: strings.Repeat("a", 59)},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.req.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestUpdateQuotasRequest_Validate(t *testing.T) {
tests := []struct {
name string
req UpdateQuotasRequest
wantErr bool
}{
{
name: "valid - all fields",
req: UpdateQuotasRequest{MaxConcurrentPods: intPtr(5), MaxCPUPerPod: intPtr(16)},
wantErr: false,
},
{
name: "valid - partial update",
req: UpdateQuotasRequest{MaxConcurrentPods: intPtr(2)},
wantErr: false,
},
{
name: "valid - empty (no-op)",
req: UpdateQuotasRequest{},
wantErr: false,
},
{
name: "invalid - zero max pods",
req: UpdateQuotasRequest{MaxConcurrentPods: intPtr(0)},
wantErr: true,
},
{
name: "invalid - negative cpu",
req: UpdateQuotasRequest{MaxCPUPerPod: intPtr(-1)},
wantErr: true,
},
{
name: "invalid - zero ram",
req: UpdateQuotasRequest{MaxRAMGBPerPod: intPtr(0)},
wantErr: true,
},
{
name: "invalid - zero monthly hours",
req: UpdateQuotasRequest{MonthlyPodHours: intPtr(0)},
wantErr: true,
},
{
name: "invalid - negative ai requests",
req: UpdateQuotasRequest{MonthlyAIRequests: intPtr(-1)},
wantErr: true,
},
{
name: "valid - zero ai requests allowed",
req: UpdateQuotasRequest{MonthlyAIRequests: intPtr(0)},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.req.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestIsValidName(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{"simple lowercase", "alice", true},
{"with numbers", "alice123", true},
{"with hyphens", "alice-dev", true},
{"single char", "a", true},
{"empty", "", false},
{"uppercase", "Alice", false},
{"starts with hyphen", "-alice", false},
{"ends with hyphen", "alice-", false},
{"underscore", "alice_dev", false},
{"dot", "alice.dev", false},
{"space", "alice dev", false},
{"64 chars (too long)", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", false},
{"63 chars (max)", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isValidName(tt.input)
if got != tt.want {
t.Errorf("isValidName(%q) = %v, want %v", tt.input, got, tt.want)
}
})
}
}
func TestDefaultQuota(t *testing.T) {
q := DefaultQuota()
if q.MaxConcurrentPods != 3 {
t.Errorf("MaxConcurrentPods = %d, want 3", q.MaxConcurrentPods)
}
if q.MaxCPUPerPod != 8 {
t.Errorf("MaxCPUPerPod = %d, want 8", q.MaxCPUPerPod)
}
if q.MaxRAMGBPerPod != 16 {
t.Errorf("MaxRAMGBPerPod = %d, want 16", q.MaxRAMGBPerPod)
}
if q.MonthlyPodHours != 500 {
t.Errorf("MonthlyPodHours = %d, want 500", q.MonthlyPodHours)
}
if q.MonthlyAIRequests != 10000 {
t.Errorf("MonthlyAIRequests = %d, want 10000", q.MonthlyAIRequests)
}
}
func TestNamespaceName(t *testing.T) {
if got := NamespaceName("alice"); got != "dev-alice" {
t.Errorf("NamespaceName(alice) = %q, want %q", got, "dev-alice")
}
}
func TestPodFullName(t *testing.T) {
if got := PodFullName("main"); got != "dev-pod-main" {
t.Errorf("PodFullName(main) = %q, want %q", got, "dev-pod-main")
}
}
func TestServiceName(t *testing.T) {
if got := ServiceName("main"); got != "dev-pod-main-svc" {
t.Errorf("ServiceName(main) = %q, want %q", got, "dev-pod-main-svc")
}
}
func TestPodURL(t *testing.T) {
got := PodURL("spinoff.dev", "alice", "main")
want := "https://spinoff.dev/@alice/main/"
if got != want {
t.Errorf("PodURL() = %q, want %q", got, want)
}
}