Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions cmd/relayfile-cli/digest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package main

import (
"context"
"errors"
"flag"
"fmt"
"io"
"strings"
)

const digestRebuildUsage = "usage: relayfile digest rebuild --window yesterday|today [--workspace NAME]"

// digestRebuilder is the seam over the internal/digest generator. The CLI ships
// a stub today; the real implementation lands when work item 1 of the
// workspace-primitives spec wires the daemon-side generator.
type digestRebuilder interface {
Rebuild(ctx context.Context, opts digestRebuildOptions) (digestRebuildResult, error)
}

type digestRebuildOptions struct {
WorkspaceID string
LocalDir string
Window string
}

type digestRebuildResult struct {
Path string
Events int
}

// activeDigestRebuilder is overridden by tests via withDigestRebuilder.
var activeDigestRebuilder digestRebuilder = stubDigestRebuilder{}

type stubDigestRebuilder struct{}

func (stubDigestRebuilder) Rebuild(context.Context, digestRebuildOptions) (digestRebuildResult, error) {
return digestRebuildResult{}, errors.New("digest generator not yet wired; see WI 1 in workspace-primitives-implementation-spec")
}

func withDigestRebuilder(r digestRebuilder, fn func()) {
prev := activeDigestRebuilder
activeDigestRebuilder = r
defer func() { activeDigestRebuilder = prev }()
fn()
}

func runDigest(args []string, stdout io.Writer) error {
if len(args) == 0 {
return errors.New("digest subcommand is required: rebuild")
}
switch args[0] {
case "rebuild":
return runDigestRebuild(args[1:], stdout)
default:
return fmt.Errorf("unknown digest subcommand %q", args[0])
}
}

func runDigestRebuild(args []string, stdout io.Writer) error {
fs := flag.NewFlagSet("digest rebuild", flag.ContinueOnError)
fs.SetOutput(io.Discard)
window := fs.String("window", "", "digest window: yesterday or today")
workspace := fs.String("workspace", "", "workspace name or id")
if err := fs.Parse(normalizeFlagArgs(args, map[string]bool{
"window": true,
"workspace": true,
})); err != nil {
return err
}
if fs.NArg() > 0 {
return errors.New(digestRebuildUsage)
}

if strings.TrimSpace(*window) == "" {
return errors.New(digestRebuildUsage)
}
normalizedWindow := strings.ToLower(strings.TrimSpace(*window))
switch normalizedWindow {
case "yesterday", "today":
default:
return fmt.Errorf("unknown window %q: %s", *window, digestRebuildUsage)
}

workspaceID, record, err := resolveWorkspaceLikeStatus(strings.TrimSpace(*workspace))
if err != nil {
return err
}

result, err := activeDigestRebuilder.Rebuild(context.Background(), digestRebuildOptions{
WorkspaceID: workspaceID,
LocalDir: record.LocalDir,
Window: normalizedWindow,
})
if err != nil {
return err
}
path := strings.TrimSpace(result.Path)
if path == "" {
path = fmt.Sprintf("<mount>/digests/%s.md", normalizedWindow)
}
fmt.Fprintf(stdout, "Regenerated %s (events=%d)\n", path, result.Events)
return nil
}
162 changes: 162 additions & 0 deletions cmd/relayfile-cli/digest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package main

import (
"bytes"
"context"
"errors"
"strings"
"testing"
"time"
)

type fakeDigestRebuilder struct {
called bool
opts digestRebuildOptions
result digestRebuildResult
err error
}

func (f *fakeDigestRebuilder) Rebuild(_ context.Context, opts digestRebuildOptions) (digestRebuildResult, error) {
f.called = true
f.opts = opts
return f.result, f.err
}

func TestDigestRequiresWindow(t *testing.T) {
t.Setenv("HOME", t.TempDir())
clearRelayfileEnv(t)

var stderr bytes.Buffer
err := run([]string{"digest", "rebuild"}, strings.NewReader(""), &stderr, &stderr)
if err == nil {
t.Fatalf("expected missing window error, got nil")
}
if !strings.Contains(err.Error(), "usage: relayfile digest rebuild --window") {
t.Fatalf("expected usage in error, got %q", err.Error())
}
}

func TestDigestUnknownWindowErrors(t *testing.T) {
t.Setenv("HOME", t.TempDir())
clearRelayfileEnv(t)

localDir := t.TempDir()
if err := ensureMirrorLayout(localDir); err != nil {
t.Fatalf("ensureMirrorLayout failed: %v", err)
}
upsertDigestWorkspace(t, localDir)

var stderr bytes.Buffer
err := run([]string{"digest", "rebuild", "--window", "monday", "--workspace", "demo"}, strings.NewReader(""), &stderr, &stderr)
if err == nil {
t.Fatalf("expected unknown window error, got nil")
}
if !strings.Contains(err.Error(), "unknown window") {
t.Fatalf("expected unknown window in error, got %q", err.Error())
}
}

