require_relative "test_helper" require "tmpdir" require "fileutils" require "json" class ProjectIndexTest < Minitest::Test def with_index Dir.mktmpdir do |dir| path = File.join(dir, ".cc-os", "projects.json") yield Backlog::ProjectIndex.new(path: path, today: -> { "2026-07-13" }), path end end def test_missing_file_reads_as_empty with_index do |index, _path| assert_equal({}, index.all) end end def test_corrupt_file_reads_as_empty_and_is_not_clobbered_by_reads with_index do |index, path| FileUtils.mkdir_p(File.dirname(path)) File.write(path, "{not json") assert_equal({}, index.all) assert_equal "{not json", File.read(path) # read paths never rewrite end end def test_upsert_creates_row_with_updated_at with_index do |index, path| index.upsert(path: "/repos/foo", name: "foo", tracker: "planka:foo", remote: nil) data = JSON.parse(File.read(path)) assert_equal 1, data["version"] row = data["projects"]["/repos/foo"] assert_equal "foo", row["name"] assert_equal "planka:foo", row["tracker"] assert_nil row["remote"] assert_equal "2026-07-13", row["updated_at"] end end def test_upsert_updates_existing_row_keyed_by_path with_index do |index, _path| index.upsert(path: "/repos/foo", name: "foo", tracker: "planka:foo", remote: nil) index.upsert(path: "/repos/foo", name: "foo", tracker: "github:me/foo", remote: "r") assert_equal 1, index.all.size assert_equal "github:me/foo", index.all["/repos/foo"]["tracker"] end end def test_name_collision_two_paths_same_basename_keeps_both with_index do |index, _path| index.upsert(path: "/a/foo", name: "foo", tracker: "planka:foo", remote: nil) index.upsert(path: "/b/foo", name: "foo", tracker: "github:me/foo", remote: nil) assert_equal %w[/a/foo /b/foo], index.all.keys.sort end end def test_upsert_over_corrupt_file_leaves_valid_json_and_no_temp_files with_index do |index, path| FileUtils.mkdir_p(File.dirname(path)) File.write(path, "{not json") index.upsert(path: "/repos/foo", name: "foo", tracker: "repo:.", remote: nil) data = JSON.parse(File.read(path)) assert_equal ["/repos/foo"], data["projects"].keys leftovers = Dir.children(File.dirname(path)).reject { |f| f == "projects.json" } assert_empty leftovers, "atomic write left temp files: #{leftovers}" end end def test_default_path_honors_cc_os_home_override path = Backlog::ProjectIndex.default_path(env: { "CC_OS_HOME" => "/tmp/x", "HOME" => "/home/y" }) assert_equal "/tmp/x/projects.json", path end def test_default_path_falls_back_to_home path = Backlog::ProjectIndex.default_path(env: { "HOME" => "/home/y" }) assert_equal "/home/y/.cc-os/projects.json", path end end class ProjectsCliContractTest < Minitest::Test BIN = File.expand_path("../bin/os-backlog", __dir__) # config-write must upsert the index (redirected via CC_OS_HOME so the # real ~/.cc-os is never touched), and `projects` must always emit a # top-level "projects" key — even when the index is empty. def test_config_write_upserts_index_and_projects_lists_it Dir.mktmpdir do |home| Dir.mktmpdir do |repo| env = "CC_OS_HOME=#{home} HOME=#{home}" out = Dir.chdir(repo) { `#{env} ruby #{BIN} projects 2>/dev/null` } assert_equal({ "projects" => {} }, JSON.parse(out)) Dir.chdir(repo) { `#{env} ruby #{BIN} config-write planka:demo 2>/dev/null` } out = Dir.chdir(repo) { `#{env} ruby #{BIN} projects 2>/dev/null` } projects = JSON.parse(out)["projects"] assert_equal 1, projects.size row = projects.values.first assert_equal "planka:demo", row["tracker"] assert_equal File.basename(File.realpath(repo)), row["name"] filtered = Dir.chdir(repo) { `#{env} ruby #{BIN} projects no-such-name 2>/dev/null` } assert_equal({}, JSON.parse(filtered)["projects"]) by_tracker = Dir.chdir(repo) { `#{env} ruby #{BIN} projects planka:demo 2>/dev/null` } assert_equal 1, JSON.parse(by_tracker)["projects"].size end end end # Umbrella repos: config-write from a subdirectory writes the config to # that subdirectory, so the index row must be keyed by the SUBDIRECTORY # (the project the config describes), never the enclosing git toplevel — # otherwise every subproject masquerades as the umbrella repo. def test_config_write_in_umbrella_subdir_indexes_the_subdir Dir.mktmpdir do |home| Dir.mktmpdir do |repo| `git -C #{repo} init -q 2>/dev/null` subdir = File.join(repo, "child-project") FileUtils.mkdir_p(subdir) env = "CC_OS_HOME=#{home} HOME=#{home}" Dir.chdir(subdir) { `#{env} ruby #{BIN} config-write planka:child 2>/dev/null` } projects = Dir.chdir(subdir) { JSON.parse(`#{env} ruby #{BIN} projects 2>/dev/null`)["projects"] } assert_equal [File.realpath(subdir)], projects.keys assert_equal "child-project", projects.values.first["name"] end end end end