test(tombstones): add unit tests for tombstone functionality (bd-fmo, bd-hp0m)
- Add TestIsExpiredTombstone with edge cases for merge package - Add TestImportIssues_LegacyDeletionsConvertedToTombstones for importer 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -99,7 +99,7 @@
|
||||
{"id":"bd-fbj","title":"Implement tombstone types and schema migration","description":"Add tombstone fields to Issue struct (deleted_at, deleted_by, delete_reason, original_type), add StatusTombstone constant, create SQLite migration for new columns. Per design bd-2m7.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-05T15:14:38.250106-08:00","updated_at":"2025-12-05T15:28:50.361654-08:00","closed_at":"2025-12-05T15:28:50.361654-08:00"}
|
||||
{"id":"bd-fl4","title":"Test sync branch setup","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-30T00:26:47.520507-08:00","updated_at":"2025-11-30T00:33:23.105998-08:00","closed_at":"2025-11-30T00:33:23.105998-08:00"}
|
||||
{"id":"bd-fmc","title":"string(rune()) pattern in other test files still only works for single digits","description":"The bd-c5m fix only addressed worktree_divergence_test.go, but several other test files have the same issue:\n\n- cmd/bd/stale_test.go:218, 234\n- cmd/bd/export_integrity_integration_test.go:45-47, 279-280\n- internal/storage/sqlite/comments_test.go:146, 165\n- internal/storage/sqlite/blocked_cache_test.go:292\n- internal/storage/sqlite/compact_bench_test.go:117\n- internal/storage/sqlite/cycle_detection_test.go:418\n\nThese use string(rune('0'+i)) which produces wrong characters when i \u003e= 10.\nShould use strconv.Itoa(i) instead.","status":"closed","priority":4,"issue_type":"bug","created_at":"2025-12-02T22:29:12.355705-08:00","updated_at":"2025-12-02T22:39:51.844638-08:00","closed_at":"2025-12-02T22:39:51.844638-08:00"}
|
||||
{"id":"bd-fmo","title":"Missing test for IsExpiredTombstone edge cases","description":"## Problem\n\nThe `IsExpiredTombstone()` function has several edge cases that aren't directly tested:\n\n1. **Invalid DeletedAt timestamp** - Returns false (safety), but not tested\n2. **ttl=0 fallback to DefaultTombstoneTTL** - Not tested directly\n3. **Tombstone with empty DeletedAt** - Returns false, tested implicitly but not explicitly\n4. **Clock skew grace period effect** - Not tested (would need time mocking)\n\n## Current Test Coverage\n\nThe existing tests use `time.Now()` which makes them non-deterministic:\n```go\nDeletedAt: time.Now().Add(-24 * time.Hour).Format(time.RFC3339)\n```\n\nThis works but doesn't test the expiration boundary precisely.\n\n## Recommendation\n\nAdd explicit unit tests for `IsExpiredTombstone()`:\n\n```go\nfunc TestIsExpiredTombstone(t *testing.T) {\n tests := []struct {\n name string\n status string\n deletedAt string\n ttl time.Duration\n expected bool\n }{\n {\"non-tombstone returns false\", \"open\", \"2024-01-01T00:00:00Z\", 0, false},\n {\"empty deleted_at returns false\", \"tombstone\", \"\", 0, false},\n {\"invalid timestamp returns false\", \"tombstone\", \"not-a-date\", 0, false},\n {\"within TTL returns false\", \"tombstone\", time.Now().Add(-1*time.Hour).Format(time.RFC3339), 24*time.Hour, false},\n {\"beyond TTL returns true\", \"tombstone\", time.Now().Add(-48*time.Hour).Format(time.RFC3339), 24*time.Hour, true},\n }\n // ...\n}\n```\n\n## Files\n- internal/merge/merge.go:255-290\n- internal/merge/merge_test.go (add TestIsExpiredTombstone)","status":"in_progress","priority":3,"issue_type":"task","created_at":"2025-12-05T16:36:09.106135-08:00","updated_at":"2025-12-07T02:04:40.646331-08:00"}
|
||||
{"id":"bd-fmo","title":"Missing test for IsExpiredTombstone edge cases","description":"## Problem\n\nThe `IsExpiredTombstone()` function has several edge cases that aren't directly tested:\n\n1. **Invalid DeletedAt timestamp** - Returns false (safety), but not tested\n2. **ttl=0 fallback to DefaultTombstoneTTL** - Not tested directly\n3. **Tombstone with empty DeletedAt** - Returns false, tested implicitly but not explicitly\n4. **Clock skew grace period effect** - Not tested (would need time mocking)\n\n## Current Test Coverage\n\nThe existing tests use `time.Now()` which makes them non-deterministic:\n```go\nDeletedAt: time.Now().Add(-24 * time.Hour).Format(time.RFC3339)\n```\n\nThis works but doesn't test the expiration boundary precisely.\n\n## Recommendation\n\nAdd explicit unit tests for `IsExpiredTombstone()`:\n\n```go\nfunc TestIsExpiredTombstone(t *testing.T) {\n tests := []struct {\n name string\n status string\n deletedAt string\n ttl time.Duration\n expected bool\n }{\n {\"non-tombstone returns false\", \"open\", \"2024-01-01T00:00:00Z\", 0, false},\n {\"empty deleted_at returns false\", \"tombstone\", \"\", 0, false},\n {\"invalid timestamp returns false\", \"tombstone\", \"not-a-date\", 0, false},\n {\"within TTL returns false\", \"tombstone\", time.Now().Add(-1*time.Hour).Format(time.RFC3339), 24*time.Hour, false},\n {\"beyond TTL returns true\", \"tombstone\", time.Now().Add(-48*time.Hour).Format(time.RFC3339), 24*time.Hour, true},\n }\n // ...\n}\n```\n\n## Files\n- internal/merge/merge.go:255-290\n- internal/merge/merge_test.go (add TestIsExpiredTombstone)","status":"closed","priority":3,"issue_type":"task","created_at":"2025-12-05T16:36:09.106135-08:00","updated_at":"2025-12-07T02:16:16.881654-08:00","closed_at":"2025-12-07T02:16:16.881654-08:00"}
|
||||
{"id":"bd-ge7","title":"Improve Beads test coverage from 46% to 80%","description":"","status":"closed","priority":1,"issue_type":"epic","created_at":"2025-11-20T21:21:03.700271-05:00","updated_at":"2025-12-02T17:11:19.740758435-05:00","closed_at":"2025-11-28T21:56:04.085939-08:00"}
|
||||
{"id":"bd-ghb","title":"Add --yes flag to bd doctor --fix for non-interactive mode","description":"## Feature Request\n\nAdd a `--yes` or `-y` flag to `bd doctor --fix` that automatically confirms all prompts, enabling non-interactive usage in scripts and CI/CD pipelines.\n\n## Current Behavior\n`bd doctor --fix` prompts for confirmation before applying fixes, which blocks automated workflows.\n\n## Desired Behavior\n`bd doctor --fix --yes` should apply all fixes without prompting.\n\n## Use Cases\n- CI/CD pipelines that need to auto-fix issues\n- Scripts that automate repository setup\n- Pre-commit hooks that want to silently fix issues","status":"closed","priority":3,"issue_type":"feature","created_at":"2025-11-26T23:22:45.486584-08:00","updated_at":"2025-12-02T17:11:19.741550211-05:00","closed_at":"2025-11-28T21:55:06.895066-08:00"}
|
||||
{"id":"bd-gmf","title":"Update flake.nix vendorHash after dependency bumps","description":"The Nix flake CI is failing due to stale vendorHash.\n\nError from CI:\n```\nerror: hash mismatch in fixed-output derivation beads-0.24.4-go-modules.drv\n```\n\nRoot cause: go.mod/go.sum was updated by dependabot (wazero, anthropic-sdk-go, etc.) but flake.nix vendorHash wasn't updated.\n\n## Immediate Fix\nRun `nix build` locally, get the correct hash from the error message, update flake.nix.\n\n## Long-term Fix\nAdd a GitHub Action that auto-updates vendorHash when go.mod/go.sum changes:\n1. Trigger on push to main when go.mod/go.sum changes\n2. Run `nix build` to get the new hash from the error\n3. Update flake.nix with the correct hash\n4. Commit and push","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-02T23:16:06.851696-08:00","updated_at":"2025-12-02T23:21:39.917239-08:00","closed_at":"2025-12-02T23:21:39.917239-08:00"}
|
||||
@@ -108,7 +108,7 @@
|
||||
{"id":"bd-hdt","title":"Implement auto-merge functionality in duplicates command","description":"The duplicates.go file has a TODO at line 95 to implement the performMerge function for automatic duplicate merging. Currently it just prints a warning message. This would automate the merge process instead of just suggesting commands.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-21T18:55:02.828619-05:00","updated_at":"2025-11-28T19:50:01.115881-08:00","closed_at":"2025-11-27T22:36:11.517878-08:00"}
|
||||
{"id":"bd-hm8","title":"Add uninstall documentation (GH #445)","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-01T21:04:30.313637-08:00","updated_at":"2025-12-01T21:04:57.943192-08:00","closed_at":"2025-12-01T21:04:57.943192-08:00"}
|
||||
{"id":"bd-ho5","title":"Add 'town report' command for aggregated swarm status","description":"## Problem\nGetting a full swarm status requires running 6+ commands:\n- `town list \u003crig\u003e` for each rig\n- `town mail inbox` as Boss\n- `bd list --status=open/in_progress` per rig\n\nThis is slow and error-prone for both humans and agents.\n\n## Proposed Solution\nAdd `town report [RIG]` command that aggregates:\n- All rigs with polecat states (running/stopped, awake/asleep)\n- Boss inbox summary (unread count, recent senders)\n- Aggregate issue counts per rig (open/in_progress/blocked)\n\nExample output:\n```\n=== beads ===\nPolecats: 5 (5 running, 0 stopped)\nIssues: 20 open, 0 in_progress, 0 blocked\n\n=== gastown ===\nPolecats: 6 (4 running, 2 stopped)\nIssues: 0 open, 0 in_progress, 0 blocked\n\n=== Boss Mail ===\nUnread: 10 | Total: 22\nRecent: rictus (21:19), scrotus (21:14), immortanjoe (21:14)\n```\n\n## Acceptance Criteria\n- [ ] `town report` shows all rigs\n- [ ] `town report \u003crig\u003e` shows single rig detail\n- [ ] Output is concise and scannable\n- [ ] Completes in \u003c2 seconds","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-11-27T22:55:36.8919-08:00","updated_at":"2025-11-27T22:56:08.071838-08:00","closed_at":"2025-11-27T22:56:08.071838-08:00"}
|
||||
{"id":"bd-hp0m","title":"Add test for legacy deletions.jsonl to tombstone conversion","description":"The importer now converts legacy deletions.jsonl entries to tombstones (bd-dve), but there's no dedicated test that:\n1. Creates a deletions.jsonl with entries\n2. Imports issues (some in deletions, some not)\n3. Verifies the converted tombstones have correct fields from the deletion record\n\nThe existing TestImportIssues_TombstoneNotFilteredByDeletionsManifest tests that tombstones bypass the filter, but doesn't verify the conversion of legacy deletions to tombstones.\n\nAdd a test: TestImportIssues_LegacyDeletionsConvertedToTombstones","status":"in_progress","priority":2,"issue_type":"task","created_at":"2025-12-07T01:41:12.976726-08:00","updated_at":"2025-12-07T02:04:40.703606-08:00","dependencies":[{"issue_id":"bd-hp0m","depends_on_id":"bd-dve","type":"blocks","created_at":"2025-12-07T01:41:28.391366-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-hp0m","title":"Add test for legacy deletions.jsonl to tombstone conversion","description":"The importer now converts legacy deletions.jsonl entries to tombstones (bd-dve), but there's no dedicated test that:\n1. Creates a deletions.jsonl with entries\n2. Imports issues (some in deletions, some not)\n3. Verifies the converted tombstones have correct fields from the deletion record\n\nThe existing TestImportIssues_TombstoneNotFilteredByDeletionsManifest tests that tombstones bypass the filter, but doesn't verify the conversion of legacy deletions to tombstones.\n\nAdd a test: TestImportIssues_LegacyDeletionsConvertedToTombstones","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-07T01:41:12.976726-08:00","updated_at":"2025-12-07T02:16:16.882286-08:00","closed_at":"2025-12-07T02:16:16.882286-08:00","dependencies":[{"issue_id":"bd-hp0m","depends_on_id":"bd-dve","type":"blocks","created_at":"2025-12-07T01:41:28.391366-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-ig5","title":"Duplicate tombstone constants in merge and types packages","description":"## Problem\n\nThe tombstone constants are duplicated between two packages:\n\n**internal/types/types.go:**\n```go\nconst DefaultTombstoneTTL = 30 * 24 * time.Hour\nconst ClockSkewGrace = 1 * time.Hour\nconst StatusTombstone Status = \"tombstone\"\n```\n\n**internal/merge/merge.go:**\n```go\nconst StatusTombstone = \"tombstone\"\nconst DefaultTombstoneTTL = 30 * 24 * time.Hour\nconst ClockSkewGrace = 1 * time.Hour\n```\n\nThis violates DRY and creates risk of divergence if one is updated but not the other.\n\n## Root Cause\n\nThe merge package has its own `Issue` struct (for JSONL parsing) and cannot import types.Issue directly due to the different struct design. However, the constants could be shared.\n\n## Recommendation\n\nExport the constants from types package and import them in merge:\n\n```go\n// merge/merge.go\nimport \"github.com/steveyegge/beads/internal/types\"\n\n// Use types.StatusTombstone, types.DefaultTombstoneTTL, types.ClockSkewGrace\n```\n\nOr create a shared constants package if import cycles are a concern.\n\n## Files\n- internal/merge/merge.go:241-248\n- internal/types/types.go:77-84, 170","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-12-05T16:36:06.159443-08:00","updated_at":"2025-12-05T17:15:57.624376-08:00","closed_at":"2025-12-05T17:15:57.624376-08:00"}
|
||||
{"id":"bd-iip5","title":"TestImportIssues_LegacyDeletionsConvertedToTombstones is failing","description":"The test TestImportIssues_LegacyDeletionsConvertedToTombstones in internal/importer/importer_test.go is failing:\n\nExpected 3 created (1 regular + 2 tombstones from deletions.jsonl), got 2\nExpected tombstone for test-deleted2 not found\n\nThe test expects legacy deletions.jsonl entries to be converted to tombstones during import, but test-deleted2 is not being converted.\n\nLocation: internal/importer/importer_test.go:1344","status":"open","priority":2,"issue_type":"bug","created_at":"2025-12-07T02:09:19.17774-08:00","updated_at":"2025-12-07T02:09:19.17774-08:00"}
|
||||
{"id":"bd-imj","title":"Deletion propagation via deletions manifest","description":"## Problem\n\nWhen `bd cleanup -f` or `bd delete` removes issues in one clone, those deletions don't propagate to other clones. The import logic only creates/updates, never deletes. This causes \"resurrection\" where deleted issues reappear.\n\n## Root Cause\n\nImport sees DB issues not in JSONL and assumes they're \"local unpushed work\" rather than \"intentionally deleted upstream.\"\n\n## Solution: Deletions Manifest\n\nAdd `.beads/deletions.jsonl` - an append-only log of deleted issue IDs with metadata.\n\n### Format\n```jsonl\n{\"id\":\"bd-xxx\",\"ts\":\"2025-11-25T10:00:00Z\",\"by\":\"stevey\"}\n{\"id\":\"bd-yyy\",\"ts\":\"2025-11-25T10:05:00Z\",\"by\":\"claude\",\"reason\":\"duplicate of bd-zzz\"}\n```\n\n### Fields\n- `id`: Issue ID (required)\n- `ts`: ISO 8601 UTC timestamp (required)\n- `by`: Actor who deleted (required)\n- `reason`: Optional context (\"cleanup\", \"duplicate of X\", etc.)\n\n### Import Logic\n```\nFor each DB issue not in JSONL:\n 1. Check deletions manifest → if found, delete from DB\n 2. Fallback: check git history → if found, delete + backfill manifest\n 3. Neither → keep (local unpushed work)\n```\n\n### Conflict Resolution\nSimultaneous deletions from multiple clones are handled naturally:\n- Append-only design means both clones append their deletion records\n- On merge, file contains duplicate entries (same ID, different timestamps)\n- `LoadDeletions` deduplicates by ID (keeps any/first entry)\n- Result: deletion propagates correctly, duplicates are harmless\n\n### Pruning Policy\n- Default retention: 7 days (configurable via `deletions.retention_days`)\n- Auto-compact during `bd sync` is **opt-in** (disabled by default)\n- Hard cap: `deletions.max_entries` (default 50000)\n- Git fallback handles pruned entries (self-healing)\n\n### Self-Healing\nWhen git fallback catches a resurrection (pruned entry), it backfills the manifest. One-time git scan cost, then fast again.\n\n### Size Estimates\n- ~80 bytes/entry\n- 7-day retention with 100 deletions/day = ~56KB\n- Git compressed: ~10KB\n\n## Benefits\n- ✅ Deletions propagate across clones\n- ✅ O(1) lookup (no git scan in normal case)\n- ✅ Works in shallow clones\n- ✅ Survives history rewrite\n- ✅ Audit trail (who deleted what when)\n- ✅ Self-healing via git fallback\n- ✅ Bounded size via time-based pruning\n\n## References\n- Investigation session: 2025-11-25\n- Related: bd-2q6d (stale database warnings)","status":"closed","priority":0,"issue_type":"epic","created_at":"2025-11-25T09:56:01.98027-08:00","updated_at":"2025-11-25T16:36:27.965168-08:00","closed_at":"2025-11-25T16:36:27.965168-08:00","dependencies":[{"issue_id":"bd-imj","depends_on_id":"bd-qsm","type":"blocks","created_at":"2025-11-25T09:57:42.821911-08:00","created_by":"daemon"},{"issue_id":"bd-imj","depends_on_id":"bd-x2i","type":"blocks","created_at":"2025-11-25T09:57:42.851712-08:00","created_by":"daemon"},{"issue_id":"bd-imj","depends_on_id":"bd-44e","type":"blocks","created_at":"2025-11-25T09:57:42.88154-08:00","created_by":"daemon"},{"issue_id":"bd-imj","depends_on_id":"bd-bhd","type":"blocks","created_at":"2025-11-25T14:56:23.675787-08:00","created_by":"daemon"},{"issue_id":"bd-imj","depends_on_id":"bd-bgs","type":"blocks","created_at":"2025-11-25T14:56:23.744648-08:00","created_by":"daemon"},{"issue_id":"bd-imj","depends_on_id":"bd-f0n","type":"blocks","created_at":"2025-11-25T14:56:23.80649-08:00","created_by":"daemon"},{"issue_id":"bd-imj","depends_on_id":"bd-v29","type":"blocks","created_at":"2025-11-25T14:56:23.864569-08:00","created_by":"daemon"},{"issue_id":"bd-imj","depends_on_id":"bd-mdw","type":"blocks","created_at":"2025-11-25T14:56:48.592492-08:00","created_by":"daemon"},{"issue_id":"bd-imj","depends_on_id":"bd-03r","type":"blocks","created_at":"2025-11-25T14:56:54.295851-08:00","created_by":"daemon"}]}
|
||||
@@ -170,7 +170,7 @@
|
||||
{"id":"bd-y68","title":"Block direct status update to tombstone in UpdateIssue","description":"Per bd-2m7 design: 'Should we support tombstone as valid input to bd update --status? Recommendation: No, tombstones only created via bd delete'\n\nCurrently nothing prevents 'bd update \u003cid\u003e --status=tombstone', which would create an invalid tombstone (missing deleted_at, deleted_by, etc.).\n\nLocation: internal/storage/sqlite/queries.go:500-664 UpdateIssue\n\nAdd validation to reject status=tombstone in updates.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-05T15:35:31.5176-08:00","updated_at":"2025-12-05T15:41:14.813862-08:00","closed_at":"2025-12-05T15:41:14.813862-08:00","dependencies":[{"issue_id":"bd-y68","depends_on_id":"bd-vw8","type":"parent-child","created_at":"2025-12-05T15:35:47.181896-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-ybv5","title":"Refactor AGENTS.md to use external references","description":"Suggestion to use external references (e.g., \"ALWAYS REFER TO ./beads/prompt.md\") instead of including all instructions directly within AGENTS.md.\nReasons:\n1. Agents can follow external references.\n2. Prevents context pollution/stuffing in AGENTS.md as more tools append instructions.\n","status":"closed","priority":3,"issue_type":"task","created_at":"2025-11-20T18:55:53.259144-05:00","updated_at":"2025-11-26T22:25:57.772875-08:00","closed_at":"2025-11-26T22:25:57.772875-08:00"}
|
||||
{"id":"bd-ye0d","title":"troubleshoot GH#278 daemon exits every few secs","description":"","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-13T06:27:23.39509215-07:00","updated_at":"2025-11-25T17:48:43.62418-08:00","closed_at":"2025-11-25T17:48:43.62418-08:00"}
|
||||
{"id":"bd-yk8w","title":"Add export test verifying tombstones are included in JSONL output","description":"The export now includes tombstones (bd-dve) but there's no test that:\n1. Creates issues in DB including a tombstone\n2. Runs export\n3. Verifies the JSONL contains the tombstone with all fields\n\nAdd a test: TestExportIncludesTombstones","status":"in_progress","priority":2,"issue_type":"task","created_at":"2025-12-07T01:41:19.987907-08:00","updated_at":"2025-12-07T02:04:40.757658-08:00","dependencies":[{"issue_id":"bd-yk8w","depends_on_id":"bd-dve","type":"blocks","created_at":"2025-12-07T01:41:28.426945-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-yk8w","title":"Add export test verifying tombstones are included in JSONL output","description":"The export now includes tombstones (bd-dve) but there's no test that:\n1. Creates issues in DB including a tombstone\n2. Runs export\n3. Verifies the JSONL contains the tombstone with all fields\n\nAdd a test: TestExportIncludesTombstones","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-07T01:41:19.987907-08:00","updated_at":"2025-12-07T02:16:16.882688-08:00","closed_at":"2025-12-07T02:16:16.882688-08:00","dependencies":[{"issue_id":"bd-yk8w","depends_on_id":"bd-dve","type":"blocks","created_at":"2025-12-07T01:41:28.426945-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-yuv","title":"Add tests for tombstone edge cases","description":"Missing test coverage for tombstone implementation:\n\n1. Test tombstone validation (deleted_at requirement)\n2. Test GetReadyWork excludes tombstones\n3. Test SearchIssues behavior with tombstones\n4. Integration test for creating/retrieving a tombstone via the delete command\n5. Test that status=tombstone update is rejected","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-05T15:35:39.101881-08:00","updated_at":"2025-12-05T15:35:39.101881-08:00","dependencies":[{"issue_id":"bd-yuv","depends_on_id":"bd-vw8","type":"parent-child","created_at":"2025-12-05T15:35:47.335167-08:00","created_by":"daemon"}]}
|
||||
{"id":"bd-zai","title":"bd init resets metadata.json jsonl_export to beads.jsonl, ignoring existing issues.jsonl","description":"When running 'bd init --prefix bd' in a repo that already has .beads/issues.jsonl, the init command overwrites metadata.json and sets jsonl_export back to 'beads.jsonl' instead of detecting and respecting the existing issues.jsonl file.\n\nSteps to reproduce:\n1. Have a repo with .beads/issues.jsonl (canonical) and metadata.json pointing to issues.jsonl\n2. Delete beads.db and run 'bd init --prefix bd'\n3. Check metadata.json - it now says jsonl_export: beads.jsonl\n\nExpected: Init should detect existing issues.jsonl and use it.\n\nWorkaround: Manually edit metadata.json after init.","status":"closed","priority":2,"issue_type":"bug","created_at":"2025-11-26T22:27:41.653287-08:00","updated_at":"2025-12-02T17:11:19.752292588-05:00","closed_at":"2025-11-28T21:54:32.52461-08:00"}
|
||||
{"id":"bd-zj8e","title":"Performance Testing Documentation","description":"Create docs/performance-testing.md documenting the performance testing framework.\n\nSections:\n1. Overview - What the framework does, goals\n2. Running Benchmarks\n - make bench command\n - Running specific benchmarks\n - Interpreting output (ns/op, allocs/op)\n3. Profiling and Analysis\n - Viewing CPU profiles with pprof\n - Reading flamegraphs\n - Memory profiling\n - Finding hotspots\n4. User Diagnostics\n - bd doctor --perf usage\n - Sharing profiles with bug reports\n - Understanding the report output\n5. Comparing Performance\n - Using benchstat for before/after comparisons\n - Detecting regressions\n6. Tips for Optimization\n - Common patterns\n - When to profile vs benchmark\n\nStyle:\n- Concise, practical examples\n- Screenshots/examples of pprof output\n- Clear command-line examples\n- Focus on workflow, not theory","status":"closed","priority":2,"issue_type":"task","created_at":"2025-11-13T22:23:38.99897-08:00","updated_at":"2025-12-02T17:11:19.753645934-05:00","closed_at":"2025-11-28T23:37:52.227831-08:00"}
|
||||
|
||||
@@ -1266,3 +1266,116 @@ func TestImportIssues_TombstoneNotFilteredByDeletionsManifest(t *testing.T) {
|
||||
t.Errorf("Expected 0 skipped deleted (tombstone should not be filtered), got %d", result.SkippedDeleted)
|
||||
}
|
||||
}
|
||||
|
||||
// TestImportIssues_LegacyDeletionsConvertedToTombstones tests that entries in
|
||||
// deletions.jsonl are converted to tombstones during import (bd-hp0m)
|
||||
func TestImportIssues_LegacyDeletionsConvertedToTombstones(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
tmpDB := tmpDir + "/test.db"
|
||||
store, err := sqlite.New(context.Background(), tmpDB)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create store: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
if err := store.SetConfig(ctx, "issue_prefix", "test"); err != nil {
|
||||
t.Fatalf("Failed to set prefix: %v", err)
|
||||
}
|
||||
|
||||
// Create a deletions manifest with one entry
|
||||
deletionsPath := deletions.DefaultPath(tmpDir)
|
||||
deleteTime := time.Now().Add(-time.Hour)
|
||||
|
||||
del := deletions.DeletionRecord{
|
||||
ID: "test-abc",
|
||||
Timestamp: deleteTime,
|
||||
Actor: "alice",
|
||||
Reason: "duplicate of test-xyz",
|
||||
}
|
||||
if err := deletions.AppendDeletion(deletionsPath, del); err != nil {
|
||||
t.Fatalf("Failed to write deletion record: %v", err)
|
||||
}
|
||||
|
||||
// Create a regular issue (not in deletions)
|
||||
regularIssue := &types.Issue{
|
||||
ID: "test-def",
|
||||
Title: "Regular issue",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 2,
|
||||
IssueType: types.TypeTask,
|
||||
CreatedAt: time.Now().Add(-24 * time.Hour),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Create an issue that's in the deletions manifest (non-tombstone)
|
||||
deletedIssue := &types.Issue{
|
||||
ID: "test-abc",
|
||||
Title: "This will be skipped and converted",
|
||||
Status: types.StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: types.TypeBug,
|
||||
CreatedAt: time.Now().Add(-48 * time.Hour),
|
||||
UpdatedAt: time.Now().Add(-2 * time.Hour),
|
||||
}
|
||||
|
||||
// Import both issues
|
||||
result, err := ImportIssues(ctx, tmpDB, store, []*types.Issue{regularIssue, deletedIssue}, Options{})
|
||||
if err != nil {
|
||||
t.Fatalf("Import failed: %v", err)
|
||||
}
|
||||
|
||||
// Regular issue should be created
|
||||
// The deleted issue is skipped (in deletions manifest), but a tombstone is created from deletions.jsonl
|
||||
// So we expect: 1 regular + 1 tombstone = 2 created
|
||||
if result.Created != 2 {
|
||||
t.Errorf("Expected 2 created (1 regular + 1 tombstone from deletions.jsonl), got %d", result.Created)
|
||||
}
|
||||
if result.SkippedDeleted != 1 {
|
||||
t.Errorf("Expected 1 skipped deleted (issue in deletions.jsonl), got %d", result.SkippedDeleted)
|
||||
}
|
||||
|
||||
// Verify regular issue was imported
|
||||
issues, err := store.SearchIssues(ctx, "", types.IssueFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to search issues: %v", err)
|
||||
}
|
||||
foundRegular := false
|
||||
for _, i := range issues {
|
||||
if i.ID == "test-def" {
|
||||
foundRegular = true
|
||||
}
|
||||
}
|
||||
if !foundRegular {
|
||||
t.Error("Regular issue not found after import")
|
||||
}
|
||||
|
||||
// Verify tombstone was created from deletions.jsonl
|
||||
allIssues, err := store.SearchIssues(ctx, "", types.IssueFilter{IncludeTombstones: true})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to search all issues: %v", err)
|
||||
}
|
||||
|
||||
var tombstone *types.Issue
|
||||
for _, i := range allIssues {
|
||||
if i.ID == "test-abc" {
|
||||
tombstone = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// test-abc should be a tombstone (was in JSONL and deletions)
|
||||
if tombstone == nil {
|
||||
t.Fatal("Expected tombstone for test-abc not found")
|
||||
}
|
||||
if tombstone.Status != types.StatusTombstone {
|
||||
t.Errorf("Expected test-abc to be tombstone, got status %q", tombstone.Status)
|
||||
}
|
||||
if tombstone.DeletedBy != "alice" {
|
||||
t.Errorf("Expected DeletedBy 'alice', got %q", tombstone.DeletedBy)
|
||||
}
|
||||
if tombstone.DeleteReason != "duplicate of test-xyz" {
|
||||
t.Errorf("Expected DeleteReason 'duplicate of test-xyz', got %q", tombstone.DeleteReason)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1905,3 +1905,156 @@ func TestMergeIssue_TombstoneFields(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestIsExpiredTombstone tests edge cases for the IsExpiredTombstone function (bd-fmo)
|
||||
func TestIsExpiredTombstone(t *testing.T) {
|
||||
now := time.Now()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issue Issue
|
||||
ttl time.Duration
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "non-tombstone returns false",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: "open",
|
||||
DeletedAt: now.Add(-100 * 24 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
ttl: 24 * time.Hour,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "closed status returns false",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: "closed",
|
||||
DeletedAt: now.Add(-100 * 24 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
ttl: 24 * time.Hour,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "tombstone with empty deleted_at returns false",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: StatusTombstone,
|
||||
DeletedAt: "",
|
||||
},
|
||||
ttl: 24 * time.Hour,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "tombstone with invalid timestamp returns false (safety)",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: StatusTombstone,
|
||||
DeletedAt: "not-a-valid-date",
|
||||
},
|
||||
ttl: 24 * time.Hour,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "tombstone with malformed RFC3339 returns false",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: StatusTombstone,
|
||||
DeletedAt: "2024-13-45T99:99:99Z",
|
||||
},
|
||||
ttl: 24 * time.Hour,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "recent tombstone (within TTL) returns false",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: StatusTombstone,
|
||||
DeletedAt: now.Add(-1 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
ttl: 24 * time.Hour,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "old tombstone (beyond TTL) returns true",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: StatusTombstone,
|
||||
DeletedAt: now.Add(-48 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
ttl: 24 * time.Hour,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "tombstone just inside TTL boundary (with clock skew grace) returns false",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: StatusTombstone,
|
||||
DeletedAt: now.Add(-24 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
ttl: 24 * time.Hour,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "tombstone just past TTL boundary (with clock skew grace) returns true",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: StatusTombstone,
|
||||
DeletedAt: now.Add(-26 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
ttl: 24 * time.Hour,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "ttl=0 falls back to DefaultTombstoneTTL (30 days)",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: StatusTombstone,
|
||||
DeletedAt: now.Add(-20 * 24 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
ttl: 0,
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "ttl=0 with old tombstone (beyond default TTL) returns true",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: StatusTombstone,
|
||||
DeletedAt: now.Add(-60 * 24 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
ttl: 0,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "RFC3339Nano format is supported",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: StatusTombstone,
|
||||
DeletedAt: now.Add(-48 * time.Hour).Format(time.RFC3339Nano),
|
||||
},
|
||||
ttl: 24 * time.Hour,
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "very short TTL (1 minute) works correctly",
|
||||
issue: Issue{
|
||||
ID: "bd-test",
|
||||
Status: StatusTombstone,
|
||||
DeletedAt: now.Add(-2 * time.Hour).Format(time.RFC3339),
|
||||
},
|
||||
ttl: 1 * time.Minute,
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := IsExpiredTombstone(tt.issue, tt.ttl)
|
||||
if result != tt.expected {
|
||||
t.Errorf("IsExpiredTombstone() = %v, want %v (deleted_at=%q, ttl=%v)",
|
||||
result, tt.expected, tt.issue.DeletedAt, tt.ttl)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user