func TestDigestUnknownSubcommandErrors(t *testing.T) {
t.Setenv("HOME", t.TempDir())
clearRelayfileEnv(t)

var stderr bytes.Buffer
err := run([]string{"digest", "compute"}, strings.NewReader(""), &stderr, &stderr)
if err == nil {
t.Fatalf("expected unknown subcommand error, got nil")
}
if !strings.Contains(err.Error(), "unknown digest subcommand") {
t.Fatalf("expected unknown digest subcommand in error, got %q", err.Error())
}
}

func TestDigestRebuildCallsRebuilder(t *testing.T) {
t.Setenv("HOME", t.TempDir())
clearRelayfileEnv(t)

localDir := t.TempDir()
if err := ensureMirrorLayout(localDir); err != nil {
t.Fatalf("ensureMirrorLayout failed: %v", err)
}
upsertDigestWorkspace(t, localDir)

fake := &fakeDigestRebuilder{result: digestRebuildResult{Path: localDir + "/digests/yesterday.md", Events: 3}}
var stdout bytes.Buffer
withDigestRebuilder(fake, func() {
if err := run([]string{"digest", "rebuild", "--window", "yesterday", "--workspace", "demo"}, strings.NewReader(""), &stdout, &stdout); err != nil {
t.Fatalf("run digest rebuild failed: %v\nstdout:\n%s", err, stdout.String())
}
})
if !fake.called {
t.Fatalf("expected rebuilder to be called")
}
if fake.opts.Window != "yesterday" {
t.Fatalf("expected window=yesterday, got %q", fake.opts.Window)
}
if fake.opts.WorkspaceID != "ws_demo" {
t.Fatalf("expected workspaceId=ws_demo, got %q", fake.opts.WorkspaceID)
}
if fake.opts.LocalDir != localDir {
t.Fatalf("expected localDir=%q, got %q", localDir, fake.opts.LocalDir)
}
if !strings.Contains(stdout.String(), "Regenerated ") || !strings.Contains(stdout.String(), "events=3") {
t.Fatalf("expected success line with event count, got %q", stdout.String())
}
}

func TestDigestRebuildSurfacesError(t *testing.T) {
t.Setenv("HOME", t.TempDir())
clearRelayfileEnv(t)

localDir := t.TempDir()
if err := ensureMirrorLayout(localDir); err != nil {
t.Fatalf("ensureMirrorLayout failed: %v", err)
}
upsertDigestWorkspace(t, localDir)

fake := &fakeDigestRebuilder{err: errors.New("boom")}
var stdout bytes.Buffer
var got error
withDigestRebuilder(fake, func() {
got = run([]string{"digest", "rebuild", "--window", "today", "--workspace", "demo"}, strings.NewReader(""), &stdout, &stdout)
})
if got == nil {
t.Fatalf("expected error, got nil")
}
if !strings.Contains(got.Error(), "boom") {
t.Fatalf("expected error to wrap rebuilder error, got %q", got.Error())
}
}

func TestDigestRebuildStubIsNotWired(t *testing.T) {
t.Setenv("HOME", t.TempDir())
clearRelayfileEnv(t)

localDir := t.TempDir()
if err := ensureMirrorLayout(localDir); err != nil {
t.Fatalf("ensureMirrorLayout failed: %v", err)
}
upsertDigestWorkspace(t, localDir)

var stdout bytes.Buffer
err := run([]string{"digest", "rebuild", "--window", "today", "--workspace", "demo"}, strings.NewReader(""), &stdout, &stdout)
if err == nil {
t.Fatalf("expected stub error, got nil")
}
if !strings.Contains(err.Error(), "not yet wired") {
t.Fatalf("expected not-yet-wired error from stub, got %q", err.Error())
}
}

func upsertDigestWorkspace(t *testing.T, localDir string) {
t.Helper()
if _, err := upsertWorkspaceDetails(workspaceRecord{
Name: "demo",
ID: "ws_demo",
LocalDir: localDir,
CreatedAt: time.Now().UTC().Format(time.RFC3339),
LastUsedAt: time.Now().UTC().Format(time.RFC3339),
}); err != nil {
t.Fatalf("upsertWorkspaceDetails failed: %v", err)
}
}
Loading
Loading