opencode-gbuild 0.5.0 → 0.6.0
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.
- package/README.md +43 -37
- package/agents/gbuild-auditor.md +1 -1
- package/agents/gbuild-reviewer.md +17 -10
- package/cypher/complete.cypher +23 -0
- package/cypher/dispatch.cypher +13 -0
- package/cypher/feature.cypher +21 -0
- package/cypher/node.cypher +39 -0
- package/cypher/record-review.cypher +17 -0
- package/cypher/schema.cypher +45 -0
- package/cypher/set-status.cypher +11 -0
- package/cypher/status.cypher +33 -0
- package/cypher/validate.cypher +105 -0
- package/package.json +2 -2
- package/reference/checklist.md +16 -16
- package/reference/cypher.md +102 -0
- package/reference/failure-policies.md +6 -6
- package/reference/graph-format.md +148 -183
- package/reference/shapes.md +10 -9
- package/skills/plan/SKILL.md +113 -102
- package/skills/pr/SKILL.md +5 -3
- package/skills/review/SKILL.md +15 -7
- package/skills/run/SKILL.md +97 -57
- package/skills/status/SKILL.md +35 -46
- package/templates/example.cypher +57 -0
- package/scripts/__pycache__/graph.cpython-314.pyc +0 -0
- package/scripts/__pycache__/test_graph.cpython-314.pyc +0 -0
- package/scripts/__pycache__/validate_format.cpython-314.pyc +0 -0
- package/scripts/graph.py +0 -239
- package/scripts/test_graph.py +0 -403
- package/scripts/validate_format.py +0 -378
- package/templates/graph.json +0 -110
package/scripts/test_graph.py
DELETED
|
@@ -1,403 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import shutil
|
|
3
|
-
import subprocess
|
|
4
|
-
import sys
|
|
5
|
-
import tempfile
|
|
6
|
-
import unittest
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
|
|
9
|
-
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
10
|
-
|
|
11
|
-
from graph import ( # noqa: E402
|
|
12
|
-
GraphError,
|
|
13
|
-
acceptance_nodes,
|
|
14
|
-
compute_frontier,
|
|
15
|
-
compute_report,
|
|
16
|
-
gbuild_nodes,
|
|
17
|
-
load_graph,
|
|
18
|
-
satisfies,
|
|
19
|
-
topo_waves,
|
|
20
|
-
validate,
|
|
21
|
-
)
|
|
22
|
-
from validate_format import TYPE_TO_LABEL # noqa: E402
|
|
23
|
-
|
|
24
|
-
PLUGIN_ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
-
TEMPLATE = PLUGIN_ROOT / "templates" / "graph.json"
|
|
26
|
-
CYPHERLITE_BIN = shutil.which("cypherlite")
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def cypherlite_query(graph_path, query):
|
|
30
|
-
"""Run a Cypher query against a GraphData file the way gbuild does.
|
|
31
|
-
|
|
32
|
-
CypherLite opens a directory store holding `graph.json`, not a bare `.json`
|
|
33
|
-
file, so stage the file in a scratch dir as `graph.json` and open that.
|
|
34
|
-
"""
|
|
35
|
-
with tempfile.TemporaryDirectory() as d:
|
|
36
|
-
shutil.copyfile(graph_path, Path(d) / "graph.json")
|
|
37
|
-
return subprocess.run(
|
|
38
|
-
[CYPHERLITE_BIN, d, "-json", query],
|
|
39
|
-
capture_output=True, text=True,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
# --- canonical factories emitting GraphData ---------------------------------
|
|
44
|
-
|
|
45
|
-
def _gbuild_node(nid, slug, ntype="code", deps=None, satisfies_acc=None, **overrides):
|
|
46
|
-
"""Build a GbuildNode dict. deps/satisfies_acc are slug/acceptance-id lists
|
|
47
|
-
resolved to ids by graph_of; here we only stash the raw properties."""
|
|
48
|
-
base = {
|
|
49
|
-
"id": nid,
|
|
50
|
-
"labels": ["GbuildNode", TYPE_TO_LABEL[ntype]],
|
|
51
|
-
"properties": {
|
|
52
|
-
"slug": slug,
|
|
53
|
-
"title": slug,
|
|
54
|
-
"type": ntype,
|
|
55
|
-
"contract": {"input": {}, "output": {}},
|
|
56
|
-
"acceptance": [f"{slug} produces its declared output"],
|
|
57
|
-
"verify": None,
|
|
58
|
-
"failure_policy": "retry",
|
|
59
|
-
"model_tier": "cheap",
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
base["properties"].update(overrides.pop("props", {}))
|
|
63
|
-
base.update(overrides)
|
|
64
|
-
base["_deps"] = deps or []
|
|
65
|
-
base["_satisfies"] = satisfies_acc or []
|
|
66
|
-
return base
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def graph_of(*nodes, acceptance=None, feature="test", destination="test",
|
|
70
|
-
context="test", out_of_scope=None):
|
|
71
|
-
"""Assemble a full GraphData document.
|
|
72
|
-
|
|
73
|
-
ids: Feature=1, then one Acceptance node per acceptance bullet, then
|
|
74
|
-
GbuildNodes in the order given. Edges: HAS_ACCEPTANCE (feature->acceptance),
|
|
75
|
-
SATISFIES (node->acceptance), DEPENDS_ON (node->node), all sequential.
|
|
76
|
-
"""
|
|
77
|
-
out_nodes = []
|
|
78
|
-
out_rels = []
|
|
79
|
-
nid = 1
|
|
80
|
-
rid = 1
|
|
81
|
-
|
|
82
|
-
out_nodes.append({
|
|
83
|
-
"id": nid,
|
|
84
|
-
"labels": ["Feature"],
|
|
85
|
-
"properties": {
|
|
86
|
-
"feature": feature,
|
|
87
|
-
"destination": destination,
|
|
88
|
-
"context": context,
|
|
89
|
-
"out_of_scope": out_of_scope or [],
|
|
90
|
-
},
|
|
91
|
-
})
|
|
92
|
-
feature_id = nid
|
|
93
|
-
nid += 1
|
|
94
|
-
|
|
95
|
-
# acceptance nodes
|
|
96
|
-
acc_id_by_key = {} # "a-1" -> node id
|
|
97
|
-
for acc in (acceptance or []):
|
|
98
|
-
out_nodes.append({
|
|
99
|
-
"id": nid,
|
|
100
|
-
"labels": ["Acceptance"],
|
|
101
|
-
"properties": {"id": acc["id"], "text": acc["text"]},
|
|
102
|
-
})
|
|
103
|
-
out_rels.append({
|
|
104
|
-
"id": rid, "type": "HAS_ACCEPTANCE",
|
|
105
|
-
"start_node": feature_id, "end_node": nid, "properties": {},
|
|
106
|
-
})
|
|
107
|
-
acc_id_by_key[acc["id"]] = nid
|
|
108
|
-
nid += 1
|
|
109
|
-
rid += 1
|
|
110
|
-
|
|
111
|
-
# gbuild nodes — assign ids, remember slug->id for edge wiring
|
|
112
|
-
slug_to_id = {}
|
|
113
|
-
staged = []
|
|
114
|
-
for n in nodes:
|
|
115
|
-
n = dict(n)
|
|
116
|
-
n["id"] = nid
|
|
117
|
-
slug_to_id[n["properties"]["slug"]] = nid
|
|
118
|
-
staged.append(n)
|
|
119
|
-
nid += 1
|
|
120
|
-
# If no node declares any explicit satisfies, auto-wire every acceptance
|
|
121
|
-
# to every gbuild node so coverage holds for structural-only test graphs.
|
|
122
|
-
explicit_satisfies = any(n.get("_satisfies") for n in staged)
|
|
123
|
-
# strip the _deps/_satisfies stashed props and wire edges
|
|
124
|
-
for n in staged:
|
|
125
|
-
raw_deps = n.pop("_deps", [])
|
|
126
|
-
raw_satisfies = n.pop("_satisfies", [])
|
|
127
|
-
for dep_slug in raw_deps:
|
|
128
|
-
# allow dangling deps (end_node that doesn't exist) so validate can
|
|
129
|
-
# reject them; unknown slugs get a sentinel id.
|
|
130
|
-
out_rels.append({
|
|
131
|
-
"id": rid, "type": "DEPENDS_ON",
|
|
132
|
-
"start_node": n["id"], "end_node": slug_to_id.get(dep_slug, -1),
|
|
133
|
-
"properties": {},
|
|
134
|
-
})
|
|
135
|
-
rid += 1
|
|
136
|
-
if explicit_satisfies:
|
|
137
|
-
for acc_key in raw_satisfies:
|
|
138
|
-
out_rels.append({
|
|
139
|
-
"id": rid, "type": "SATISFIES",
|
|
140
|
-
"start_node": n["id"], "end_node": acc_id_by_key[acc_key],
|
|
141
|
-
"properties": {},
|
|
142
|
-
})
|
|
143
|
-
rid += 1
|
|
144
|
-
else:
|
|
145
|
-
for acc_nid in acc_id_by_key.values():
|
|
146
|
-
out_rels.append({
|
|
147
|
-
"id": rid, "type": "SATISFIES",
|
|
148
|
-
"start_node": n["id"], "end_node": acc_nid, "properties": {},
|
|
149
|
-
})
|
|
150
|
-
rid += 1
|
|
151
|
-
out_nodes.extend(staged)
|
|
152
|
-
|
|
153
|
-
return {
|
|
154
|
-
"nodes": out_nodes,
|
|
155
|
-
"relationships": out_rels,
|
|
156
|
-
"indexes": [],
|
|
157
|
-
"constraints": [],
|
|
158
|
-
"next_node_id": nid,
|
|
159
|
-
"next_rel_id": rid,
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def node(slug, deps=None, satisfies=None, ntype="code", **overrides):
|
|
164
|
-
return _gbuild_node(None, slug, ntype=ntype, deps=deps, satisfies_acc=satisfies, **overrides)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def write_checkpoint(state_dir, slug, status, **fields):
|
|
168
|
-
Path(state_dir).mkdir(parents=True, exist_ok=True)
|
|
169
|
-
payload = {"status": status, **fields}
|
|
170
|
-
(Path(state_dir) / f"{slug}.json").write_text(json.dumps(payload))
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
ACC = [{"id": "a-1", "text": "feature bar"}]
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
class ChainTests(unittest.TestCase):
|
|
177
|
-
def test_waves_are_one_node_each(self):
|
|
178
|
-
g = graph_of(node("a"), node("b", ["a"]), node("c", ["b"]), acceptance=ACC)
|
|
179
|
-
validate(g)
|
|
180
|
-
self.assertEqual(topo_waves(g), [["a"], ["b"], ["c"]])
|
|
181
|
-
|
|
182
|
-
def test_frontier_is_only_the_root(self):
|
|
183
|
-
g = graph_of(node("a"), node("b", ["a"]), node("c", ["b"]), acceptance=ACC)
|
|
184
|
-
with tempfile.TemporaryDirectory() as d:
|
|
185
|
-
self.assertEqual(compute_frontier(g, d), ["a"])
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
class DiamondTests(unittest.TestCase):
|
|
189
|
-
def setUp(self):
|
|
190
|
-
self.g = graph_of(
|
|
191
|
-
node("a"),
|
|
192
|
-
node("b", ["a"]),
|
|
193
|
-
node("c", ["a"]),
|
|
194
|
-
node("d", ["b", "c"]),
|
|
195
|
-
acceptance=ACC,
|
|
196
|
-
)
|
|
197
|
-
validate(self.g)
|
|
198
|
-
|
|
199
|
-
def test_waves_group_b_and_c_together(self):
|
|
200
|
-
self.assertEqual(topo_waves(self.g), [["a"], ["b", "c"], ["d"]])
|
|
201
|
-
|
|
202
|
-
def test_b_and_c_both_blocked_until_a_completes(self):
|
|
203
|
-
with tempfile.TemporaryDirectory() as d:
|
|
204
|
-
report = compute_report(self.g, d)
|
|
205
|
-
self.assertEqual(report["frontier"], ["a"])
|
|
206
|
-
self.assertEqual(report["blocked"], ["b", "c", "d"])
|
|
207
|
-
|
|
208
|
-
def test_b_and_c_become_frontier_together_once_a_completes(self):
|
|
209
|
-
with tempfile.TemporaryDirectory() as d:
|
|
210
|
-
write_checkpoint(d, "a", "completed")
|
|
211
|
-
report = compute_report(self.g, d)
|
|
212
|
-
self.assertEqual(report["frontier"], ["b", "c"])
|
|
213
|
-
self.assertEqual(report["blocked"], ["d"])
|
|
214
|
-
|
|
215
|
-
def test_d_waits_for_both_b_and_c(self):
|
|
216
|
-
with tempfile.TemporaryDirectory() as d:
|
|
217
|
-
write_checkpoint(d, "a", "completed")
|
|
218
|
-
write_checkpoint(d, "b", "completed")
|
|
219
|
-
report = compute_report(self.g, d)
|
|
220
|
-
self.assertEqual(report["frontier"], ["c"])
|
|
221
|
-
self.assertEqual(report["blocked"], ["d"])
|
|
222
|
-
|
|
223
|
-
write_checkpoint(d, "c", "completed")
|
|
224
|
-
report = compute_report(self.g, d)
|
|
225
|
-
self.assertEqual(report["frontier"], ["d"])
|
|
226
|
-
self.assertEqual(report["blocked"], [])
|
|
227
|
-
|
|
228
|
-
def test_resume_skips_completed_nodes(self):
|
|
229
|
-
with tempfile.TemporaryDirectory() as d:
|
|
230
|
-
write_checkpoint(d, "a", "completed")
|
|
231
|
-
write_checkpoint(d, "b", "completed")
|
|
232
|
-
write_checkpoint(d, "c", "completed")
|
|
233
|
-
report = compute_report(self.g, d)
|
|
234
|
-
self.assertEqual(report["completed"], ["a", "b", "c"])
|
|
235
|
-
self.assertEqual(report["frontier"], ["d"])
|
|
236
|
-
|
|
237
|
-
def test_cancelled_dependency_counts_as_satisfied(self):
|
|
238
|
-
with tempfile.TemporaryDirectory() as d:
|
|
239
|
-
write_checkpoint(d, "a", "completed")
|
|
240
|
-
write_checkpoint(d, "b", "cancelled")
|
|
241
|
-
write_checkpoint(d, "c", "completed")
|
|
242
|
-
report = compute_report(self.g, d)
|
|
243
|
-
self.assertEqual(report["frontier"], ["d"])
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
class ValidationTests(unittest.TestCase):
|
|
247
|
-
def test_cycle_is_rejected(self):
|
|
248
|
-
g = graph_of(node("a", ["b"]), node("b", ["a"]), acceptance=ACC)
|
|
249
|
-
with self.assertRaises(GraphError):
|
|
250
|
-
validate(g)
|
|
251
|
-
|
|
252
|
-
def test_missing_dependency_is_rejected(self):
|
|
253
|
-
g = graph_of(node("a", ["ghost"]), acceptance=ACC)
|
|
254
|
-
with self.assertRaises(GraphError):
|
|
255
|
-
validate(g)
|
|
256
|
-
|
|
257
|
-
def test_empty_acceptance_is_rejected(self):
|
|
258
|
-
n = node("a")
|
|
259
|
-
n["properties"]["acceptance"] = []
|
|
260
|
-
g = graph_of(n, acceptance=ACC)
|
|
261
|
-
with self.assertRaises(GraphError):
|
|
262
|
-
validate(g)
|
|
263
|
-
|
|
264
|
-
def test_blank_acceptance_bullet_is_rejected(self):
|
|
265
|
-
n = node("a")
|
|
266
|
-
n["properties"]["acceptance"] = [" "]
|
|
267
|
-
g = graph_of(n, acceptance=ACC)
|
|
268
|
-
with self.assertRaises(GraphError):
|
|
269
|
-
validate(g)
|
|
270
|
-
|
|
271
|
-
def test_uncovered_acceptance_is_rejected(self):
|
|
272
|
-
# acceptance a-2 has no SATISFIES edge
|
|
273
|
-
g = graph_of(node("a", satisfies=["a-1"]),
|
|
274
|
-
acceptance=ACC + [{"id": "a-2", "text": "uncovered"}])
|
|
275
|
-
with self.assertRaises(GraphError):
|
|
276
|
-
validate(g)
|
|
277
|
-
|
|
278
|
-
def test_counter_invariant_is_checked(self):
|
|
279
|
-
g = graph_of(node("a"), acceptance=ACC)
|
|
280
|
-
g["next_node_id"] = 999
|
|
281
|
-
with self.assertRaises(GraphError):
|
|
282
|
-
validate(g)
|
|
283
|
-
|
|
284
|
-
def test_satisfies_property_is_rejected(self):
|
|
285
|
-
n = node("a")
|
|
286
|
-
n["properties"]["satisfies"] = ["a-1"]
|
|
287
|
-
g = graph_of(n, acceptance=ACC)
|
|
288
|
-
with self.assertRaises(GraphError):
|
|
289
|
-
validate(g)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
class TemplateTests(unittest.TestCase):
|
|
293
|
-
def test_template_validates(self):
|
|
294
|
-
g = load_graph(str(TEMPLATE))
|
|
295
|
-
validate(g) # raises if invalid
|
|
296
|
-
|
|
297
|
-
def test_template_waves(self):
|
|
298
|
-
g = load_graph(str(TEMPLATE))
|
|
299
|
-
self.assertEqual(topo_waves(g), [
|
|
300
|
-
["a-produce-shared-value"],
|
|
301
|
-
["b-consume-doubled", "c-consume-squared"],
|
|
302
|
-
["d-join-and-sum"],
|
|
303
|
-
])
|
|
304
|
-
|
|
305
|
-
def test_template_frontier_is_root(self):
|
|
306
|
-
g = load_graph(str(TEMPLATE))
|
|
307
|
-
with tempfile.TemporaryDirectory() as d:
|
|
308
|
-
self.assertEqual(compute_frontier(g, d), ["a-produce-shared-value"])
|
|
309
|
-
|
|
310
|
-
def test_template_acceptance_coverage(self):
|
|
311
|
-
g = load_graph(str(TEMPLATE))
|
|
312
|
-
# every acceptance node is targeted by >=1 SATISFIES and 1 HAS_ACCEPTANCE
|
|
313
|
-
acc_ids = {n["id"] for n in acceptance_nodes(g)}
|
|
314
|
-
sat_targets = set()
|
|
315
|
-
has_targets = set()
|
|
316
|
-
for r in g["relationships"]:
|
|
317
|
-
if r["type"] == "SATISFIES":
|
|
318
|
-
sat_targets.add(r["end_node"])
|
|
319
|
-
if r["type"] == "HAS_ACCEPTANCE":
|
|
320
|
-
has_targets.add(r["end_node"])
|
|
321
|
-
self.assertEqual(acc_ids, sat_targets)
|
|
322
|
-
self.assertEqual(acc_ids, has_targets)
|
|
323
|
-
|
|
324
|
-
def test_template_counters(self):
|
|
325
|
-
g = load_graph(str(TEMPLATE))
|
|
326
|
-
max_nid = max(n["id"] for n in g["nodes"])
|
|
327
|
-
max_rid = max(r["id"] for r in g["relationships"])
|
|
328
|
-
self.assertEqual(g["next_node_id"], max_nid + 1)
|
|
329
|
-
self.assertEqual(g["next_rel_id"], max_rid + 1)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
@unittest.skipUnless(CYPHERLITE_BIN, "cypherlite binary not on PATH")
|
|
333
|
-
class CypherLiteLoadTests(unittest.TestCase):
|
|
334
|
-
def test_validator_cross_check_passes_on_template(self):
|
|
335
|
-
# main() should exit 0 — stdlib checks pass AND cypherlite loads it.
|
|
336
|
-
res = subprocess.run(
|
|
337
|
-
["python3", str(PLUGIN_ROOT / "scripts" / "validate_format.py"), str(TEMPLATE)],
|
|
338
|
-
capture_output=True, text=True,
|
|
339
|
-
)
|
|
340
|
-
self.assertEqual(res.returncode, 0, res.stderr)
|
|
341
|
-
self.assertIn("valid:", res.stdout)
|
|
342
|
-
|
|
343
|
-
def test_validator_cross_check_can_be_disabled(self):
|
|
344
|
-
res = subprocess.run(
|
|
345
|
-
["python3", str(PLUGIN_ROOT / "scripts" / "validate_format.py"),
|
|
346
|
-
"--no-cypherlite", str(TEMPLATE)],
|
|
347
|
-
capture_output=True, text=True,
|
|
348
|
-
)
|
|
349
|
-
self.assertEqual(res.returncode, 0, res.stderr)
|
|
350
|
-
self.assertIn("valid:", res.stdout)
|
|
351
|
-
|
|
352
|
-
def test_template_loads_and_queries(self):
|
|
353
|
-
g = load_graph(str(TEMPLATE))
|
|
354
|
-
q = "MATCH (n:GbuildNode) RETURN n.slug AS slug ORDER BY slug"
|
|
355
|
-
res = cypherlite_query(TEMPLATE, q)
|
|
356
|
-
self.assertEqual(res.returncode, 0, res.stderr)
|
|
357
|
-
rows = json.loads(res.stdout)
|
|
358
|
-
slugs = [r["slug"] for r in rows]
|
|
359
|
-
self.assertEqual(slugs, [
|
|
360
|
-
"a-produce-shared-value", "b-consume-doubled",
|
|
361
|
-
"c-consume-squared", "d-join-and-sum",
|
|
362
|
-
])
|
|
363
|
-
|
|
364
|
-
def test_sibling_nodes_dir_does_not_collide(self):
|
|
365
|
-
# The plan lives in <feature>/db/ and checkpoints in <feature>/nodes/.
|
|
366
|
-
# CypherLite treats a directory named `nodes` inside the opened store as
|
|
367
|
-
# Parquet shards, so the checkpoint dir must stay outside `db/`.
|
|
368
|
-
with tempfile.TemporaryDirectory() as root:
|
|
369
|
-
feature = Path(root) / ".gbuild" / "example-diamond"
|
|
370
|
-
db = feature / "db"
|
|
371
|
-
nodes = feature / "nodes"
|
|
372
|
-
db.mkdir(parents=True)
|
|
373
|
-
nodes.mkdir(parents=True)
|
|
374
|
-
shutil.copyfile(TEMPLATE, db / "graph.json")
|
|
375
|
-
(nodes / "a-produce-shared-value.json").write_text('{"status": "completed"}')
|
|
376
|
-
res = subprocess.run(
|
|
377
|
-
[CYPHERLITE_BIN, str(db), "-json", "MATCH (n) RETURN count(n) AS c"],
|
|
378
|
-
capture_output=True, text=True,
|
|
379
|
-
)
|
|
380
|
-
self.assertEqual(res.returncode, 0, res.stderr)
|
|
381
|
-
self.assertEqual(json.loads(res.stdout)[0]["c"], 7)
|
|
382
|
-
|
|
383
|
-
def test_template_transitive_deps(self):
|
|
384
|
-
q = ("MATCH (n {slug:'d-join-and-sum'})-[:DEPENDS_ON*]->(d) "
|
|
385
|
-
"RETURN DISTINCT d.slug AS slug ORDER BY slug")
|
|
386
|
-
res = cypherlite_query(TEMPLATE, q)
|
|
387
|
-
self.assertEqual(res.returncode, 0, res.stderr)
|
|
388
|
-
rows = json.loads(res.stdout)
|
|
389
|
-
self.assertEqual([r["slug"] for r in rows], [
|
|
390
|
-
"a-produce-shared-value", "b-consume-doubled", "c-consume-squared",
|
|
391
|
-
])
|
|
392
|
-
|
|
393
|
-
def test_template_acceptance_query(self):
|
|
394
|
-
q = ("MATCH (f:Feature)-[:HAS_ACCEPTANCE]->(a:Acceptance) "
|
|
395
|
-
"RETURN a.id AS id ORDER BY id")
|
|
396
|
-
res = cypherlite_query(TEMPLATE, q)
|
|
397
|
-
self.assertEqual(res.returncode, 0, res.stderr)
|
|
398
|
-
rows = json.loads(res.stdout)
|
|
399
|
-
self.assertEqual([r["id"] for r in rows], ["a-1", "a-2"])
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
if __name__ == "__main__":
|
|
403
|
-
unittest.main()
|