Introduce a data directory in place of the contacts directory
PRO_TACTS_DATA_DIR (default data) now roots everything under it, with
contacts at data/contacts, so the coming database has a home. Paths
compose with Pathname throughout.

Assisted-by: GLM-5.3 via pi
change yzxllzwumuqnspvtpvynorlpkwpkrkzz
commit 30ba7a247dfdc5e37d765b388f60caa787d2acbd
author Alpha Chen <alpha@kejadlen.dev>
date
parent zvrpvvps
diff --git a/Rakefile b/Rakefile
index 8a4365a..9e6f52f 100644
--- a/Rakefile
+++ b/Rakefile
@@ -2,7 +2,6 @@
 require "pathname"
 
 $LOAD_PATH.unshift(Pathname.new(__dir__) / "lib")
-
 require "minitest/test_task"
 
 Minitest::TestTask.create
@@ -21,7 +20,7 @@ task :fixtures do
   ENV["RACK_ENV"] = "test"
   # Mirrors test/test_helper.rb, which cannot be required here without
   # minitest/autorun running its at_exit hook inside rake.
-  ENV["PRO_TACTS_CONTACTS_DIR"] = File.expand_path("test/fixtures/contacts", __dir__)
+  ENV["PRO_TACTS_DATA_DIR"] = (Pathname.new(__dir__) / "test/fixtures").to_s
   require "pro_tacts/web"
   require_relative "test/pro_tacts/exchange_fixtures"
   ExchangeFixtures.record_responses(ProTacts::Web)
diff --git a/lib/pro_tacts/config.rb b/lib/pro_tacts/config.rb
index 3d9804b..667a78d 100644
--- a/lib/pro_tacts/config.rb
+++ b/lib/pro_tacts/config.rb
@@ -1,4 +1,6 @@
 
+require "pathname"
+
 module ProTacts
   # Single source of truth for configuration read from the environment.
   # Nothing else in the app should read ENV directly; add a method here and
@@ -32,10 +34,17 @@ module ProTacts
       !value.nil? && value.match?(TRUTHY)
     end
 
-    # Directory of contact KDL files, one contact per file; the filename
-    # is the contact ID. See docs/plans/2026-01-12-carddav-architecture.md.
+    # Root data directory: holds the contacts directory and, later, the
+    # database. Overridable with PRO_TACTS_DATA_DIR.
+    def data_dir
+      Pathname.new(@env.fetch("PRO_TACTS_DATA_DIR", "data"))
+    end
+
+    # Contacts live at data/contacts, one KDL file per contact; the
+    # filename is the contact ID. See
+    # docs/plans/2026-01-12-carddav-architecture.md.
     def contacts_dir
-      @env.fetch("PRO_TACTS_CONTACTS_DIR", "data/contacts")
+      data_dir / "contacts"
     end
 
     # Where the debug logger writes. A path, overridable with
diff --git a/lib/pro_tacts/contact.rb b/lib/pro_tacts/contact.rb
index 1b6cfe7..0c65823 100644
--- a/lib/pro_tacts/contact.rb
+++ b/lib/pro_tacts/contact.rb
@@ -18,7 +18,7 @@ module ProTacts
     # Every contact in the directory, one file per contact. An empty
     # directory is a valid empty address book, but any non-hidden
     # non-.kdl file raises — a misplaced file or a wrong
-    # PRO_TACTS_CONTACTS_DIR should not quietly serve a partial address
+    # PRO_TACTS_DATA_DIR should not quietly serve a partial address
     # book. Files that fail to parse or render are reported and skipped;
     # Sentry is a no-op while uninitialized, so tests need no DSN.
     def self.all(directory = ProTacts.config.contacts_dir)
diff --git a/test/pro_tacts/test_config.rb b/test/pro_tacts/test_config.rb
index aaa0d52..bcfb1cc 100644
--- a/test/pro_tacts/test_config.rb
+++ b/test/pro_tacts/test_config.rb
@@ -25,12 +25,17 @@ class ConfigTest < Minitest::Test
     assert_raises(KeyError) { ProTacts::Config.new({}).sentry_dsn }
   end
 
-  def test_contacts_dir_defaults_to_data_contacts
-    assert_equal "data/contacts", ProTacts::Config.new({}).contacts_dir
+  def test_data_dir_defaults_to_data
+    assert_equal Pathname.new("data"), ProTacts::Config.new({}).data_dir
   end
 
-  def test_contacts_dir_is_overridable
-    assert_equal "/tmp/kdl", ProTacts::Config.new("PRO_TACTS_CONTACTS_DIR" => "/tmp/kdl").contacts_dir
+  def test_data_dir_is_overridable
+    assert_equal Pathname.new("/tmp/state"), ProTacts::Config.new("PRO_TACTS_DATA_DIR" => "/tmp/state").data_dir
+  end
+
+  def test_contacts_dir_lives_under_the_data_dir
+    assert_equal Pathname.new("data/contacts"), ProTacts::Config.new({}).contacts_dir
+    assert_equal Pathname.new("/tmp/state/contacts"), ProTacts::Config.new("PRO_TACTS_DATA_DIR" => "/tmp/state").contacts_dir
   end
 
   def test_debug_defaults_off
diff --git a/test/pro_tacts/test_contact.rb b/test/pro_tacts/test_contact.rb
index b30b72e..f4af361 100644
--- a/test/pro_tacts/test_contact.rb
+++ b/test/pro_tacts/test_contact.rb
@@ -1,5 +1,6 @@
 require_relative "../test_helper"
 
+require "pathname"
 require "tmpdir"
 
 require "pro_tacts/contact"
@@ -7,8 +8,9 @@ require "pro_tacts/contact"
 class ContactTest < Minitest::Test
   def with_contacts(files)
     Dir.mktmpdir do |dir|
-      files.each { |name, content| File.write(File.join(dir, name), content) }
-      yield ProTacts::Contact.all(dir)
+      directory = Pathname.new(dir)
+      files.each { |name, content| File.write(directory / name, content) }
+      yield ProTacts::Contact.all(directory)
     end
   end
 
@@ -46,7 +48,7 @@ class ContactTest < Minitest::Test
 
   def test_non_kdl_files_raise
     Dir.mktmpdir do |dir|
-      File.write(File.join(dir, "notes.txt"), "hello")
+      File.write(Pathname.new(dir) / "notes.txt", "hello")
 
       error = assert_raises(ArgumentError) { ProTacts::Contact.all(dir) }
 
diff --git a/test/pro_tacts/test_web.rb b/test/pro_tacts/test_web.rb
index ec4126a..7fc855c 100644
--- a/test/pro_tacts/test_web.rb
+++ b/test/pro_tacts/test_web.rb
@@ -1,5 +1,6 @@
 
 require_relative "../test_helper"
+require "pathname"
 require "rack/test"
 require "tmpdir"
 
@@ -106,15 +107,17 @@ class WebTest < Minitest::Test
     assert_equal "Not Found", last_response.body
   end
 
-  # Swaps in a throwaway contacts directory so the multi-contact routes
-  # can be exercised without touching the exchange fixture data.
+  # Swaps in a throwaway data directory so the multi-contact routes can
+  # be exercised without touching the exchange fixture data.
   def with_contacts(files)
     Dir.mktmpdir do |dir|
-      files.each { |name, content| File.write(File.join(dir, name), content) }
+      contacts_dir = Pathname.new(dir) / "contacts"
+      Dir.mkdir(contacts_dir)
+      files.each { |name, content| File.write(contacts_dir / name, content) }
       original = ProTacts.config
       ProTacts.config = ProTacts::Config.new({
         "RACK_ENV" => "test",
-        "PRO_TACTS_CONTACTS_DIR" => dir,
+        "PRO_TACTS_DATA_DIR" => dir,
       })
       begin
         yield
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 3606673..6d59f00 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -1,9 +1,13 @@
 
 # Marks the process as running tests before anything requires the app, so
-# web.rb skips Sentry.init and no SENTRY_DSN is needed. The contacts
-# directory holds the card the recorded macOS exchange asked for, so the
-# fixture replay resolves the same hrefs the client did.
+# web.rb skips Sentry.init and no SENTRY_DSN is needed. The fixtures
+# directory serves as the data directory: its contacts/ holds the card the
+# recorded macOS exchange asked for, so the replay resolves the same hrefs
+# the client did.
 ENV["RACK_ENV"] = "test"
-ENV["PRO_TACTS_CONTACTS_DIR"] = File.expand_path("fixtures/contacts", __dir__)
+
+require "pathname"
+
+ENV["PRO_TACTS_DATA_DIR"] = (Pathname.new(__dir__) / "fixtures").to_s
 
 require "minitest/autorun"