test_project_scaffold.py
6.56 KB
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
"""U-002 tests: c_project_create's directory-scaffolding + DB-persistence logic.
Run: python3 -m unittest src.test_project_scaffold -v
"""
import os
import shutil
import stat
import subprocess
import tempfile
import unittest
from src import agent_profile, project_scaffold, schema
def _init_git_repo(path):
subprocess.run(["git", "init"], cwd=path, check=True, capture_output=True)
class ProjectScaffoldTestBase(unittest.TestCase):
def setUp(self):
self.conn = schema.connect(":memory:")
schema.create_schema(self.conn)
self.tmpdir = tempfile.mkdtemp(prefix="graphlemon-u002-")
def tearDown(self):
self.conn.close()
shutil.rmtree(self.tmpdir, ignore_errors=True)
class HappyPathTest(ProjectScaffoldTestBase):
def test_existing_git_repo_gets_graphlemon_subdir_and_init_commit(self):
_init_git_repo(self.tmpdir)
result = project_scaffold.create_project(self.conn, self.tmpdir)
self.assertTrue(result["has_git_repo"])
self.assertTrue(result["graphlemon_subdir_initialized"])
self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, ".graphlemon")))
log = subprocess.run(
["git", "log", "--oneline"], cwd=self.tmpdir, check=True, capture_output=True, text=True
)
self.assertIn("graphlemon: initialize .graphlemon/ workspace", log.stdout)
row = self.conn.execute(
"SELECT project_id, target_folder_path, has_git_repo, graphlemon_subdir_initialized, status "
"FROM project WHERE project_id = ?",
(result["project_id"],),
).fetchone()
self.assertIsNotNone(row)
self.assertEqual(row[1], self.tmpdir)
self.assertEqual(row[2], 1)
self.assertEqual(row[3], 1)
self.assertEqual(row[4], "active")
def test_project_name_defaults_to_folder_name(self):
_init_git_repo(self.tmpdir)
result = project_scaffold.create_project(self.conn, self.tmpdir)
row = self.conn.execute(
"SELECT name FROM project WHERE project_id = ?", (result["project_id"],)
).fetchone()
self.assertEqual(row[0], os.path.basename(self.tmpdir))
def test_explicit_name_used_when_given(self):
_init_git_repo(self.tmpdir)
result = project_scaffold.create_project(self.conn, self.tmpdir, name="Snake Game")
row = self.conn.execute(
"SELECT name FROM project WHERE project_id = ?", (result["project_id"],)
).fetchone()
self.assertEqual(row[0], "Snake Game")
def test_agent_definitions_deployed_and_committed(self):
# Real incident (2026-08-03): without this, the first task ever
# triggered on a freshly created project fails immediately --
# `claude --agent planner` has nothing to find in this cwd. Only
# self-target usage was ever exercised, where .claude/agents/ is
# already committed as part of graphlemon's own repo, so this gap
# went unnoticed until a real non-self-target project hit it.
_init_git_repo(self.tmpdir)
project_scaffold.create_project(self.conn, self.tmpdir)
for org_node in agent_profile.ORG_NODES:
agent_path = os.path.join(self.tmpdir, ".claude", "agents", org_node + ".md")
self.assertTrue(os.path.isfile(agent_path), agent_path)
status = subprocess.run(
["git", "status", "--porcelain"], cwd=self.tmpdir, check=True, capture_output=True, text=True
)
self.assertEqual(status.stdout, "", "agent definitions must be committed, not left untracked")
class NonGitRepoConfirmationFlowTest(ProjectScaffoldTestBase):
"""02-V §2.1 / 03-C §3.0: never silently `git init`."""
def test_declines_without_confirmation_and_leaves_folder_untouched(self):
with self.assertRaises(project_scaffold.GitInitDeclinedError):
project_scaffold.create_project(self.conn, self.tmpdir, init_git_if_missing=False)
self.assertFalse(os.path.isdir(os.path.join(self.tmpdir, ".git")))
self.assertFalse(os.path.isdir(os.path.join(self.tmpdir, ".graphlemon")))
count = self.conn.execute("SELECT COUNT(*) FROM project").fetchone()[0]
self.assertEqual(count, 0)
def test_initializes_git_when_explicitly_confirmed(self):
result = project_scaffold.create_project(self.conn, self.tmpdir, init_git_if_missing=True)
self.assertTrue(result["has_git_repo"])
self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, ".git")))
self.assertTrue(os.path.isdir(os.path.join(self.tmpdir, ".graphlemon")))
class IdempotentCreationTest(ProjectScaffoldTestBase):
def test_second_call_returns_existing_record_without_new_commit(self):
_init_git_repo(self.tmpdir)
first = project_scaffold.create_project(self.conn, self.tmpdir)
second = project_scaffold.create_project(self.conn, self.tmpdir)
self.assertEqual(first["project_id"], second["project_id"])
count = self.conn.execute("SELECT COUNT(*) FROM project").fetchone()[0]
self.assertEqual(count, 1)
log = subprocess.run(
["git", "log", "--oneline"], cwd=self.tmpdir, check=True, capture_output=True, text=True
)
self.assertEqual(len(log.stdout.strip().splitlines()), 1)
def test_idempotent_hit_skips_path_validation(self):
# REQ-11 failure case: repeat calls on an already-registered path must
# return the existing record even if the path itself is no longer
# valid, not re-validate and error.
_init_git_repo(self.tmpdir)
first = project_scaffold.create_project(self.conn, self.tmpdir)
shutil.rmtree(self.tmpdir)
second = project_scaffold.create_project(self.conn, self.tmpdir)
self.assertEqual(first["project_id"], second["project_id"])
class PathValidationErrorsTest(ProjectScaffoldTestBase):
def test_nonexistent_path_raises_path_not_found(self):
missing = os.path.join(self.tmpdir, "does-not-exist")
with self.assertRaises(project_scaffold.PathNotFoundError):
project_scaffold.create_project(self.conn, missing)
def test_unwritable_path_raises_path_not_writable(self):
os.chmod(self.tmpdir, stat.S_IRUSR | stat.S_IXUSR)
try:
if os.access(self.tmpdir, os.W_OK):
self.skipTest("running as a user that bypasses permission bits (e.g. root)")
with self.assertRaises(project_scaffold.PathNotWritableError):
project_scaffold.create_project(self.conn, self.tmpdir)
finally:
os.chmod(self.tmpdir, stat.S_IRWXU)
if __name__ == "__main__":
unittest.main()