Skip to content
Open
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
4 changes: 2 additions & 2 deletions cmd/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ func TestBlockFromDifferentStatuses(t *testing.T) {
defer database.Close()

testCases := []struct {
name string
initialStatus models.Status
name string
initialStatus models.Status
shouldTransition bool
}{
{"from open", models.StatusOpen, true},
Expand Down
6 changes: 3 additions & 3 deletions cmd/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,9 @@ func TestErrorsCommandCount(t *testing.T) {
defer database.Close()

tests := []struct {
name string
numErrs int
wantCnt int
name string
numErrs int
wantCnt int
}{
{
name: "no errors",
Expand Down
4 changes: 2 additions & 2 deletions cmd/sync_tail.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import (

// Styles for sync tail output
var (
pushArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render("→") // green
pullArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("45")).Render("←") // cyan
pushArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).Render("→") // green
pullArrow = lipgloss.NewStyle().Foreground(lipgloss.Color("45")).Render("←") // cyan
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
)

Expand Down
2 changes: 1 addition & 1 deletion cmd/sync_tail_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func TestPrintSyncEntry(t *testing.T) {
DeviceID: "",
Timestamp: time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC),
},
contains: []string{"pull", "comments", "c_short", "delete", "seq:7"},
contains: []string{"pull", "comments", "c_short", "delete", "seq:7"},
},
}

Expand Down
10 changes: 5 additions & 5 deletions cmd/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,11 +557,11 @@ var importCmd = &cobra.Command{

// exportedItem matches the JSON structure produced by the export command.
type exportedItem struct {
Issue models.Issue `json:"issue"`
Logs []models.Log `json:"logs"`
Handoffs []models.Handoff `json:"handoffs"`
Dependencies []models.IssueDependency `json:"dependencies"`
Files []models.IssueFile `json:"files"`
Issue models.Issue `json:"issue"`
Logs []models.Log `json:"logs"`
Handoffs []models.Handoff `json:"handoffs"`
Dependencies []models.IssueDependency `json:"dependencies"`
Files []models.IssueFile `json:"files"`
}

// UnmarshalJSON supports backward-compatible deserialization:
Expand Down
1 change: 0 additions & 1 deletion internal/api/action_log_promotion.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,4 +238,3 @@ func shouldPromote(method string) bool {
}
return true
}

1 change: 0 additions & 1 deletion internal/api/action_log_promotion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -454,4 +454,3 @@ func TestPromote_RecoveryAfterError(t *testing.T) {
t.Errorf("events count = %d, want 1", len(events))
}
}

