aboutsummaryrefslogtreecommitdiff
path: root/hooks/tests/test_git_commit_confirm.py
blob: 83519adb1e19b2709d746400e211385dbd9d93c2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
"""Tests for hooks/git-commit-confirm.py — file-backed commit messages."""

from conftest import load_hook

hook = load_hook("git-commit-confirm.py")


# --- existing forms still parse (regression guard) -------------------------

def test_extract_dash_m_simple():
    msg = hook.extract_commit_message('git commit -m "fix: tidy parser"')
    assert msg == "fix: tidy parser"


def test_extract_heredoc():
    cmd = (
        "git commit -m \"$(cat <<'EOF'\n"
        "feat: add thing\n"
        "\n"
        "body line\n"
        "EOF\n"
        ")\""
    )
    msg = hook.extract_commit_message(cmd)
    assert msg.startswith("feat: add thing")
    assert "body line" in msg


def test_extract_unparseable_falls_through():
    # Bare `git commit` would drop into $EDITOR.
    assert hook.extract_commit_message("git commit") == hook.UNPARSEABLE_MESSAGE


# --- new: -F / --file / --file= forms read the file ------------------------

def test_extract_dash_F_reads_file(tmp_path):
    f = tmp_path / "msg.txt"
    f.write_text("fix: from a file\n\nsome body\n")
    assert hook.extract_commit_message(f"git commit -F {f}") == "fix: from a file\n\nsome body"


def test_extract_long_file_flag_reads_file(tmp_path):
    f = tmp_path / "msg.txt"
    f.write_text("docs: long form file flag\n")
    assert hook.extract_commit_message(f"git commit --file {f}") == "docs: long form file flag"


def test_extract_file_equals_form_reads_file(tmp_path):
    f = tmp_path / "msg.txt"
    f.write_text("chore: equals form\n")
    assert hook.extract_commit_message(f"git commit --file={f}") == "chore: equals form"


def test_extract_F_strips_quotes_around_path(tmp_path):
    f = tmp_path / "my msg.txt"
    f.write_text("feat: quoted path\n")
    assert hook.extract_commit_message(f'git commit -F "{f}"') == "feat: quoted path"


# --- the audit-item bug: attribution in a file-backed message is now caught -

def test_file_backed_attribution_is_caught(tmp_path):
    f = tmp_path / "msg.txt"
    f.write_text("feat: add widget\n\nCo-Authored-By: Claude <noreply@anthropic.com>\n")
    msg = hook.extract_commit_message(f"git commit -F {f}")
    issues = hook.collect_issues(msg, staged=["a.py"], author="Real Dev <dev@example.com>")
    assert any(i.startswith("AI-attribution") for i in issues)


def test_inline_message_without_attribution_is_clean(tmp_path):
    # Sanity: a clean file-backed message produces no attribution issue.
    f = tmp_path / "msg.txt"
    f.write_text("fix: handle empty input\n")
    msg = hook.extract_commit_message(f"git commit -F {f}")
    issues = hook.collect_issues(msg, staged=["a.py"], author="Real Dev <dev@example.com>")
    assert not any(i.startswith("AI-attribution") for i in issues)


# --- unreadable file falls through to UNPARSEABLE (fail-safe: ask) ---------

def test_missing_file_falls_through_to_unparseable(tmp_path):
    missing = tmp_path / "nope.txt"
    assert hook.extract_commit_message(f"git commit -F {missing}") == hook.UNPARSEABLE_MESSAGE


def test_oversized_file_falls_through_and_hook_asks(tmp_path, monkeypatch):
    f = tmp_path / "big.txt"
    f.write_text("x" * 5000)
    # Force the read to refuse via a tiny limit (simulates oversize).
    monkeypatch.setattr(
        hook, "read_referenced_file", lambda p, max_bytes=10: None
    )
    msg = hook.extract_commit_message(f"git commit -F {f}")
    assert msg == hook.UNPARSEABLE_MESSAGE
    # And the hook would ask, because UNPARSEABLE_MESSAGE is a flagged issue.
    issues = hook.collect_issues(msg, staged=["a.py"], author="Dev <d@e.com>")
    assert any("not parseable" in i for i in issues)