Skip to content
49 changes: 49 additions & 0 deletions cmd/context_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package cmd

import (
"fmt"
"os"

"github.com/chris-regnier/diaryctl/internal/context"
"github.com/chris-regnier/diaryctl/internal/entry"
)

// buildContentProviders creates ContentProviders from config names, skipping unknown.
func buildContentProviders(names []string) []context.ContentProvider {
var providers []context.ContentProvider
for _, name := range names {
p := context.LookupContentProvider(name)
if p != nil {
providers = append(providers, p)
}
}
return providers
}

// buildContextResolvers creates ContextResolvers from config names, skipping unknown.
func buildContextResolvers(names []string) []context.ContextResolver {
var resolvers []context.ContextResolver
for _, name := range names {
r := context.LookupContextResolver(name)
if r != nil {
resolvers = append(resolvers, r)
}
}
return resolvers
}

// resolveContexts is a convenience function that builds resolvers from config,
// loads manual contexts, calls ResolveActiveContexts, and prints warnings.
// Returns the resolved context refs.
func resolveContexts() []entry.ContextRef {
resolvers := buildContextResolvers(appConfig.ContextResolvers)

Check failure

Code scanning / gavel

Detect critical bugs, logic errors, and potential runtime failures Error

Global variable 'appConfig' is accessed without null checking or initialization verification.
manual, err := context.LoadManualContexts(appConfig.DataDir)
if err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to load manual contexts: %v\n", err)
}

Check failure

Code scanning / gavel

Detect critical bugs, logic errors, and potential runtime failures Error

Global variable 'store' is accessed without null checking or initialization verification.
refs, warnings := context.ResolveActiveContexts(resolvers, manual, store)
for _, w := range warnings {
fmt.Fprintf(os.Stderr, "Warning: %s\n", w)
}
return refs

Check warning

Code scanning / gavel

Identify code smells that impact maintainability and readability Warning

Functions have hidden dependencies on global variables making them difficult to test and reason about.

Check warning

Code scanning / gavel

Assess test quality, coverage gaps, and testing best practices Warning

Functions with global dependencies and error handling logic lack visible test coverage.
}
50 changes: 50 additions & 0 deletions cmd/context_helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package cmd

import (
"testing"
)

func TestBuildContentProviders(t *testing.T) {
// Known providers
providers := buildContentProviders([]string{"datetime", "git"})
if len(providers) != 2 {
t.Fatalf("expected 2 providers, got %d", len(providers))
}
if providers[0].Name() != "datetime" {
t.Errorf("expected datetime, got %s", providers[0].Name())
}
if providers[1].Name() != "git" {
t.Errorf("expected git, got %s", providers[1].Name())
}

Check warning

Code scanning / gavel

Assess test quality, coverage gaps, and testing best practices Warning test

Tests lack verification that the correct provider instances are created and only check names.
}

func TestBuildContentProvidersSkipsUnknown(t *testing.T) {
providers := buildContentProviders([]string{"datetime", "nonexistent"})
if len(providers) != 1 {
t.Fatalf("expected 1 provider, got %d", len(providers))
}
}

func TestBuildContentProvidersEmpty(t *testing.T) {
providers := buildContentProviders(nil)
if len(providers) != 0 {
t.Fatalf("expected 0 providers, got %d", len(providers))
}
}

func TestBuildContextResolvers(t *testing.T) {
resolvers := buildContextResolvers([]string{"git"})
if len(resolvers) != 1 {
t.Fatalf("expected 1 resolver, got %d", len(resolvers))
}
if resolvers[0].Name() != "git" {
t.Errorf("expected git, got %s", resolvers[0].Name())
}
}

func TestBuildContextResolversSkipsUnknown(t *testing.T) {
resolvers := buildContextResolvers([]string{"nonexistent"})
if len(resolvers) != 0 {
t.Fatalf("expected 0 resolvers, got %d", len(resolvers))
}
}
11 changes: 10 additions & 1 deletion cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"strings"
"time"

ctxpkg "github.com/chris-regnier/diaryctl/internal/context"
"github.com/chris-regnier/diaryctl/internal/editor"
"github.com/chris-regnier/diaryctl/internal/entry"
tmpl "github.com/chris-regnier/diaryctl/internal/template"
Expand Down Expand Up @@ -87,11 +88,15 @@
}
}

// Compose content from providers and template

Check failure

Code scanning / gavel

Detect critical bugs, logic errors, and potential runtime failures Error

Function buildContentProviders is called but not defined in the visible code, which will cause a compilation error.
providers := buildContentProviders(appConfig.ContextProviders)
editorContent := ctxpkg.ComposeContent(providers, templateContent)

// Open editor
editorCmd := editor.ResolveEditor(appConfig.Editor)
var err error
var changed bool
content, changed, err = editor.Edit(editorCmd, templateContent)
content, changed, err = editor.Edit(editorCmd, editorContent)
if err != nil {
fmt.Fprintln(os.Stderr, "Editor error:", err)
os.Exit(3)
Expand All @@ -115,13 +120,17 @@
os.Exit(2)
}

// Resolve active contexts
contextRefs := resolveContexts()

Check failure

Code scanning / gavel

Detect critical bugs, logic errors, and potential runtime failures Error

Function resolveContexts is called but not defined in the visible code, which will cause a compilation error.

Check failure

Code scanning / gavel

Shall this code be merged? Error

Code contains undefined function calls that will prevent compilation and execution.

now := time.Now().UTC()
e := entry.Entry{
ID: id,
Content: strings.TrimSpace(content),
CreatedAt: now,
UpdatedAt: now,
Templates: templateRefs,
Contexts: contextRefs,
}

if err := store.Create(e); err != nil {
Expand Down
48 changes: 48 additions & 0 deletions cmd/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"testing"
"time"

"github.com/chris-regnier/diaryctl/internal/context"
"github.com/chris-regnier/diaryctl/internal/entry"
"github.com/chris-regnier/diaryctl/internal/storage"
tmpl "github.com/chris-regnier/diaryctl/internal/template"
Expand Down Expand Up @@ -191,3 +192,50 @@
t.Errorf("expected ErrNotFound in error, got: %v", err)
}
}

func TestCreateInlineWithContextResolution(t *testing.T) {
setupTestEnv(t)
appConfig.DataDir = t.TempDir()
appConfig.ContextResolvers = []string{}

ctx := storage.Context{
ID: "ctx001",
Name: "test-context",
Source: "manual",
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if err := store.CreateContext(ctx); err != nil {
t.Fatalf("CreateContext: %v", err)
}

if err := context.SetManualContext(appConfig.DataDir, "test-context"); err != nil {
t.Fatalf("SetManualContext: %v", err)
}

id, _ := entry.NewID()
now := time.Now().UTC()

Check warning

Code scanning / gavel

Assess test quality, coverage gaps, and testing best practices Warning test

Test uses global resolveContexts() function without verifying its behavior or mocking its dependencies.
refs := resolveContexts()

e := entry.Entry{
ID: id,
Content: "Entry with context",
CreatedAt: now,
UpdatedAt: now,
Contexts: refs,
}
if err := store.Create(e); err != nil {
t.Fatalf("Create: %v", err)
}

got, err := store.Get(id)
if err != nil {
t.Fatalf("Get: %v", err)
}
if len(got.Contexts) != 1 {
t.Fatalf("expected 1 context ref, got %d", len(got.Contexts))
}
if got.Contexts[0].ContextName != "test-context" {
t.Errorf("context name = %q, want %q", got.Contexts[0].ContextName, "test-context")
}
}
6 changes: 6 additions & 0 deletions cmd/jot.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@
return fmt.Errorf("updating entry: %w", err)
}

// Resolve and attach contexts
contextRefs := resolveContexts()
for _, ref := range contextRefs {
_ = store.AttachContext(e.ID, ref.ContextID)

Check failure

Code scanning / gavel

Detect critical bugs, logic errors, and potential runtime failures Error

Error from AttachContext is silently ignored, potentially causing data inconsistency.
}

Check warning

Code scanning / gavel

Assess test quality, coverage gaps, and testing best practices Warning

New context attachment functionality lacks visible error handling and testing strategy.

Check warning

Code scanning / gavel

Identify code smells that impact maintainability and readability Warning

Context attachment logic mixed with main entry creation flow violates single responsibility principle.

if jsonOutput {
return ui.FormatJSON(w, updated)
}
Expand Down
88 changes: 88 additions & 0 deletions cmd/jot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"time"

"github.com/chris-regnier/diaryctl/internal/config"
"github.com/chris-regnier/diaryctl/internal/context"
"github.com/chris-regnier/diaryctl/internal/daily"
"github.com/chris-regnier/diaryctl/internal/entry"
"github.com/chris-regnier/diaryctl/internal/storage"
Expand Down Expand Up @@ -183,3 +184,90 @@
t.Error("expected jot content in JSON")
}
}

func TestJotAttachesContextsToNewEntry(t *testing.T) {
s := setupTestStore(t)
store = s
appConfig = &config.Config{}
appConfig.DataDir = t.TempDir()
appConfig.ContextResolvers = []string{}

ctx := storage.Context{
ID: "ctx001",
Name: "sprint-1",
Source: "manual",
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if err := s.CreateContext(ctx); err != nil {
t.Fatalf("CreateContext: %v", err)
}
if err := context.SetManualContext(appConfig.DataDir, "sprint-1"); err != nil {
t.Fatalf("SetManualContext: %v", err)
}

err := jotRun(io.Discard, "hello", "")
if err != nil {
t.Fatalf("jotRun: %v", err)
}

e, _, err := daily.GetOrCreateToday(s, "")
if err != nil {
t.Fatalf("GetOrCreateToday: %v", err)
}

got, err := s.Get(e.ID)

Check warning

Code scanning / gavel

Assess test quality, coverage gaps, and testing best practices Warning test

Test ignores error return value from critical function call that could mask test failures.
if err != nil {
t.Fatalf("Get: %v", err)
}
if len(got.Contexts) != 1 {
t.Fatalf("expected 1 context, got %d", len(got.Contexts))
}
if got.Contexts[0].ContextName != "sprint-1" {
t.Errorf("context = %q, want sprint-1", got.Contexts[0].ContextName)
}
}

func TestJotAttachesContextsToExistingEntry(t *testing.T) {
s := setupTestStore(t)
store = s
appConfig = &config.Config{}
appConfig.DataDir = t.TempDir()
appConfig.ContextResolvers = []string{}

_, _, err := daily.GetOrCreateToday(s, "")
if err != nil {
t.Fatalf("GetOrCreateToday: %v", err)
}

ctx := storage.Context{
ID: "ctx002",
Name: "bugfix",
Source: "manual",
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
if err := s.CreateContext(ctx); err != nil {
t.Fatalf("CreateContext: %v", err)
}
if err := context.SetManualContext(appConfig.DataDir, "bugfix"); err != nil {
t.Fatalf("SetManualContext: %v", err)
}

err = jotRun(io.Discard, "fixed the bug", "")
if err != nil {
t.Fatalf("jotRun: %v", err)
}

e, _, _ := daily.GetOrCreateToday(s, "")
got, err := s.Get(e.ID)
if err != nil {
t.Fatalf("Get: %v", err)
}
if len(got.Contexts) != 1 {
t.Fatalf("expected 1 context, got %d", len(got.Contexts))
}
if got.Contexts[0].ContextName != "bugfix" {
t.Errorf("context = %q, want bugfix", got.Contexts[0].ContextName)
}

Check warning

Code scanning / gavel

Identify code smells that impact maintainability and readability Warning test

Significant code duplication between test functions reduces maintainability.
}
11 changes: 7 additions & 4 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,13 @@ var rootCmd = &cobra.Command{
return todayRun(os.Stdout, false, false)
}
return ui.RunTUI(store, ui.TUIConfig{
Editor: editor.ResolveEditor(appConfig.Editor),
DefaultTemplate: appConfig.DefaultTemplate,
MaxWidth: appConfig.MaxWidth,
Theme: ui.ResolveTheme(appConfig.Theme),
Editor: editor.ResolveEditor(appConfig.Editor),
DefaultTemplate: appConfig.DefaultTemplate,
MaxWidth: appConfig.MaxWidth,
Theme: ui.ResolveTheme(appConfig.Theme),
ContextProviders: appConfig.ContextProviders,
ContextResolvers: appConfig.ContextResolvers,
DataDir: appConfig.DataDir,
})
},
}
Expand Down
12 changes: 12 additions & 0 deletions cmd/today.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@
return fmt.Errorf("getting today's entry: %w", err)
}

// Resolve and attach contexts
contextRefs := resolveContexts()
for _, ref := range contextRefs {
_ = store.AttachContext(e.ID, ref.ContextID)

Check failure

Code scanning / gavel

Detect critical bugs, logic errors, and potential runtime failures Error

Error from store.AttachContext() is silently ignored, potentially masking critical failures.
}

Check warning

Code scanning / gavel

Identify code smells that impact maintainability and readability Warning

Duplicate code block for context resolution and attachment appears in multiple functions.

if jsonOutput {
return ui.FormatJSON(w, e)
}
Expand All @@ -69,6 +75,12 @@
return fmt.Errorf("getting today's entry: %w", err)
}

// Resolve and attach contexts
contextRefs := resolveContexts()
for _, ref := range contextRefs {
_ = store.AttachContext(e.ID, ref.ContextID)

Check failure

Code scanning / gavel

Detect critical bugs, logic errors, and potential runtime failures Error

Error from store.AttachContext() is silently ignored, potentially masking critical failures.

Check warning

Code scanning / gavel

Assess test quality, coverage gaps, and testing best practices Warning

New context attachment functionality lacks visible test coverage and error handling paths are untestable due to ignored errors.
}

editorCmd := editor.ResolveEditor(appConfig.Editor)
content, changed, err := editor.Edit(editorCmd, e.Content)
if err != nil {
Expand Down
Loading
Loading