Add rake import:status
Assisted-by: GLM-5.3 via pi
change tkmyozmlqpqvotuksxpvpukowskyztuo
commit 8ffd9985edcbc3033f1919587e163d9bcd3dbb62
author Alpha Chen <alpha@kejadlen.dev>
date
parent olkyomor
diff --git a/tasks/import.rake b/tasks/import.rake
index f094b66..0888845 100644
--- a/tasks/import.rake
+++ b/tasks/import.rake
@@ -8,6 +8,12 @@ require "pathname"
 module ImportTasks
   DIR = Pathname.new("data/imports")
 
+  # What each carrying task waits on — a contact still to carry through
+  # that step — named so a status read can say what those tasks would
+  # pick next without restating their choice.
+  TO_LAND = ->(plan) { plan.with_status(nil).any? || plan.with_status("landed").any? }
+  TO_LEAVE = ->(plan) { plan.with_status("imported").any? }
+
   # The plan PLAN names, or the oldest one still waiting for this step:
   # plans are carried in the order they were built, so the next one to
   # take further is the oldest that has not been. A PLAN that is not a
@@ -55,7 +61,7 @@ namespace :import do
       require "pro_tacts/import/macos"
       require "pro_tacts/import/remove"
 
-      plan = ImportTasks.plan(->(it) { it.with_status("imported").any? }, "to leave this Mac")
+      plan = ImportTasks.plan(ImportTasks::TO_LEAVE, "to leave this Mac")
       host = plan.host or abort("#{plan.dir} has not landed on a host")
       uri = URI.parse(host)
       puts "removing the contacts #{plan.dir} landed on #{host}"
@@ -71,6 +77,38 @@ namespace :import do
     end
   end
 
+  desc "Summarize the imports in data/imports and what execute and remove would carry next"
+  task :status do
+    require "pro_tacts/import/plan"
+
+    plans = ProTacts::Import::Plan.all(ImportTasks::DIR)
+    if plans.empty?
+      puts "no plans in #{ImportTasks::DIR} yet; run rake import:macos:plan"
+      next
+    end
+
+    # The statuses in their order through an import, so the line reads
+    # as where the plan has got to rather than an alphabetical jumble.
+    plans.each do
+      host = it.host ? "on #{it.host}" : "not yet on a host"
+      puts "#{it.dir.basename}  #{it.source}  #{host}"
+      counts = it.contacts.group_by(&:status).transform_values(&:length)
+      summary = [
+        ["to land", counts.fetch(nil, 0)],
+        ["landed", counts.fetch("landed", 0)],
+        ["imported", counts.fetch("imported", 0)],
+        ["removed", counts.fetch("removed", 0)],
+      ].reject { |_, count| count.zero? }.map { |label, count| "#{count} #{label}" }.join(", ")
+      word = it.contacts.size == 1 ? "contact" : "contacts"
+      puts summary.empty? ? "  #{it.contacts.size} #{word}" : "  #{it.contacts.size} #{word}: #{summary}"
+    end
+
+    landing = plans.find(&ImportTasks::TO_LAND)
+    leaving = plans.find(&ImportTasks::TO_LEAVE)
+    puts "next: import:execute would land #{landing.dir.basename}" if landing
+    puts "next: import:macos:remove would clear #{leaving.dir.basename}" if leaving
+  end
+
   desc "Land the oldest plan in data/imports still to land, or the one in PLAN, on the pro-tacts at HOST, a base URL such as https://contacts"
   task :execute do
     require "net/http"
@@ -79,7 +117,7 @@ namespace :import do
     require "pro_tacts/import/http_client"
     require "pro_tacts/import/plan"
 
-    plan = ImportTasks.plan(->(it) { it.with_status(nil).any? || it.with_status("landed").any? }, "to land")
+    plan = ImportTasks.plan(ImportTasks::TO_LAND, "to land")
     # A bare hostname is the base URL of a server that serves HTTPS, which
     # every deployment does; the scheme is spelled out here so the host
     # the plan records is the one a second run is compared against.
diff --git a/test/tasks/test_import.rb b/test/tasks/test_import.rb
new file mode 100644
index 0000000..2f29d52
--- /dev/null
+++ b/test/tasks/test_import.rb
@@ -0,0 +1,61 @@
+require_relative "../test_helper"
+
+require "pathname"
+require "rake"
+require "tmpdir"
+
+require "pro_tacts/import/plan"
+
+class ImportStatusTaskTest < Minitest::Test
+  Plan = ProTacts::Import::Plan
+
+  SOURCE = {"identifier" => "A:ABPerson", "vcard" => "BEGIN:VCARD\r\nEND:VCARD\r\n", "note" => nil, "contact" => {}}
+  ADA = ProTacts::Import::Card.new(first: "Ada", last: "Lovelace", nickname: nil, birthday: nil, phones: [], emails: [], addresses: [], note: nil, photo: false, groups: [], source: SOURCE)
+
+  def entry(id) = Plan::Entry.new(id:, source_id: "#{id}:ABPerson", card: ADA)
+
+  def test_status_counts_each_plans_contacts_and_names_what_comes_next
+    Dir.mktmpdir do |root|
+      root = Pathname.new(root)
+      imports = root / "data" / "imports"
+      # A plan mid-flight — one contact through to the Mac-cleanup step,
+      # one past it — and one freshly built with everything still to do.
+      underway = Plan.write(imports / "macos-20260901T000000Z", source: "macos", created_at: Time.utc(2026, 9, 1), entries: [entry("kmnuqmzxylru"), entry("vmnlryyvktux")])
+      underway.host = "https://contacts.example"
+      underway.record("kmnuqmzxylru", "imported")
+      underway.record("vmnlryyvktux", "removed")
+      Plan.write(imports / "macos-20260916T180412Z", source: "macos", created_at: Time.utc(2026, 9, 16, 18, 4, 12), entries: [entry("aaaaaaaaaaaa")])
+
+      out, = run_task(root)
+
+      assert_includes out, "macos-20260901T000000Z  macos  on https://contacts.example"
+      assert_includes out, "  2 contacts: 1 imported, 1 removed"
+      assert_includes out, "macos-20260916T180412Z  macos  not yet on a host"
+      assert_includes out, "  1 contact: 1 to land"
+      assert_includes out, "next: import:execute would land macos-20260916T180412Z"
+      assert_includes out, "next: import:macos:remove would clear macos-20260901T000000Z"
+    end
+  end
+
+  def test_status_with_no_plans_says_how_to_build_one
+    Dir.mktmpdir do |root|
+      out, = run_task(Pathname.new(root))
+
+      assert_includes out, "no plans in data/imports yet"
+    end
+  end
+
+  private
+
+  # Runs import:status in a Rake application of its own, from root as
+  # the working directory: the task reads data/imports relative to
+  # where rake runs, not through ProTacts.config.
+  def run_task(root)
+    application = Rake.application
+    Rake.application = Rake::Application.new
+    load (Pathname.new(__dir__).parent.parent / "tasks" / "import.rake").to_s
+    Dir.chdir(root) { capture_io { Rake.application["import:status"].invoke } }
+  ensure
+    Rake.application = application
+  end
+end