10 changes: 5 additions & 5 deletions internal/api/admin_projects_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,11 @@ func TestAdminSyncCursors_BackfillFromEvents(t *testing.T) {
events := make([]EventInput, count)
for i := 0; i < count; i++ {
events[i] = EventInput{
ClientActionID: startID + int64(i),
ActionType: "create",
EntityType: "issues",
EntityID: fmt.Sprintf("i_%s_%d", dev, i),
Payload: json.RawMessage(`{"title":"x"}`),
ClientActionID: startID + int64(i),
ActionType: "create",
EntityType: "issues",
EntityID: fmt.Sprintf("i_%s_%d", dev, i),
Payload: json.RawMessage(`{"title":"x"}`),
ClientTimestamp: "2025-01-01T00:00:00Z",
}
}
Expand Down
1 change: 0 additions & 1 deletion internal/api/metrics_lag_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -423,4 +423,3 @@ func TestPoolSnapshot_NoDeadlock(t *testing.T) {
close(stop)
wg.Wait()
}

6 changes: 3 additions & 3 deletions internal/db/activity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -818,9 +818,9 @@ func TestAddHandoff_EmptyArrays(t *testing.T) {
IssueID: issue.ID,
SessionID: "ses_test",
Done: []string{"Task 1"},
Remaining: []string{}, // Empty
Decisions: nil, // Nil
Uncertain: []string{}, // Empty
Remaining: []string{}, // Empty
Decisions: nil, // Nil
Uncertain: []string{}, // Empty
}

err = db.AddHandoff(handoff)
Expand Down
4 changes: 2 additions & 2 deletions internal/db/fk_audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import (
// OrphanCount reports the number of rows in a child table whose foreign-key
// column points at a non-existent row in the parent table.
//
// Count = number of child rows where child.col NOT NULL / NOT '' AND no
// Count = number of child rows where child.col NOT NULL / NOT AND no
// matching parent row exists. We treat empty-string and NULL as "no link"
// to match td's mixed-sentinel convention (many FK columns default to '').
// to match td's mixed-sentinel convention (many FK columns default to ).
type OrphanCount struct {
Relation string // e.g. "handoffs.issue_id -> issues.id"
ChildTable string
Expand Down
18 changes: 9 additions & 9 deletions internal/db/fk_audit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,15 @@ func TestAuditForeignKeys_DetectsOrphans(t *testing.T) {
}

expect := map[string]int{
"issues.parent_id -> issues.id": 1,
"handoffs.issue_id -> issues.id": 1,
"git_snapshots.issue_id -> issues.id": 1,
"issue_files.issue_id -> issues.id": 1,
"issue_dependencies.issue_id -> issues.id": 1,
"issue_dependencies.depends_on_id -> issues.id": 1,
"issues.parent_id -> issues.id": 1,
"handoffs.issue_id -> issues.id": 1,
"git_snapshots.issue_id -> issues.id": 1,
"issue_files.issue_id -> issues.id": 1,
"issue_dependencies.issue_id -> issues.id": 1,
"issue_dependencies.depends_on_id -> issues.id": 1,
"work_session_issues.work_session_id -> work_sessions.id": 1,
"work_session_issues.issue_id -> issues.id": 1,
"comments.issue_id -> issues.id": 1,
"work_session_issues.issue_id -> issues.id": 1,
"comments.issue_id -> issues.id": 1,
}

for rel, want := range expect {
Expand All @@ -114,7 +114,7 @@ func TestAuditForeignKeys_DetectsOrphans(t *testing.T) {
}

// TestAuditForeignKeys_IgnoresEmptyFKValues ensures default-empty FK columns
// (e.g. issues.parent_id='') are not counted as orphans.
// (e.g. issues.parent_id=) are not counted as orphans.
func TestAuditForeignKeys_IgnoresEmptyFKValues(t *testing.T) {
dir := t.TempDir()
database, err := Initialize(dir)
Expand Down
2 changes: 1 addition & 1 deletion internal/db/ids.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const (
commentIDPrefix = "cm-"
snapshotIDPrefix = "gs-"
noteIDPrefix = "nt-"
actionIDPrefix = "al-"
actionIDPrefix = "al-"

// Deterministic ID prefixes for composite-key tables
boardIssuePosIDPrefix = "bip_"
Expand Down
1 change: 0 additions & 1 deletion internal/db/issue_relations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1731,7 +1731,6 @@ func TestCascadeUnblockDependents_UndoData(t *testing.T) {
}
}


func TestGetIssueDependencyRelations(t *testing.T) {
dir := t.TempDir()
database, err := Initialize(dir)
Expand Down
14 changes: 7 additions & 7 deletions internal/db/migration_fk_enforcement.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,24 @@ import (
//
// 1. Cleans up the FK orphans identified by the Wave 1 audit (td-b8dd0d).
// Policies (justified per relation):
// - issues.parent_id: nullify to '' (empty-string sentinel) — preserve
// the issue itself; parent link was dangling.
// - issues.parent_id: nullify to (empty-string sentinel) — preserve
// the issue itself; parent link was dangling.
// - handoffs/git_snapshots/issue_dependencies/issue_session_history:
// DELETE — a row referring to a missing issue has no meaningful value.
// DELETE — a row referring to a missing issue has no meaningful value.
// 2. Rewrites child tables to add ON DELETE CASCADE where td's
// internal/sync/events.go already performs a manual cascade delete. This
// makes the DB enforce what the app code does. internal/sync/events.go
// still runs the manual cascade; that's fine (schema cascade is a no-op
// after the manual path removes rows). td-0001eb will remove the
// manual emulation once this is in.
// 3. issues.parent_id FK: the constraint is DROPPED at the schema level.
// Rationale: td's codebase uses '' (empty string) as the "no parent"
// Rationale: td's codebase uses (empty string) as the "no parent"
// sentinel throughout (INSERTs, UPDATEs, sync event payloads). SQLite
// FK semantics treat '' as a real value, not as NULL, so a schema-level
// FK semantics treat as a real value, not as NULL, so a schema-level
// FK on parent_id would reject every top-level issue insert (since no
// issue has id = ''). Migrating every writer to use NULL is out of
// issue has id = ). Migrating every writer to use NULL is out of
// scope for td-4846e6; keeping parent_id unconstrained matches the
// long-standing behaviour, preserves the audit (which treats '' as not
// long-standing behaviour, preserves the audit (which treats as not
// a link), and leaves deleting a parent as non-cascading (children
// become orphans with stale parent_ids until the app or a follow-up
// cleanup rewrites them). The fk_audit keeps flagging any true orphans.
Expand Down
6 changes: 3 additions & 3 deletions internal/db/sync_state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ func TestParseTimestamp(t *testing.T) {
{"RFC3339", "2026-03-21T09:00:00Z"},
{"RFC3339Nano", now.Format(time.RFC3339Nano)},
{"Go time.String() UTC", now.String()},
{"Go time.String() local", local.Round(0).String()}, // non-UTC, no monotonic
{"Go time.String() with monotonic", local.String()}, // includes m=+... suffix
{"non-UTC with offset", "2026-03-21 19:00:00.123456 +1000 AEST"}, // non-UTC timezone
{"Go time.String() local", local.Round(0).String()}, // non-UTC, no monotonic
{"Go time.String() with monotonic", local.String()}, // includes m=+... suffix
{"non-UTC with offset", "2026-03-21 19:00:00.123456 +1000 AEST"}, // non-UTC timezone
{"RFC3339 with offset", "2026-03-21T09:00:00+00:00"},
}

Expand Down
30 changes: 30 additions & 0 deletions internal/features/features_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,27 @@ import (
"github.com/marcus/td/internal/config"
)

func resetFeatureEnv(t *testing.T) {
t.Helper()

for _, feature := range ListAll() {
t.Setenv("TD_FEATURE_"+normalizeForEnvKey(feature.Name), "")
}

for _, key := range []string{
"TD_ENABLE_FEATURE",
"TD_ENABLE_FEATURES",
"TD_DISABLE_FEATURE",
"TD_DISABLE_FEATURES",
"TD_DISABLE_EXPERIMENTAL",
} {
t.Setenv(key, "")
}
}

func TestKnownFeatureDefaults(t *testing.T) {
resetFeatureEnv(t)

for _, feature := range ListAll() {
if IsEnabledForProcess(feature.Name) != feature.Default {
t.Fatalf("default mismatch for %s", feature.Name)
Expand All @@ -15,6 +35,8 @@ func TestKnownFeatureDefaults(t *testing.T) {
}

func TestIsEnabledForProcess_EnvVarOverride(t *testing.T) {
resetFeatureEnv(t)

t.Setenv("TD_FEATURE_SYNC_CLI", "true")
if !IsEnabledForProcess(SyncCLI.Name) {
t.Fatal("TD_FEATURE_SYNC_CLI=true should enable sync_cli")
Expand All @@ -27,6 +49,8 @@ func TestIsEnabledForProcess_EnvVarOverride(t *testing.T) {
}

func TestIsEnabledForProcess_EnableDisableLists(t *testing.T) {
resetFeatureEnv(t)

t.Setenv("TD_ENABLE_FEATURE", "sync_cli,sync_monitor_prompt")
if !IsEnabledForProcess(SyncCLI.Name) {
t.Fatal("TD_ENABLE_FEATURE should enable sync_cli")
Expand All @@ -42,6 +66,8 @@ func TestIsEnabledForProcess_EnableDisableLists(t *testing.T) {
}

func TestIsEnabled_ProjectConfigAndEnvPrecedence(t *testing.T) {
resetFeatureEnv(t)

dir := t.TempDir()

if err := config.SetFeatureFlag(dir, SyncCLI.Name, true); err != nil {
Expand All @@ -60,6 +86,8 @@ func TestIsEnabled_ProjectConfigAndEnvPrecedence(t *testing.T) {
}

func TestDisableExperimentalKillSwitch(t *testing.T) {
resetFeatureEnv(t)

t.Setenv("TD_ENABLE_FEATURE", "sync_cli")
if !IsEnabledForProcess(SyncCLI.Name) {
t.Fatal("expected sync_cli enabled before kill-switch")
Expand All @@ -75,6 +103,8 @@ func TestDisableExperimentalKillSwitch(t *testing.T) {
}

func TestSyncGateMapReferencesKnownFeatures(t *testing.T) {
resetFeatureEnv(t)

if len(SyncGateMap) == 0 {
t.Fatal("SyncGateMap should not be empty")
}
Expand Down
12 changes: 6 additions & 6 deletions internal/query/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,10 +209,10 @@ var CrossEntityFields = map[string]map[string]string{

// Enum values for validation
var EnumValues = map[string][]string{
"status": {"open", "in_progress", "blocked", "in_review", "closed"},
"type": {"bug", "feature", "task", "epic", "chore"},
"priority": {"P0", "P1", "P2", "P3", "P4"},
"log.type": {"progress", "blocker", "decision", "hypothesis", "tried", "result", "orchestration"},
"status": {"open", "in_progress", "blocked", "in_review", "closed"},
"type": {"bug", "feature", "task", "epic", "chore"},
"priority": {"P0", "P1", "P2", "P3", "P4"},
"log.type": {"progress", "blocker", "decision", "hypothesis", "tried", "result", "orchestration"},
"file.role": {"implementation", "test", "reference", "config"},
}

Expand Down Expand Up @@ -278,8 +278,8 @@ var NoteSortFieldToColumn = map[string]string{
// Query represents a parsed TDQ query
type Query struct {
Root Node
Raw string // original query string
Sort *SortClause // optional sort clause
Raw string // original query string
Sort *SortClause // optional sort clause
}

func (q *Query) String() string {
Expand Down
2 changes: 1 addition & 1 deletion internal/query/execute.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func applyCrossEntityFilters(database QuerySource, issues []models.Issue, query

// crossEntityPrefetch holds pre-fetched bulk data to avoid per-issue queries
type crossEntityPrefetch struct {
reworkIDs map[string]bool
reworkIDs map[string]bool
issuesWithOpenDeps map[string]bool
}

Expand Down
6 changes: 3 additions & 3 deletions internal/query/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,9 @@ func TestExecute(t *testing.T) {
wantCount: 2,
},
{
name: "invalid query",
query: "status = ",
wantErr: true,
name: "invalid query",
query: "status = ",
wantErr: true,
},
}

Expand Down
4 changes: 2 additions & 2 deletions internal/query/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -993,8 +993,8 @@ func TestLexerPositionTracking(t *testing.T) {
line int
column int
}{
{TokenIdent, 0, 1, 1}, // "status"
{TokenEq, 7, 1, 8}, // "="
{TokenIdent, 0, 1, 1}, // "status"
{TokenEq, 7, 1, 8}, // "="
{TokenIdent, 10, 2, 2}, // "open"
},
},
Expand Down
10 changes: 5 additions & 5 deletions internal/serve/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (
)

const (
webAgentType = "web"
webAgentPID = 0
webBranch = "default"
webSessionName = "td-serve-web"
heartbeatInterval = 60 * time.Second
webAgentType = "web"
webAgentPID = 0
webBranch = "default"
webSessionName = "td-serve-web"
heartbeatInterval = 60 * time.Second
)

// GetOrCreateWebSession finds or creates the shared web session used by
Expand Down
6 changes: 3 additions & 3 deletions internal/serverdb/apikeys.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ import (
)

const (
apiKeyPrefix = "td_live_"
impersonationKeyPrefix = "td_ipk_"
keyLength = 32
apiKeyPrefix = "td_live_"
impersonationKeyPrefix = "td_ipk_"
keyLength = 32
)

var base62Chars = []byte("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")
Expand Down
Loading