os-backlog: align .cc-os/config with os-status key=value contract (refs #14)

Config parsed/wrote YAML while os-status owns the file as key=value
lines, so config-write output was invisible to the tracker-configured
check. Now reads key=value (legacy 'key: value' lines still accepted)
and merge emits key=value.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUyiRB4vHaRkhYKUdoMW2P
This commit is contained in:
jared 2026-07-12 16:04:12 -04:00
parent 696aefcf23
commit 3b9c4fbd88
2 changed files with 40 additions and 22 deletions

View File

@ -1,15 +1,26 @@
require "yaml"
module Backlog
# The parsed contents of a repo's `.cc-os/config` (YAML). Only the
# `board` key matters to the resolver: an explicit board-name override.
# The parsed contents of a repo's `.cc-os/config`. The file format is
# '#'-commented key=value lines — the contract is owned by os-status
# (plugins/os-status/hooks/state.py); legacy `key: value` YAML lines
# from earlier os-backlog versions are still read for compatibility.
# Never touches the filesystem itself — callers read the file and pass
# its contents (or nil, if absent) in.
class Config
def initialize(contents)
@data = contents.to_s.strip.empty? ? {} : (YAML.safe_load(contents) || {})
rescue Psych::SyntaxError
@data = {}
contents.to_s.each_line do |line|
line = line.strip
next if line.empty? || line.start_with?("#") || line == "---"
if line.include?("=")
key, value = line.split("=", 2)
elsif line.include?(": ")
key, value = line.split(": ", 2)
else
next
end
@data[key.strip] = value.strip
end
end
# @return [String, nil] the explicit board name override, if configured
@ -28,18 +39,19 @@ module Backlog
# @return [Hash] a copy of the parsed config data
def to_h = @data.dup
# Return new YAML contents with `key` set to `value`, preserving every
# other key already present in `contents`. Pure — does not touch the
# Return new key=value contents with `key` set to `value`, preserving
# every other key already present in `contents` (legacy YAML-style
# lines are rewritten to key=value). Pure — does not touch the
# filesystem; callers read/write the file themselves.
#
# @param contents [String, nil] existing raw `.cc-os/config` contents
# @param key [String, Symbol] key to set
# @param value [Object] value to set it to
# @return [String] the updated YAML contents
# @return [String] the updated key=value contents
def self.merge(contents, key, value)
data = new(contents).to_h
data[key.to_s] = value
YAML.dump(data)
data[key.to_s] = value.to_s
data.map { |k, v| "#{k}=#{v}\n" }.join
end
end
end

View File

@ -13,39 +13,45 @@ class ConfigTest < Minitest::Test
end
def test_reads_board_key
config = Backlog::Config.new("board: llf-schema\n")
config = Backlog::Config.new("board=llf-schema\n")
assert config.configured?
assert_equal "llf-schema", config.board
end
def test_malformed_yaml_is_treated_as_unconfigured
config = Backlog::Config.new("board: [unterminated\n")
def test_reads_legacy_yaml_style_board_key
config = Backlog::Config.new("---\nboard: llf-schema\n")
assert config.configured?
assert_equal "llf-schema", config.board
end
def test_lines_without_separator_are_ignored
config = Backlog::Config.new("# comment\njunk line\n")
refute config.configured?
end
def test_yaml_without_board_key_is_unconfigured
config = Backlog::Config.new("other: value\n")
def test_contents_without_board_key_is_unconfigured
config = Backlog::Config.new("other=value\n")
refute config.configured?
end
def test_reads_tracker_key
config = Backlog::Config.new("tracker: forgejo:jared/cc-os\n")
config = Backlog::Config.new("tracker=forgejo:jared/cc-os\n")
assert config.tracker_configured?
assert_equal "forgejo:jared/cc-os", config.tracker
end
def test_tracker_unconfigured_when_absent
config = Backlog::Config.new("board: llf-schema\n")
config = Backlog::Config.new("board=llf-schema\n")
refute config.tracker_configured?
end
def test_merge_sets_new_key_on_nil_contents
updated = Backlog::Config.new(Backlog::Config.merge(nil, "tracker", "forgejo:jared/cc-os"))
assert_equal "forgejo:jared/cc-os", updated.tracker
contents = Backlog::Config.merge(nil, "tracker", "forgejo:jared/cc-os")
assert_equal "tracker=forgejo:jared/cc-os\n", contents
end
def test_merge_preserves_other_keys
existing = "board: llf-schema\nother: kept\n"
existing = "board=llf-schema\nother=kept\n"
updated_contents = Backlog::Config.merge(existing, "tracker", "planka:llf-schema")
updated = Backlog::Config.new(updated_contents)
@ -55,7 +61,7 @@ class ConfigTest < Minitest::Test
end
def test_merge_overwrites_existing_value_for_key
existing = "tracker: repo:old\n"
existing = "tracker=repo:old\n"
updated = Backlog::Config.new(Backlog::Config.merge(existing, "tracker", "repo:new"))
assert_equal "repo:new", updated.tracker
end