Build the OsGit core objects test-first against Fake and Cli backends #1588

Closed
opened 2026-11-06 16:51:25 +00:00 by jared · 2 comments
Owner

Problem

The call sites in docs/os-git/call-site-checklist.md need one set of domain objects behind one interface. Today four wrappers answer the same questions with different results (four ways to name the current branch, raise versus soft-fail decided per wrapper, two commit models).

Design: docs/os-git/use-cases-and-domain.md (design) and docs/os-git/call-site-checklist.md (call sites). Settled decisions are in design section 1 (docs/os-git/use-cases-and-domain.md): the gem lives in plugins/os-git with a path: source; git-context and os:worktree merge into os-git; old wrappers go only after all references are updated; design order is use-cases, objects, interfaces; os-context and os-vault become Ruby; ticket tracking stays out of os-git.

Reproduce

n/a: new objects. The disagreements are listed in design section 2.6.

Approach

Write one characterization test per use-case through the public messages (UC-01 to UC-14, UC-22, UC-23, UC-25, UC-27; the worktree use-cases UC-15 to UC-21 belong to the worktree ticket #1589). Write the test first. Run each test against Backend::Fake for decisions and against Backend::Cli on a ScratchRepo for behavior. Run a shared contract test module against every adapter that exists.

Objects and messages come from design sections 3 and 4.3: Repository, Branch, ChangeSet, Span, Commit::Plan, Commit, History, Remote. The error rule is design section 6: raise on git failure, return nil, [], or false when git answers "nothing there". No rescue that returns "" inside the library. Add OsGit::RepoRoot, a process-free root finder (walk up for .git, follow the .git file of a linked worktree), for hooks that run on every prompt or edit (design Q3). Rule Q3 with the spike result (#1586).

Required work:

  1. Repository with open, find, init, clone, root, primary_root, linked?, at_top_level?, branch, branch_named, primary_branch, changes, changes_between, files, tracked?, ignored?, read_at, diff, resolve, exists?, merge_base, ancestor?, history, remote, remotes, add_remote, config, commit, move, remove, tag, tag? — check: present primary_branch in plugins/os-git/lib/os_git/repository.rb
  2. Branch with name, detached?, exists?, worktree, merged_into?, and a safe delete that never forces — check: present NotMerged in plugins/os-git/lib/os_git/branch.rb
  3. ChangeSet as a value with no I/O — check: present all_untracked_within? in plugins/os-git/lib/os_git/change_set.rb
  4. Span with between, since_fork, since — check: present since_fork in plugins/os-git/lib/os_git/span.rb
  5. Commit::Plan that refuses empty paths and a blank message, and Commit — check: present ArgumentError in plugins/os-git/lib/os_git/commit/plan.rb
  6. History with last_commit_touching, touching, recent, matching, find, branches_containing — check: present last_commit_touching in plugins/os-git/lib/os_git/history.rb
  7. Remote with fetch, pull_fast_forward, push; timeout: is a required argument — check: present pull_fast_forward in plugins/os-git/lib/os_git/remote.rb
  8. RepoRoot process-free finder — check: present RepoRoot in plugins/os-git/lib/os_git/repo_root.rb
  9. Characterization tests and the shared backend contract test.

Acceptance criteria

  1. Given a detached HEAD, when Repository#branch runs, then Branch#detached? is true and Branch#name is nil; the same answer holds on every adapter.
  2. Given a Commit::Plan with no paths or a blank message, when it is built, then it raises ArgumentError.
  3. Given Repository, when its public messages are listed, then none stages everything, none runs checkout, restore, or stash, and none runs commit -a.
  4. Given staged changes outside the plan paths, when Repository#commit(plan) runs, then the commit holds only the plan paths.
  5. Given a path git does not know, when Repository#tracked? and #ignored? run, then they return false; given git fails, then they raise OsGit::Error.
  6. Given a revision that does not exist, when Repository#resolve runs, then it returns nil; given Repository#merge_base on an unknown ref, then it raises.
  7. Given a branch not merged into the base, when Branch#delete runs, then it raises NotMerged and the branch remains.
  8. Given a path with no history, when History#last_commit_touching runs, then it returns nil.
  9. Given Remote#fetch, #pull_fast_forward, or #push called without timeout:, then it raises ArgumentError.
  10. Given a linked worktree, when RepoRoot.find runs from inside it, then it returns the worktree root and primary_root returns the main checkout, and no git process starts.
  11. Given the use-case table, when the tests are listed, then each use-case in scope has one test through the public messages.
  12. Given Backend::Cli and Backend::Fake, when the shared contract test runs, then both pass.

Owner sketch

class OsGit::Repository
  def self.find(path); end
  def changes(against: nil, glob: nil); end
  def commit(plan); end
end
class OsGit::ChangeSet; end
class OsGit::Commit::Plan; def initialize(paths:, message:); end; end

Non-goals

Worktree, Integration, Sync (own tickets). Any caller migration.

Blocking edges

Blocked by the scaffold ticket (#1587).

Origin

  • Trigger: User request in session 1a5b2630-09c9-46cb-aee4-a5a98e2e7d24 on 2026-11-06: one os-git gem for all local git work in cc-os and the user's projects.
  • Improvised this session: none
  • Chain: callers disagree on branch, raise/soft-fail, commit model ← each wrapper designed on the fly ← DESIGN (docs/os-git/use-cases-and-domain.md section 2.6)
  • Root candidate: none found; searched open issues for git, worktree, octokit, ruby port, python, statusline, reset --hard. Related but not a match: #845, #853, #857, #1527, #1554.
  • Where: plugins/os-git/lib/os_git/
  • Session: 1a5b2630-09c9-46cb-aee4-a5a98e2e7d24
  • Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/1a5b2630-09c9-46cb-aee4-a5a98e2e7d24.jsonl
## Problem The call sites in `docs/os-git/call-site-checklist.md` need one set of domain objects behind one interface. Today four wrappers answer the same questions with different results (four ways to name the current branch, raise versus soft-fail decided per wrapper, two commit models). Design: `docs/os-git/use-cases-and-domain.md` (design) and `docs/os-git/call-site-checklist.md` (call sites). Settled decisions are in design section 1 (`docs/os-git/use-cases-and-domain.md`): the gem lives in `plugins/os-git` with a `path:` source; `git-context` and `os:worktree` merge into `os-git`; old wrappers go only after all references are updated; design order is use-cases, objects, interfaces; `os-context` and `os-vault` become Ruby; ticket tracking stays out of `os-git`. ## Reproduce n/a: new objects. The disagreements are listed in design section 2.6. ## Approach Write one characterization test per use-case through the public messages (UC-01 to UC-14, UC-22, UC-23, UC-25, UC-27; the worktree use-cases UC-15 to UC-21 belong to the worktree ticket #1589). Write the test first. Run each test against `Backend::Fake` for decisions and against `Backend::Cli` on a `ScratchRepo` for behavior. Run a shared contract test module against every adapter that exists. Objects and messages come from design sections 3 and 4.3: `Repository`, `Branch`, `ChangeSet`, `Span`, `Commit::Plan`, `Commit`, `History`, `Remote`. The error rule is design section 6: raise on git failure, return `nil`, `[]`, or `false` when git answers "nothing there". No `rescue` that returns `""` inside the library. Add `OsGit::RepoRoot`, a process-free root finder (walk up for `.git`, follow the `.git` file of a linked worktree), for hooks that run on every prompt or edit (design Q3). Rule Q3 with the spike result (#1586). Required work: 1. `Repository` with open, find, init, clone, root, primary_root, linked?, at_top_level?, branch, branch_named, primary_branch, changes, changes_between, files, tracked?, ignored?, read_at, diff, resolve, exists?, merge_base, ancestor?, history, remote, remotes, add_remote, config, commit, move, remove, tag, tag? — check: present `primary_branch` in `plugins/os-git/lib/os_git/repository.rb` 2. `Branch` with name, detached?, exists?, worktree, merged_into?, and a safe `delete` that never forces — check: present `NotMerged` in `plugins/os-git/lib/os_git/branch.rb` 3. `ChangeSet` as a value with no I/O — check: present `all_untracked_within?` in `plugins/os-git/lib/os_git/change_set.rb` 4. `Span` with `between`, `since_fork`, `since` — check: present `since_fork` in `plugins/os-git/lib/os_git/span.rb` 5. `Commit::Plan` that refuses empty paths and a blank message, and `Commit` — check: present `ArgumentError` in `plugins/os-git/lib/os_git/commit/plan.rb` 6. `History` with last_commit_touching, touching, recent, matching, find, branches_containing — check: present `last_commit_touching` in `plugins/os-git/lib/os_git/history.rb` 7. `Remote` with fetch, pull_fast_forward, push; `timeout:` is a required argument — check: present `pull_fast_forward` in `plugins/os-git/lib/os_git/remote.rb` 8. `RepoRoot` process-free finder — check: present `RepoRoot` in `plugins/os-git/lib/os_git/repo_root.rb` 9. Characterization tests and the shared backend contract test. ## Acceptance criteria 1. Given a detached HEAD, when `Repository#branch` runs, then `Branch#detached?` is true and `Branch#name` is `nil`; the same answer holds on every adapter. 2. Given a `Commit::Plan` with no paths or a blank message, when it is built, then it raises `ArgumentError`. 3. Given `Repository`, when its public messages are listed, then none stages everything, none runs `checkout`, `restore`, or `stash`, and none runs `commit -a`. 4. Given staged changes outside the plan paths, when `Repository#commit(plan)` runs, then the commit holds only the plan paths. 5. Given a path git does not know, when `Repository#tracked?` and `#ignored?` run, then they return `false`; given git fails, then they raise `OsGit::Error`. 6. Given a revision that does not exist, when `Repository#resolve` runs, then it returns `nil`; given `Repository#merge_base` on an unknown ref, then it raises. 7. Given a branch not merged into the base, when `Branch#delete` runs, then it raises `NotMerged` and the branch remains. 8. Given a path with no history, when `History#last_commit_touching` runs, then it returns `nil`. 9. Given `Remote#fetch`, `#pull_fast_forward`, or `#push` called without `timeout:`, then it raises `ArgumentError`. 10. Given a linked worktree, when `RepoRoot.find` runs from inside it, then it returns the worktree root and `primary_root` returns the main checkout, and no `git` process starts. 11. Given the use-case table, when the tests are listed, then each use-case in scope has one test through the public messages. 12. Given `Backend::Cli` and `Backend::Fake`, when the shared contract test runs, then both pass. ## Owner sketch ```ruby class OsGit::Repository def self.find(path); end def changes(against: nil, glob: nil); end def commit(plan); end end class OsGit::ChangeSet; end class OsGit::Commit::Plan; def initialize(paths:, message:); end; end ``` ## Non-goals Worktree, Integration, Sync (own tickets). Any caller migration. ## Blocking edges Blocked by the scaffold ticket (#1587). ## Origin - Trigger: User request in session 1a5b2630-09c9-46cb-aee4-a5a98e2e7d24 on 2026-11-06: one os-git gem for all local git work in cc-os and the user's projects. - Improvised this session: none - Chain: callers disagree on branch, raise/soft-fail, commit model ← each wrapper designed on the fly ← DESIGN (docs/os-git/use-cases-and-domain.md section 2.6) - Root candidate: none found; searched open issues for git, worktree, octokit, ruby port, python, statusline, reset --hard. Related but not a match: #845, #853, #857, #1527, #1554. - Where: plugins/os-git/lib/os_git/ - Session: 1a5b2630-09c9-46cb-aee4-a5a98e2e7d24 - Transcript: /home/jared/.claude/projects/-home-jared-dev-cc-os/1a5b2630-09c9-46cb-aee4-a5a98e2e7d24.jsonl
Author
Owner

Work started by the #1610 orchestrator. Branch: ticket-1588.

Work started by the #1610 orchestrator. Branch: ticket-1588.
Author
Owner

Resolution

Done: Repository, Branch, ChangeSet, Span, Commit, Commit::Plan, History, Remote, RepoRoot built behind the Backend seam; all ticket acceptance criteria met. Hand-finished after escalation bound_exhausted.

Evidence: 592c8851. 165 runs, 461 assertions, 0 failures; rubocop clean. Behavior verifier passed 10 of 12, literal-pathspec fix closed the other two. Design audit found 10 blockers and 4 minors, all fixed test-first. Implementation 243 abandoned as hand-finished.

Follow-ups: Three items posted on #1592, whose migration needs them: commit_changes on a merge commit, log(rev:) with a range, original bytes for non-UTF-8 paths.

## Resolution **Done:** Repository, Branch, ChangeSet, Span, Commit, Commit::Plan, History, Remote, RepoRoot built behind the Backend seam; all ticket acceptance criteria met. Hand-finished after escalation bound_exhausted. **Evidence:** 592c8851. 165 runs, 461 assertions, 0 failures; rubocop clean. Behavior verifier passed 10 of 12, literal-pathspec fix closed the other two. Design audit found 10 blockers and 4 minors, all fixed test-first. Implementation 243 abandoned as hand-finished. **Follow-ups:** Three items posted on #1592, whose migration needs them: commit_changes on a merge commit, log(rev:) with a range, original bytes for non-UTF-8 paths.
jared closed this issue 2026-11-06 18:54:59 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jared/cc-os#1588
No description provided.