2026-07-13 14:48:19 +00:00
|
|
|
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))
|
|
|
|
|
|
Retire Planka: os-backlog reworked to git-issues-only (ADR-0042)
Executes OpenSpec change retire-planka-git-issues-only (archived, 25/25 tasks):
- os-backlog v0.3.0: Planka lib/CLI/agents deleted; issue-create/issues
helpers (tea/gh/repo-file dispatch); ten-label taxonomy, human-only next
- planka: rejected fail-soft citing ADR-0042 in tracker grammar,
config-write, and the os-status check
- Skills, SessionStart note (~687 -> ~444 tokens), /to-issues, os-status fix
rewritten to the single-tracker model
- All boards snapshotted + pg-dumped (ovh-vps ~/planka-final-snapshot-2026-07-16);
cards migrated to jared/cc-os#61-70, jared/ops#1-21, jared/llf-schema#7-10
- Planka server decommissioned; planka gem repo archived read-only
- Delta specs synced to openspec/specs/ (issue-backlog, issue-state-labels)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HWNfGWoWnhrub4EJa1M1Y
2026-07-16 19:48:40 +00:00
|
|
|
Dir.chdir(repo) { `#{env} ruby #{BIN} config-write forgejo:jared/demo 2>/dev/null` }
|
2026-07-13 14:48:19 +00:00
|
|
|
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
|
Retire Planka: os-backlog reworked to git-issues-only (ADR-0042)
Executes OpenSpec change retire-planka-git-issues-only (archived, 25/25 tasks):
- os-backlog v0.3.0: Planka lib/CLI/agents deleted; issue-create/issues
helpers (tea/gh/repo-file dispatch); ten-label taxonomy, human-only next
- planka: rejected fail-soft citing ADR-0042 in tracker grammar,
config-write, and the os-status check
- Skills, SessionStart note (~687 -> ~444 tokens), /to-issues, os-status fix
rewritten to the single-tracker model
- All boards snapshotted + pg-dumped (ovh-vps ~/planka-final-snapshot-2026-07-16);
cards migrated to jared/cc-os#61-70, jared/ops#1-21, jared/llf-schema#7-10
- Planka server decommissioned; planka gem repo archived read-only
- Delta specs synced to openspec/specs/ (issue-backlog, issue-state-labels)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HWNfGWoWnhrub4EJa1M1Y
2026-07-16 19:48:40 +00:00
|
|
|
assert_equal "forgejo:jared/demo", row["tracker"]
|
2026-07-13 14:48:19 +00:00
|
|
|
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"])
|
2026-07-13 16:27:43 +00:00
|
|
|
|
Retire Planka: os-backlog reworked to git-issues-only (ADR-0042)
Executes OpenSpec change retire-planka-git-issues-only (archived, 25/25 tasks):
- os-backlog v0.3.0: Planka lib/CLI/agents deleted; issue-create/issues
helpers (tea/gh/repo-file dispatch); ten-label taxonomy, human-only next
- planka: rejected fail-soft citing ADR-0042 in tracker grammar,
config-write, and the os-status check
- Skills, SessionStart note (~687 -> ~444 tokens), /to-issues, os-status fix
rewritten to the single-tracker model
- All boards snapshotted + pg-dumped (ovh-vps ~/planka-final-snapshot-2026-07-16);
cards migrated to jared/cc-os#61-70, jared/ops#1-21, jared/llf-schema#7-10
- Planka server decommissioned; planka gem repo archived read-only
- Delta specs synced to openspec/specs/ (issue-backlog, issue-state-labels)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HWNfGWoWnhrub4EJa1M1Y
2026-07-16 19:48:40 +00:00
|
|
|
by_tracker = Dir.chdir(repo) { `#{env} ruby #{BIN} projects forgejo:jared/demo 2>/dev/null` }
|
2026-07-13 16:27:43 +00:00
|
|
|
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}"
|
|
|
|
|
|
Retire Planka: os-backlog reworked to git-issues-only (ADR-0042)
Executes OpenSpec change retire-planka-git-issues-only (archived, 25/25 tasks):
- os-backlog v0.3.0: Planka lib/CLI/agents deleted; issue-create/issues
helpers (tea/gh/repo-file dispatch); ten-label taxonomy, human-only next
- planka: rejected fail-soft citing ADR-0042 in tracker grammar,
config-write, and the os-status check
- Skills, SessionStart note (~687 -> ~444 tokens), /to-issues, os-status fix
rewritten to the single-tracker model
- All boards snapshotted + pg-dumped (ovh-vps ~/planka-final-snapshot-2026-07-16);
cards migrated to jared/cc-os#61-70, jared/ops#1-21, jared/llf-schema#7-10
- Planka server decommissioned; planka gem repo archived read-only
- Delta specs synced to openspec/specs/ (issue-backlog, issue-state-labels)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HWNfGWoWnhrub4EJa1M1Y
2026-07-16 19:48:40 +00:00
|
|
|
Dir.chdir(subdir) { `#{env} ruby #{BIN} config-write forgejo:jared/child 2>/dev/null` }
|
2026-07-13 16:27:43 +00:00
|
|
|
|
|
|
|
|
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"]
|
2026-07-13 14:48:19 +00:00
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|
|
|
|
|
end
|