flashgate 0.5.2__tar.gz → 0.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {flashgate-0.5.2 → flashgate-0.5.3}/PKG-INFO +6 -3
  2. {flashgate-0.5.2 → flashgate-0.5.3}/README.md +5 -2
  3. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate/board.py +2 -0
  4. flashgate-0.5.3/flashgate/gatestate.py +144 -0
  5. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate.egg-info/PKG-INFO +6 -3
  6. {flashgate-0.5.2 → flashgate-0.5.3}/pyproject.toml +1 -1
  7. flashgate-0.5.3/tests/test_gatestate.py +112 -0
  8. flashgate-0.5.2/flashgate/gatestate.py +0 -81
  9. flashgate-0.5.2/tests/test_gatestate.py +0 -42
  10. {flashgate-0.5.2 → flashgate-0.5.3}/LICENSE +0 -0
  11. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate/__init__.py +0 -0
  12. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate/__main__.py +0 -0
  13. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate/cli.py +0 -0
  14. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate/flasher.py +0 -0
  15. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate/mcp_server.py +0 -0
  16. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate/probes.py +0 -0
  17. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate/results.py +0 -0
  18. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate/serialmon.py +0 -0
  19. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate/sttools.py +0 -0
  20. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate/swdsig.py +0 -0
  21. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate.egg-info/SOURCES.txt +0 -0
  22. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate.egg-info/dependency_links.txt +0 -0
  23. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate.egg-info/entry_points.txt +0 -0
  24. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate.egg-info/requires.txt +0 -0
  25. {flashgate-0.5.2 → flashgate-0.5.3}/flashgate.egg-info/top_level.txt +0 -0
  26. {flashgate-0.5.2 → flashgate-0.5.3}/setup.cfg +0 -0
  27. {flashgate-0.5.2 → flashgate-0.5.3}/tests/test_board.py +0 -0
  28. {flashgate-0.5.2 → flashgate-0.5.3}/tests/test_cli.py +0 -0
  29. {flashgate-0.5.2 → flashgate-0.5.3}/tests/test_mcp.py +0 -0
  30. {flashgate-0.5.2 → flashgate-0.5.3}/tests/test_probes.py +0 -0
  31. {flashgate-0.5.2 → flashgate-0.5.3}/tests/test_swdsig.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashgate
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/Lion-1209/flashgate
@@ -47,8 +47,11 @@ FLASHGATE-BOOT board=apollo-h743 git=2c58bd3 build=2026-08-28T07:26:35Z rtos=Fre
47
47
  ```
48
48
 
49
49
  Both carry the git sha plus `-dirty` when the tree differs from HEAD, so a
50
- passing verify proves the board is running exactly the code you're looking
51
- at. Functional probes then send real commands and assert on the answers,
50
+ passing verify proves the board is running a build of your current HEAD
51
+ (`-dirty` marks uncommitted changes; it does not fingerprint their
52
+ content — the Stop hook's tree fingerprint covers that side: tracked diffs
53
+ in full, untracked files by path + size + first 4 MiB. Files ignored by
54
+ git and nested git repositories are outside the gate). Functional probes then send real commands and assert on the answers,
52
55
  including register readbacks (TIM3 CCR), not firmware self-reports:
53
56
 
54
57
  ```yaml
@@ -28,8 +28,11 @@ FLASHGATE-BOOT board=apollo-h743 git=2c58bd3 build=2026-08-28T07:26:35Z rtos=Fre
28
28
  ```
29
29
 
30
30
  Both carry the git sha plus `-dirty` when the tree differs from HEAD, so a
31
- passing verify proves the board is running exactly the code you're looking
32
- at. Functional probes then send real commands and assert on the answers,
31
+ passing verify proves the board is running a build of your current HEAD
32
+ (`-dirty` marks uncommitted changes; it does not fingerprint their
33
+ content — the Stop hook's tree fingerprint covers that side: tracked diffs
34
+ in full, untracked files by path + size + first 4 MiB. Files ignored by
35
+ git and nested git repositories are outside the gate). Functional probes then send real commands and assert on the answers,
33
36
  including register readbacks (TIM3 CCR), not firmware self-reports:
34
37
 
35
38
  ```yaml
