workflow

git clone https://git.godosa.eu/workflow

master

raw ยท 7293 bytes

import os
import sys
import tempfile
import unittest
from pathlib import Path

sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from wflib import config as C
from test_claims import FOUR, git
from test_cli import TOML, Cli

SETUP = TOML + 'worktree_setup = ["mkdir -p out && ln -sfn \\"$WF_MAIN/out/data\\" out/data", "echo ran >> log.txt"]\n'


class SetupConfigTest(unittest.TestCase):
    def setUp(self):
        self.tmp = tempfile.TemporaryDirectory()
        self.root = Path(self.tmp.name).resolve()

    def tearDown(self):
        self.tmp.cleanup()

    def load(self, extra):
        (self.root / "workflow.toml").write_text('format = 1\ntasks = "T.md"\narchive = "a.md"\n' + extra)
        return C.load(self.root)

    def test_default_empty(self):
        self.assertEqual(self.load("").worktree_setup, [])

    def test_list_of_commands(self):
        self.assertEqual(self.load('worktree_setup = ["a", "b c"]\n').worktree_setup, ["a", "b c"])

    def test_wrong_type(self):
        with self.assertRaisesRegex(C.ConfigError, "'worktree_setup' must be a list of strings"):
            self.load('worktree_setup = "a"\n')

    def test_unknown_key_still_errors(self):
        with self.assertRaisesRegex(C.ConfigError, "unknown key 'worktree_setp'"):
            self.load('worktree_setp = []\n')


class GitCli(Cli):
    tasks_text = FOUR
    toml = SETUP

    def setUp(self):
        super().setUp()
        git(self.root, "init", "-q", "-b", "master")
        (self.root / ".gitignore").write_text(".worktrees/\n.wf/\nout/\n")
        git(self.root, "add", "-A")
        git(self.root, "commit", "-qm", "init")


class WorktreeCli(GitCli):
    def setUp(self):
        super().setUp()
        self.wt = self.root / ".worktrees" / "opus"
        git(self.root, "worktree", "add", "-q", str(self.wt), "-b", "opus/t-three")
        (self.root / "out" / "data").mkdir(parents=True)
        (self.root / "out" / "data" / "x.bin").write_text("main data\n")

    def setup_(self, *args, cwd=None):
        return self.wf("setup", *args, project=False, cwd=cwd or self.wt)


class SetupCliTest(WorktreeCli):
    def test_runs_commands_in_worktree_with_wf_main(self):
        code, out, err = self.setup_()
        self.assertEqual((code, err), (0, ""), out)
        self.assertEqual((self.wt / "out" / "data" / "x.bin").read_text(), "main data\n")
        self.assertEqual((self.wt / "log.txt").read_text(), "ran\n")
        self.assertFalse((self.root / "log.txt").exists())
        self.assertIn("$ echo ran >> log.txt\n", out)
        self.assertTrue(out.endswith("worktree_setup: 2 commands ok in .worktrees/opus\n"), out)

    def test_idempotent_rerun(self):
        self.assertEqual(self.setup_()[0], 0)
        self.assertEqual(self.setup_()[0], 0)
        self.assertEqual((self.wt / "out" / "data" / "x.bin").read_text(), "main data\n")

    def test_from_subfolder_runs_at_worktree_project_root(self):
        code, out, err = self.setup_(cwd=self.wt / "docs")
        self.assertEqual((code, err), (0, ""), out)
        self.assertTrue((self.wt / "log.txt").is_file())

    def test_nonzero_exit_reported_and_rest_skipped(self):
        (self.wt / "workflow.toml").write_text(TOML + 'worktree_setup = ["echo a > a.txt", "exit 3", "echo c > c.txt"]\n')
        code, out, err = self.setup_()
        self.assertEqual(code, 1, out + err)
        self.assertEqual(err, "wf: worktree_setup 'exit 3' failed (exit 3): later commands skipped\n")
        self.assertTrue((self.wt / "a.txt").is_file())
        self.assertFalse((self.wt / "c.txt").exists())

    def test_outside_worktree_fails(self):
        code, out, err = self.wf("setup", project=False, cwd=self.root)
        self.assertEqual((code, err), (1, "wf: setup runs inside a linked git worktree (lane worktree)\n"))

    def test_none_configured(self):
        (self.wt / "workflow.toml").write_text(TOML)
        code, out, err = self.setup_()
        self.assertEqual((code, out, err), (0, "no worktree_setup in workflow.toml: nothing to do\n", ""))


class WorktreeConfigTest(WorktreeCli):
    """A lane worktree's own workflow.toml / area file: setup, gate, areas read it; --mark writes it."""

    def edit_wt_toml(self, extra):
        (self.wt / "workflow.toml").write_text(TOML + extra)

    def test_setup_and_gate_read_worktree_toml(self):
        self.edit_wt_toml('worktree_setup = ["echo branch > b.txt"]\nquick_gate = ["echo g > g.txt"]\n')
        code, out, err = self.setup_()
        self.assertEqual((code, err), (0, ""), out)
        self.assertEqual((self.wt / "b.txt").read_text(), "branch\n")
        self.assertFalse((self.wt / "log.txt").exists())
        code, out, err = self.wf("gate", project=False, cwd=self.wt)
        self.assertEqual((code, err), (0, ""), out)
        self.assertTrue((self.wt / "g.txt").is_file())

    def test_books_stay_main_tree(self):
        self.edit_wt_toml("")
        (self.wt / "TASKS.md").write_text("# other\n")
        out = self.ok("show", "t-three", project=False, cwd=self.wt)
        self.assertIn("t-three", out)
        cfg = C.load_at(self.wt)
        self.assertEqual((cfg.root, cfg.local, cfg.tasks), (self.root, self.wt, self.root / "TASKS.md"))
        self.assertEqual(cfg.areas_file, self.wt / "CLAUDE.md")

    def test_areas_read_and_mark_worktree_copy(self):
        notes = "## Areas\n### Core\n- Code map: `nothing_here`\n"
        (self.root / "CLAUDE.md").write_text(notes.replace("Core", "Main"))
        (self.wt / "CLAUDE.md").write_text(notes)
        self.assertIn("Core: ", self.ok("areas", project=False, cwd=self.wt))
        self.ok("areas", "--mark", "Core", project=False, cwd=self.wt)
        self.assertIn("- Checked: ", (self.wt / "CLAUDE.md").read_text())
        self.assertEqual((self.root / "CLAUDE.md").read_text(), notes.replace("Core", "Main"))
        self.assertIn("Main: ", self.ok("areas", project=False, cwd=self.root))

    def test_main_tree_ignores_worktree(self):
        self.edit_wt_toml('quick_gate = ["exit 9"]\n')
        self.assertIsNone(C.find_local(self.root))
        code, out, err = self.wf("gate", project=False, cwd=self.root)
        self.assertEqual((code, out), (0, "no quick_gate in workflow.toml: nothing to do\n"))


class SetupHintTest(GitCli):
    def env(self, name, pid):
        sock = self.root / f"{name}.sock"
        sock.write_text("")
        return {"CLAUDE_CODE_MESSAGING_SOCKET": str(sock), "CLAUDE_PID": str(pid), "CLAUDE_CODE_SESSION_ID": name}

    def test_multi_session_hint_adds_setup(self):
        son, me = self.env("s", os.getppid()), self.env("o", os.getpid())
        self.ok("next", "--lane", "slow", "--as", "sonnet", "--brief", env=son)
        out = self.ok("next", "--lane", "fast", "--as", "opus", env=me)
        self.assertIn("  git worktree add .worktrees/fast -b fast/<task> master && cd .worktrees/fast && wf setup"
                      "  (wf there writes this TASKS.md)\n", out)
        (self.root / ".worktrees" / "fast").mkdir(parents=True)
        out = self.ok("next", "--lane", "fast", "--as", "opus", env=me)
        self.assertIn("  cd .worktrees/fast && git switch -c fast/<task> master && wf setup"
                      "  (wf there writes this TASKS.md)\n", out)

if __name__ == "__main__":
    unittest.main()