require_relative "test_helper" require "tmpdir" class FakeIssuesRunner attr_reader :calls def initialize(responses) @responses = responses.dup @calls = [] end def capture(*cmd) @calls << cmd @responses.shift || ["", true] end end class IssuesCreateTest < Minitest::Test def test_forgejo_create_shells_out_to_tea_with_priority_label runner = FakeIssuesRunner.new([["Created issue #7: https://forgejo.example/jared/cc-os/issues/7\n", true]]) issues = Backlog::Issues.new(runner: runner) result = issues.create(tracker: "forgejo:jared/cc-os", title: "Fix the thing", body: "context here", priority: "P1") assert_equal 7, result[:number] assert_equal "https://forgejo.example/jared/cc-os/issues/7", result[:url] cmd = runner.calls.first assert_equal "tea", cmd[0] assert_includes cmd, "--repo" assert_includes cmd, "jared/cc-os" assert_includes cmd, "--title" assert_includes cmd, "Fix the thing" assert_includes cmd, "--labels" assert_includes cmd, "P1" end def test_forgejo_create_omits_labels_flag_without_priority runner = FakeIssuesRunner.new([["Created issue #1: https://x/issues/1\n", true]]) Backlog::Issues.new(runner: runner).create(tracker: "forgejo:jared/cc-os", title: "T", body: "B") refute_includes runner.calls.first, "--labels" end def test_github_create_shells_out_to_gh runner = FakeIssuesRunner.new([["https://github.com/acme/widget/issues/42\n", true]]) issues = Backlog::Issues.new(runner: runner) result = issues.create(tracker: "github:acme/widget", title: "Fix", body: "ctx", priority: "P0") assert_equal 42, result[:number] assert_equal "https://github.com/acme/widget/issues/42", result[:url] cmd = runner.calls.first assert_equal "gh", cmd[0] assert_includes cmd, "--label" assert_includes cmd, "P0" end def test_create_raises_command_failed_on_nonzero_exit runner = FakeIssuesRunner.new([["auth error", false]]) issues = Backlog::Issues.new(runner: runner) assert_raises(Backlog::Issues::CommandFailed) do issues.create(tracker: "forgejo:jared/cc-os", title: "T", body: "B") end end def test_create_rejects_invalid_priority issues = Backlog::Issues.new(runner: FakeIssuesRunner.new([])) assert_raises(ArgumentError) do issues.create(tracker: "forgejo:jared/cc-os", title: "T", body: "B", priority: "P9") end end def test_create_can_never_apply_the_next_label # There is no `labels:`/state-label parameter on #create at all — the # only label surface is `priority:`, restricted to P0-P3. This test # documents that constraint as executable spec. issues = Backlog::Issues.new(runner: FakeIssuesRunner.new([])) assert_raises(ArgumentError) do issues.create(tracker: "forgejo:jared/cc-os", title: "T", body: "B", priority: "next") end end def test_repo_create_writes_a_frontmattered_markdown_file Dir.mktmpdir do |dir| issues = Backlog::Issues.new(runner: FakeIssuesRunner.new([]), repo_root: dir, clock: -> { Time.new(2026, 7, 16) }) result = issues.create(tracker: "repo:docs/issues", title: "Fix the Thing!", body: "details here", priority: "P2") assert_equal 1, result[:number] assert_equal File.join(dir, "docs/issues/001-fix-the-thing.md"), result[:path] contents = File.read(result[:path]) assert_includes contents, "number: 1" assert_includes contents, "title: Fix the Thing!" assert_includes contents, "labels: P2" assert_includes contents, "state: open" assert_includes contents, "created: 2026-07-16" assert_includes contents, "details here" end end def test_repo_create_numbers_sequentially Dir.mktmpdir do |dir| issues = Backlog::Issues.new(runner: FakeIssuesRunner.new([]), repo_root: dir) issues.create(tracker: "repo:docs/issues", title: "First", body: "b") second = issues.create(tracker: "repo:docs/issues", title: "Second", body: "b") assert_equal 2, second[:number] end end end class IssuesListTest < Minitest::Test def test_forgejo_list_groups_by_state_label tea_json = JSON.generate([ { "index" => 1, "title" => "Next thing", "labels" => "P1,next", "assignees" => "jared" }, { "index" => 2, "title" => "Blocked thing", "labels" => "waiting", "assignees" => "" }, { "index" => 3, "title" => "Shipped semi", "labels" => "review", "assignees" => "" }, { "index" => 4, "title" => "Ad hoc", "labels" => "P3", "assignees" => "" } ]) runner = FakeIssuesRunner.new([[tea_json, true]]) grouped = Backlog::Issues.new(runner: runner).list(tracker: "forgejo:jared/cc-os") assert_equal [1], grouped[:next].map { |i| i[:number] } assert_equal [2], grouped[:waiting].map { |i| i[:number] } assert_equal [3], grouped[:review].map { |i| i[:number] } assert_equal [4], grouped[:other].map { |i| i[:number] } assert_equal "jared", grouped[:next].first[:assignee] cmd = runner.calls.first assert_equal "tea", cmd[0] end def test_github_list_groups_by_state_label gh_json = JSON.generate([ { "number" => 5, "title" => "Ship it", "labels" => [{ "name" => "review" }], "assignees" => [{ "login" => "jared" }] } ]) runner = FakeIssuesRunner.new([[gh_json, true]]) grouped = Backlog::Issues.new(runner: runner).list(tracker: "github:acme/widget") assert_equal [5], grouped[:review].map { |i| i[:number] } assert_equal "jared", grouped[:review].first[:assignee] assert_equal "gh", runner.calls.first[0] end def test_list_raises_command_failed_on_nonzero_exit runner = FakeIssuesRunner.new([["boom", false]]) assert_raises(Backlog::Issues::CommandFailed) do Backlog::Issues.new(runner: runner).list(tracker: "forgejo:jared/cc-os") end end def test_repo_list_reads_open_issue_files_and_groups_them Dir.mktmpdir do |dir| issues = Backlog::Issues.new(runner: FakeIssuesRunner.new([]), repo_root: dir) issues.create(tracker: "repo:docs/issues", title: "Waiting one", body: "b", priority: "P1") waiting_path = Dir.glob(File.join(dir, "docs/issues/*.md")).first File.write(waiting_path, File.read(waiting_path).sub("labels: P1", "labels: P1,waiting")) grouped = issues.list(tracker: "repo:docs/issues") assert_equal ["Waiting one"], grouped[:waiting].map { |i| i[:title] } end end def test_repo_list_is_empty_when_directory_absent Dir.mktmpdir do |dir| grouped = Backlog::Issues.new(runner: FakeIssuesRunner.new([]), repo_root: dir) .list(tracker: "repo:docs/issues") assert_equal [], grouped[:other] end end def test_unroutable_tracker_raises issues = Backlog::Issues.new(runner: FakeIssuesRunner.new([])) assert_raises(ArgumentError) { issues.list(tracker: "planka:board") } assert_raises(ArgumentError) { issues.create(tracker: nil, title: "T", body: "B") } end end