@@ -48,11 +48,13 @@ class Board:
48
48
  sha = subprocess.run(
49
49
  ["git", "rev-parse", "--short=7", "HEAD"],
50
50
  cwd=self.firmware_dir, capture_output=True, text=True,
51
+ encoding="utf-8", errors="replace",
51
52
  timeout=15, check=True,
52
53
  ).stdout.strip()
53
54
  status = subprocess.run(
54
55
  ["git", "status", "--porcelain"],
55
56
  cwd=self.firmware_dir, capture_output=True, text=True,
57
+ encoding="utf-8", errors="replace",
56
58
  timeout=15, check=True,
57
59
  ).stdout.strip()
58
60
  if status:
@@ -0,0 +1,144 @@
1
+ """Gate state: fingerprint the firmware tree, remember the last PASS.
2
+
3
+ The Stop hook only re-runs the (slow) hardware verify when the firmware tree
4
+ actually changed since the last passing run. The fingerprint covers HEAD,
5
+ the full diff against HEAD, and the untracked-file list — so any content
6
+ change to a watched file produces a new fingerprint.
7
+
8
+ Escalation: after MAX_CONSECUTIVE_BLOCKS failed blocks for the SAME
9
+ fingerprint, the gate releases with a warning instead of blocking forever
10
+ (coderio VerifyGate semantics: never wedge the session, never silently
11
+ give up).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import json
18
+ import subprocess
19
+ from datetime import datetime, timezone
20
+ from fnmatch import fnmatch
21
+ from pathlib import Path
22
+
23
+ STATE_DIR = ".flashgate"
24
+ STATE_FILE = "state.json"
25
+ MAX_CONSECUTIVE_BLOCKS = 2
26
+
27
+ DEFAULT_WATCH = [
28
+ "*.c", "*.h", "*.s", "*.ld", "*.ioc",
29
+ "CMakeLists.txt", "*.cmake", "cmake/*",
30
+ ]
31
+
32
+
33
+ def _git(args: list[str], cwd: Path) -> str:
34
+ # errors="surrogateescape": `ls-files -z` emits raw UTF-8 path bytes and
35
+ # text=True alone would decode with the ANSI code page (cp936 on a
36
+ # Chinese Windows) — a non-ASCII path then kills the reader thread,
37
+ # stdout becomes None, and the Stop hook crashes to exit 1, which the
38
+ # hook contract treats as non-blocking: the gate would fail OPEN.
39
+ try:
40
+ proc = subprocess.run(
41
+ ["git", *args], cwd=cwd, capture_output=True, text=True,
42
+ encoding="utf-8", errors="surrogateescape",
43
+ timeout=20, check=True,
44
+ )
45
+ return proc.stdout or ""
46
+ except (subprocess.SubprocessError, OSError):
47
+ return ""
48
+
49
+
50
+ _HASH_CAP_BYTES = 4 * 1024 * 1024 # per file: length + first 4 MiB
51
+
52
+
53
+ def _untracked_files(fw_dir: Path) -> list[str]:
54
+ """Untracked files, recursively, gitignore-respecting, no quoting.
55
+
56
+ `git status --porcelain` collapses a new directory to '?? dir/' (hiding
57
+ every file inside) and C-escapes non-ASCII paths under core.quotepath —
58
+ both let content edits slip past a content digest. `ls-files
59
+ --others --exclude-standard -z` lists real paths, NUL-separated."""
60
+ out = _git(["ls-files", "--others", "--exclude-standard", "-z"], fw_dir)
61
+ return sorted(p for p in out.split("\x00") if p)
62
+
63
+
64
+ def _untracked_content_digest(fw_dir: Path) -> str:
65
+ """Digest over the CONTENT of untracked files.
66
+
67
+ Found by the 2026-09-13 external review: git diff ignores untracked
68
+ files and git status lists only their paths, so editing an untracked
69
+ source file after a cached PASS kept the fingerprint unchanged — the
70
+ Stop hook reused the stale green. Hashing path + size + content closes
71
+ it. The gate's own state dir is excluded: save_state() rewrites a
72
+ timestamp there, and letting it feed the fingerprint would invalidate
73
+ every cached PASS."""
74
+ h = hashlib.sha256()
75
+ for path in _untracked_files(fw_dir):
76
+ if path == ".flashgate" or path.startswith(".flashgate/"):
77
+ continue
78
+ f = fw_dir / path
79
+ try:
80
+ size = f.stat().st_size
81
+ h.update(f"{path}\x00{size}\x00".encode("utf-8", errors="replace"))
82
+ with f.open("rb") as fh: # chunked: never OOM on a
83
+ remaining = min(size, _HASH_CAP_BYTES) # stray huge file
84
+ while remaining > 0:
85
+ chunk = fh.read(min(1 << 20, remaining))
86
+ if not chunk:
87
+ break
88
+ h.update(chunk)
89
+ remaining -= len(chunk)
90
+ except OSError:
91
+ h.update(f"{path}\x00<unreadable>\x00".encode("utf-8", errors="replace"))
92
+ h.update(b"\x00")
93
+ return h.hexdigest()
94
+
95
+
96
+ def tree_fingerprint(fw_dir: Path) -> str:
97
+ """Content-identity of the working tree (not just HEAD: dirty trees with
98
+ different edits must NOT share a fingerprint — including edits to
99
+ untracked files, whose contents are hashed in explicitly)."""
100
+ head = _git(["rev-parse", "HEAD"], fw_dir)
101
+ diff = _git(["diff", "HEAD"], fw_dir)
102
+ status = _git(["status", "--porcelain"], fw_dir)
103
+ untracked = _untracked_content_digest(fw_dir)
104
+ blob = "\x00".join((head, diff, status, untracked))
105
+ return hashlib.sha256(blob.encode("utf-8", errors="replace")).hexdigest()
106
+
107
+
108
+ def watched_paths(fw_dir: Path, patterns: list[str]) -> list[str]:
109
+ """Working-tree changes that match the watch globs (gate triggers)."""
110
+ status = _git(["status", "--porcelain"], fw_dir)
111
+ watched: list[str] = []
112
+ for line in status.splitlines():
113
+ if line.startswith("?? "):
114
+ continue # untracked: handled below, dir-collapsed in status
115
+ path = line[3:].strip().strip('"')
116
+ if " -> " in path: # rename: judge by the new name
117
+ path = path.split(" -> ")[1]
118
+ if path and any(fnmatch(path, pat) for pat in patterns):
119
+ watched.append(path)
120
+ # Untracked files come from ls-files: a new directory collapses to a
121
+ # single '?? dir/' status line that no *.c glob can match, which used
122
+ # to mean new files in new dirs never even triggered the gate.
123
+ for path in _untracked_files(fw_dir):
124
+ if any(fnmatch(path, pat) for pat in patterns):
125
+ watched.append(path)
126
+ return watched
127
+
128
+
129
+ def state_path(fw_dir: Path) -> Path:
130
+ return fw_dir / STATE_DIR / STATE_FILE
131
+
132
+
133
+ def load_state(fw_dir: Path) -> dict:
134
+ try:
135
+ return json.loads(state_path(fw_dir).read_text(encoding="utf-8"))
136
+ except (OSError, ValueError):
137
+ return {}
138
+
139
+
140
+ def save_state(fw_dir: Path, **fields) -> None:
141
+ path = state_path(fw_dir)
142
+ path.parent.mkdir(parents=True, exist_ok=True)
143
+ payload = {"updated": datetime.now(timezone.utc).isoformat(), **fields}
144
+ path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashgate
3
- Version: 0.5.2
3
+ Version: 0.5.3
4
4
  Summary: Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/Lion-1209/flashgate
@@ -47,8 +47,11 @@ FLASHGATE-BOOT board=apollo-h743 git=2c58bd3 build=2026-08-28T07:26:35Z rtos=Fre
47
47
  ```
48
48
 
49
49
  Both carry the git sha plus `-dirty` when the tree differs from HEAD, so a
50
- passing verify proves the board is running exactly the code you're looking
51
- at. Functional probes then send real commands and assert on the answers,
50
+ passing verify proves the board is running a build of your current HEAD
51
+ (`-dirty` marks uncommitted changes; it does not fingerprint their
52
+ content — the Stop hook's tree fingerprint covers that side: tracked diffs
53
+ in full, untracked files by path + size + first 4 MiB. Files ignored by
54
+ git and nested git repositories are outside the gate). Functional probes then send real commands and assert on the answers,
52
55
  including register readbacks (TIM3 CCR), not firmware self-reports:
53
56
 
54
57
  ```yaml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flashgate"
7
- version = "0.5.2"
7
+ version = "0.5.3"
8
8
  description = "Hardware-in-the-loop verification gate: your agent can't claim the firmware works until the board says so."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -0,0 +1,112 @@
1
+ """Tree fingerprint and watched-path filtering over a real git repo."""
2
+
3
+ from flashgate import gatestate
4
+
5
+
6
+ class TestWatchedPaths:
7
+ def test_watch_filter(self, git_repo):
8
+ (git_repo / "App" / "extra.c").write_text("int x;\n")
9
+ (git_repo / "readme2.md").write_text("docs\n")
10
+ watched = gatestate.watched_paths(git_repo, ["*.c", "*.h"])
11
+ assert "App/extra.c" in watched
12
+ assert all(w.endswith((".c", ".h")) for w in watched)
13
+
14
+ def test_no_changes(self, git_repo):
15
+ assert gatestate.watched_paths(git_repo, ["*.c"]) == []
16
+
17
+
18
+ class TestTreeFingerprint:
19
+ def test_stable_when_unchanged(self, git_repo):
20
+ a = gatestate.tree_fingerprint(git_repo)
21
+ b = gatestate.tree_fingerprint(git_repo)
22
+ assert a == b
23
+
24
+ def test_changes_with_content(self, git_repo):
25
+ before = gatestate.tree_fingerprint(git_repo)
26
+ (git_repo / "App" / "main.c").write_text("int main(void) { return 2; }\n")
27
+ assert gatestate.tree_fingerprint(git_repo) != before
28
+
29
+ def test_two_dirty_states_differ(self, git_repo):
30
+ (git_repo / "App" / "main.c").write_text("/* a */\n")
31
+ a = gatestate.tree_fingerprint(git_repo)
32
+ (git_repo / "App" / "main.c").write_text("/* b */\n")
33
+ assert gatestate.tree_fingerprint(git_repo) != a
34
+
35
+
36
+ class TestState:
37
+ def test_roundtrip_and_missing(self, tmp_path, git_repo):
38
+ assert gatestate.load_state(git_repo) == {}
39
+ gatestate.save_state(git_repo, verified_fingerprint="abc", consecutive_blocks=0)
40
+ s = gatestate.load_state(git_repo)
41
+ assert s["verified_fingerprint"] == "abc"
42
+ assert "updated" in s
43
+
44
+
45
+ class TestUntrackedContentFingerprint:
46
+ def test_untracked_content_edit_changes_fingerprint(self, git_repo):
47
+ # 2026-09-13 external-review finding: git status lists untracked
48
+ # paths only, so editing an untracked file's CONTENT kept the tree
49
+ # fingerprint unchanged — the Stop hook would reuse a cached PASS
50
+ # over silently-modified code. Contents must be hashed in.
51
+ from flashgate.gatestate import tree_fingerprint
52
+ untracked = git_repo / "App" / "new.c"
53
+ untracked.write_text("int x = 1;\n")
54
+ fp1 = tree_fingerprint(git_repo)
55
+ untracked.write_text("int x = 999; /* silently changed */\n")
56
+ fp2 = tree_fingerprint(git_repo)
57
+ assert fp1 != fp2, "untracked content edit MUST change the fingerprint"
58
+
59
+ def test_untracked_appearance_changes_fingerprint(self, git_repo):
60
+ from flashgate.gatestate import tree_fingerprint
61
+ fp1 = tree_fingerprint(git_repo)
62
+ (git_repo / "App" / "added.c").write_text("int y;\n")
63
+ assert tree_fingerprint(git_repo) != fp1
64
+
65
+ def test_tracked_edit_still_changes_fingerprint(self, git_repo):
66
+ from flashgate.gatestate import tree_fingerprint
67
+ fp1 = tree_fingerprint(git_repo)
68
+ (git_repo / "App" / "main.c").write_text("int main(void){return 1;}\n")
69
+ assert tree_fingerprint(git_repo) != fp1
70
+
71
+ def test_clean_tree_fingerprint_stable(self, git_repo):
72
+ from flashgate.gatestate import tree_fingerprint
73
+ assert tree_fingerprint(git_repo) == tree_fingerprint(git_repo)
74
+
75
+
76
+ class TestUntrackedFingerprintCompleteness:
77
+ """Residual holes found by the adversarial re-review of the first fix:
78
+ untracked NEW directories (git status collapses to '?? dir/') and
79
+ non-ASCII paths (core.quotepath escaping) must not hide content edits,
80
+ and the gate's own state writes must not invalidate the PASS cache."""
81
+
82
+ def test_new_directory_content_edit_changes_fingerprint(self, git_repo):
83
+ from flashgate.gatestate import tree_fingerprint
84
+ mod = git_repo / "NewModule"
85
+ mod.mkdir()
86
+ (mod / "mod.c").write_text("int f(void){return 1;}\n")
87
+ fp1 = tree_fingerprint(git_repo)
88
+ (mod / "mod.c").write_text("int f(void){return 999;}\n")
89
+ fp2 = tree_fingerprint(git_repo)
90
+ assert fp1 != fp2, "content edit inside an untracked NEW dir must be visible"
91
+
92
+ def test_non_ascii_path_content_edit_changes_fingerprint(self, git_repo):
93
+ from flashgate.gatestate import tree_fingerprint
94
+ src = git_repo / "App" / "新模块.c"
95
+ src.write_text("int a = 1;\n")
96
+ fp1 = tree_fingerprint(git_repo)
97
+ src.write_text("int a = 999; /* silently changed */\n")
98
+ fp2 = tree_fingerprint(git_repo)
99
+ assert fp1 != fp2, "non-ASCII path (quotepath escaping) must not hide edits"
100
+
101
+ def test_state_dir_writes_do_not_change_fingerprint(self, git_repo):
102
+ from flashgate.gatestate import save_state, tree_fingerprint
103
+ save_state(git_repo, status="pass", fingerprint="x") # state dir exists
104
+ fp1 = tree_fingerprint(git_repo)
105
+ save_state(git_repo, status="pass", fingerprint="y") # timestamp moves
106
+ assert tree_fingerprint(git_repo) == fp1, ".flashgate/ must stay out of the digest"
107
+
108
+ def test_new_dir_source_triggers_watch(self, git_repo):
109
+ from flashgate.gatestate import DEFAULT_WATCH, watched_paths
110
+ (git_repo / "NewModule").mkdir()
111
+ (git_repo / "NewModule" / "mod.c").write_text("int x;\n")
112
+ assert watched_paths(git_repo, DEFAULT_WATCH) == ["NewModule/mod.c"]
@@ -1,81 +0,0 @@
1
- """Gate state: fingerprint the firmware tree, remember the last PASS.
2
-
3
- The Stop hook only re-runs the (slow) hardware verify when the firmware tree
4
- actually changed since the last passing run. The fingerprint covers HEAD,
5
- the full diff against HEAD, and the untracked-file list — so any content
6
- change to a watched file produces a new fingerprint.
7
-
8
- Escalation: after MAX_CONSECUTIVE_BLOCKS failed blocks for the SAME
9
- fingerprint, the gate releases with a warning instead of blocking forever
10
- (coderio VerifyGate semantics: never wedge the session, never silently
11
- give up).
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- import hashlib
17
- import json
18
- import subprocess
19
- from datetime import datetime, timezone
20
- from fnmatch import fnmatch
21
- from pathlib import Path
22
-
23
- STATE_DIR = ".flashgate"
24
- STATE_FILE = "state.json"
25
- MAX_CONSECUTIVE_BLOCKS = 2
26
-
27
- DEFAULT_WATCH = [
28
- "*.c", "*.h", "*.s", "*.ld", "*.ioc",
29
- "CMakeLists.txt", "*.cmake", "cmake/*",
30
- ]
31
-
32
-
33
- def _git(args: list[str], cwd: Path) -> str:
34
- try:
35
- return subprocess.run(
36
- ["git", *args], cwd=cwd, capture_output=True, text=True,
37
- timeout=20, check=True,
38
- ).stdout
39
- except (subprocess.SubprocessError, OSError):
40
- return ""
41
-
42
-
43
- def tree_fingerprint(fw_dir: Path) -> str:
44
- """Content-identity of the working tree (not just HEAD: dirty trees with
45
- different edits must NOT share a fingerprint)."""
46
- head = _git(["rev-parse", "HEAD"], fw_dir)
47
- diff = _git(["diff", "HEAD"], fw_dir)
48
- status = _git(["status", "--porcelain"], fw_dir)
49
- blob = "\x00".join((head, diff, status))
50
- return hashlib.sha256(blob.encode("utf-8", errors="replace")).hexdigest()
51
-
52
-
53
- def watched_paths(fw_dir: Path, patterns: list[str]) -> list[str]:
54
- """Working-tree changes that match the watch globs (gate triggers)."""
55
- status = _git(["status", "--porcelain"], fw_dir)
56
- watched: list[str] = []
57
- for line in status.splitlines():
58
- path = line[3:].strip().strip('"')
59
- if " -> " in path: # rename: judge by the new name
60
- path = path.split(" -> ")[1]
61
- if path and any(fnmatch(path, pat) for pat in patterns):
62
- watched.append(path)
63
- return watched
64
-
65
-
66
- def state_path(fw_dir: Path) -> Path:
67
- return fw_dir / STATE_DIR / STATE_FILE
68
-
69
-
70
- def load_state(fw_dir: Path) -> dict:
71
- try:
72
- return json.loads(state_path(fw_dir).read_text(encoding="utf-8"))
73
- except (OSError, ValueError):
74
- return {}
75
-
76
-
77
- def save_state(fw_dir: Path, **fields) -> None:
78
- path = state_path(fw_dir)
79
- path.parent.mkdir(parents=True, exist_ok=True)
80
- payload = {"updated": datetime.now(timezone.utc).isoformat(), **fields}
81
- path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
@@ -1,42 +0,0 @@
1
- """Tree fingerprint and watched-path filtering over a real git repo."""
2
-
3
- from flashgate import gatestate
4
-
5
-
6
- class TestWatchedPaths:
7
- def test_watch_filter(self, git_repo):
8
- (git_repo / "App" / "extra.c").write_text("int x;\n")
9
- (git_repo / "readme2.md").write_text("docs\n")
10
- watched = gatestate.watched_paths(git_repo, ["*.c", "*.h"])
11
- assert "App/extra.c" in watched
12
- assert all(w.endswith((".c", ".h")) for w in watched)
13
-
14
- def test_no_changes(self, git_repo):
15
- assert gatestate.watched_paths(git_repo, ["*.c"]) == []
16
-
17
-
18
- class TestTreeFingerprint:
19
- def test_stable_when_unchanged(self, git_repo):
20
- a = gatestate.tree_fingerprint(git_repo)
21
- b = gatestate.tree_fingerprint(git_repo)
22
- assert a == b
23
-
24
- def test_changes_with_content(self, git_repo):
25
- before = gatestate.tree_fingerprint(git_repo)
26
- (git_repo / "App" / "main.c").write_text("int main(void) { return 2; }\n")
27
- assert gatestate.tree_fingerprint(git_repo) != before
28
-
29
- def test_two_dirty_states_differ(self, git_repo):
30
- (git_repo / "App" / "main.c").write_text("/* a */\n")
31
- a = gatestate.tree_fingerprint(git_repo)
32
- (git_repo / "App" / "main.c").write_text("/* b */\n")
33
- assert gatestate.tree_fingerprint(git_repo) != a
34
-
35
-
36
- class TestState:
37
- def test_roundtrip_and_missing(self, tmp_path, git_repo):
38
- assert gatestate.load_state(git_repo) == {}
39
- gatestate.save_state(git_repo, verified_fingerprint="abc", consecutive_blocks=0)
40
- s = gatestate.load_state(git_repo)
41
- assert s["verified_fingerprint"] == "abc"
42
- assert "updated" in s
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes