Skip to content

Commit ce96ecc

Browse files
committed
Add comprehensive smoke test: version + index + search + trace + folder integrity
1 parent 18fa997 commit ce96ecc

3 files changed

Lines changed: 256 additions & 23 deletions

File tree

.github/workflows/dry-run.yml

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,30 @@ permissions:
77
contents: read
88

99
jobs:
10-
test-unix:
10+
lint:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Install deps
16+
run: |
17+
sudo apt-get update && sudo apt-get install -y libsqlite3-dev zlib1g-dev cppcheck
18+
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
19+
sudo add-apt-repository -y "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-18 main"
20+
sudo apt-get update && sudo apt-get install -y clang-tidy-18 clang-format-18
21+
sudo ln -sf /usr/bin/clang-tidy-18 /usr/bin/clang-tidy
22+
sudo ln -sf /usr/bin/clang-format-18 /usr/bin/clang-format
23+
24+
- name: clang-format
25+
run: make -f Makefile.cbm lint-format
26+
27+
- name: cppcheck
28+
run: make -f Makefile.cbm lint-cppcheck
29+
30+
- name: clang-tidy
31+
run: make -f Makefile.cbm lint-tidy
32+
33+
test:
1134
strategy:
1235
matrix:
1336
include:
@@ -30,28 +53,64 @@ jobs:
3053
- name: Run C tests (ASan + UBSan)
3154
run: make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu) -f Makefile.cbm test
3255

56+
build-unix:
57+
needs: [lint, test]
58+
strategy:
59+
matrix:
60+
include:
61+
- os: ubuntu-latest
62+
goos: linux
63+
goarch: amd64
64+
- os: ubuntu-24.04-arm
65+
goos: linux
66+
goarch: arm64
67+
- os: macos-14
68+
goos: darwin
69+
goarch: arm64
70+
- os: macos-15-intel
71+
goos: darwin
72+
goarch: amd64
73+
variant: [standard, ui]
74+
runs-on: ${{ matrix.os }}
75+
steps:
76+
- uses: actions/checkout@v4
77+
78+
- name: Install deps (Ubuntu)
79+
if: startsWith(matrix.os, 'ubuntu')
80+
run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev zlib1g-dev
81+
3382
- name: Build standard binary
83+
if: matrix.variant == 'standard'
3484
run: make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu) -f Makefile.cbm cbm
3585

36-
- name: Smoke test (standard)
37-
run: |
38-
./build/c/codebase-memory-mcp --version
39-
echo '{}' | timeout 3 ./build/c/codebase-memory-mcp 2>&1 || true
40-
echo "Smoke test passed"
41-
4286
- uses: actions/setup-node@v4
87+
if: matrix.variant == 'ui'
4388
with:
4489
node-version: "22"
4590

4691
- name: Build UI binary
92+
if: matrix.variant == 'ui'
4793
run: make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu) -f Makefile.cbm cbm-with-ui
4894

49-
- name: Smoke test (UI)
95+
- name: Smoke test
96+
run: scripts/smoke-test.sh ./build/c/codebase-memory-mcp
97+
98+
- name: Archive binary
5099
run: |
51-
./build/c/codebase-memory-mcp --version
52-
echo "UI binary smoke test passed"
100+
SUFFIX=${{ matrix.variant == 'ui' && '-ui' || '' }}
101+
tar -czf codebase-memory-mcp${SUFFIX}-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz \
102+
-C build/c codebase-memory-mcp
53103
54-
test-windows:
104+
- uses: actions/upload-artifact@v4
105+
with:
106+
name: ${{ matrix.variant }}-${{ matrix.goos }}-${{ matrix.goarch }}
107+
path: "*.tar.gz"
108+
109+
build-windows:
110+
needs: [lint, test]
111+
strategy:
112+
matrix:
113+
variant: [standard, ui]
55114
runs-on: windows-latest
56115
steps:
57116
- uses: actions/checkout@v4
@@ -64,12 +123,36 @@ jobs:
64123
mingw-w64-ucrt-x86_64-gcc
65124
mingw-w64-ucrt-x86_64-sqlite3
66125
mingw-w64-ucrt-x86_64-zlib
126+
mingw-w64-ucrt-x86_64-python3
67127
make
68128
69129
- name: Build standard binary
130+
if: matrix.variant == 'standard'
70131
shell: msys2 {0}
71132
run: make -j$(nproc) -f Makefile.cbm cbm
72133

134+
- uses: actions/setup-node@v4
135+
if: matrix.variant == 'ui'
136+
with:
137+
node-version: "22"
138+
139+
- name: Build UI binary
140+
if: matrix.variant == 'ui'
141+
shell: msys2 {0}
142+
run: make -j$(nproc) -f Makefile.cbm cbm-with-ui
143+
73144
- name: Smoke test
74145
shell: msys2 {0}
75-
run: ./build/c/codebase-memory-mcp.exe --version || ./build/c/codebase-memory-mcp --version
146+
run: scripts/smoke-test.sh ./build/c/codebase-memory-mcp
147+
148+
- name: Archive binary
149+
shell: pwsh
150+
run: |
151+
$suffix = if ("${{ matrix.variant }}" -eq "ui") { "-ui" } else { "" }
152+
Copy-Item build/c/codebase-memory-mcp -Destination codebase-memory-mcp.exe
153+
Compress-Archive -Path codebase-memory-mcp.exe -DestinationPath "codebase-memory-mcp${suffix}-windows-amd64.zip"
154+
155+
- uses: actions/upload-artifact@v4
156+
with:
157+
name: ${{ matrix.variant }}-windows-amd64
158+
path: "*.zip"

.github/workflows/release.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ jobs:
5858
CFLAGS_EXTRA="-DCBM_VERSION=\"\\\"$CLEAN_VERSION\\\"\""
5959
6060
- name: Smoke test (standard)
61-
run: ./build/c/codebase-memory-mcp --version
61+
run: scripts/smoke-test.sh ./build/c/codebase-memory-mcp
6262

6363
- name: Archive standard binary
6464
run: |
@@ -79,7 +79,7 @@ jobs:
7979
CFLAGS_EXTRA="-DCBM_VERSION=\"\\\"$CLEAN_VERSION\\\"\""
8080
8181
- name: Smoke test (UI)
82-
run: ./build/c/codebase-memory-mcp --version
82+
run: scripts/smoke-test.sh ./build/c/codebase-memory-mcp
8383

8484
- name: Archive UI binary
8585
run: |
@@ -104,6 +104,7 @@ jobs:
104104
mingw-w64-ucrt-x86_64-gcc
105105
mingw-w64-ucrt-x86_64-sqlite3
106106
mingw-w64-ucrt-x86_64-zlib
107+
mingw-w64-ucrt-x86_64-python3
107108
make
108109
109110
# ── Standard binary ──
@@ -116,9 +117,9 @@ jobs:
116117
make -j$(nproc) -f Makefile.cbm cbm \
117118
CFLAGS_EXTRA="-DCBM_VERSION=\"\\\"$CLEAN_VERSION\\\"\""
118119
119-
- name: Smoke test
120+
- name: Smoke test (standard)
120121
shell: msys2 {0}
121-
run: ./build/c/codebase-memory-mcp --version
122+
run: scripts/smoke-test.sh ./build/c/codebase-memory-mcp
122123

123124
- name: Archive standard binary
124125
shell: pwsh
@@ -140,6 +141,10 @@ jobs:
140141
make -j$(nproc) -f Makefile.cbm cbm-with-ui \
141142
CFLAGS_EXTRA="-DCBM_VERSION=\"\\\"$CLEAN_VERSION\\\"\""
142143
144+
- name: Smoke test (UI)
145+
shell: msys2 {0}
146+
run: scripts/smoke-test.sh ./build/c/codebase-memory-mcp
147+
143148
- name: Archive UI binary
144149
shell: pwsh
145150
run: |

scripts/smoke-test.sh

Lines changed: 152 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,163 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
33

4-
# Smoke test: verify the binary starts and reports its version.
4+
# Smoke test: verify the binary is fully operational.
5+
#
6+
# Phase 1: --version output
7+
# Phase 2: Index a small multi-language project
8+
# Phase 3: Verify node/edge counts, search, and trace
9+
#
10+
# Usage: smoke-test.sh <binary-path>
11+
512
BINARY="${1:?usage: smoke-test.sh <binary-path>}"
13+
TMPDIR=$(mktemp -d)
14+
trap 'rm -rf "$TMPDIR"' EXIT
15+
16+
cli() { "$BINARY" cli "$@" 2>/dev/null; }
617

7-
echo "=== smoke-test: --version ==="
18+
echo "=== Phase 1: version ==="
819
OUTPUT=$("$BINARY" --version 2>&1)
920
echo "$OUTPUT"
10-
11-
if echo "$OUTPUT" | grep -qE 'v?[0-9]+\.[0-9]+|dev'; then
12-
echo "OK: version output looks valid"
13-
else
21+
if ! echo "$OUTPUT" | grep -qE 'v?[0-9]+\.[0-9]+|dev'; then
1422
echo "FAIL: unexpected version output"
1523
exit 1
1624
fi
25+
echo "OK"
26+
27+
echo ""
28+
echo "=== Phase 2: index test project ==="
29+
30+
# Create a small multi-language project (Python + Go + JS)
31+
mkdir -p "$TMPDIR/src/pkg"
32+
33+
cat > "$TMPDIR/src/main.py" << 'PYEOF'
34+
from pkg import helper
35+
36+
def main():
37+
result = helper.compute(42)
38+
print(result)
39+
40+
class Config:
41+
DEBUG = True
42+
PORT = 8080
43+
PYEOF
44+
45+
cat > "$TMPDIR/src/pkg/__init__.py" << 'PYEOF'
46+
from .helper import compute
47+
PYEOF
48+
49+
cat > "$TMPDIR/src/pkg/helper.py" << 'PYEOF'
50+
def compute(x):
51+
return x * 2
52+
53+
def validate(data):
54+
if not data:
55+
raise ValueError("empty")
56+
return True
57+
PYEOF
58+
59+
cat > "$TMPDIR/src/server.go" << 'GOEOF'
60+
package main
61+
62+
import "fmt"
63+
64+
func StartServer(port int) {
65+
fmt.Printf("listening on :%d\n", port)
66+
}
67+
68+
func HandleRequest(path string) string {
69+
return "ok: " + path
70+
}
71+
GOEOF
72+
73+
cat > "$TMPDIR/src/app.js" << 'JSEOF'
74+
function render(data) {
75+
return `<div>${data}</div>`;
76+
}
77+
78+
function fetchData(url) {
79+
return fetch(url).then(r => r.json());
80+
}
81+
82+
module.exports = { render, fetchData };
83+
JSEOF
84+
85+
cat > "$TMPDIR/config.yaml" << 'YAMLEOF'
86+
server:
87+
port: 8080
88+
debug: true
89+
database:
90+
host: localhost
91+
YAMLEOF
92+
93+
# Index
94+
RESULT=$(cli index_repository "{\"repo_path\":\"$TMPDIR\"}")
95+
echo "$RESULT"
96+
97+
STATUS=$(echo "$RESULT" | python3 -c "import json,sys; d=json.loads(json.loads(sys.stdin.read())['content'][0]['text']); print(d.get('status',''))" 2>/dev/null || echo "")
98+
if [ "$STATUS" != "indexed" ]; then
99+
echo "FAIL: index status is '$STATUS', expected 'indexed'"
100+
exit 1
101+
fi
102+
103+
NODES=$(echo "$RESULT" | python3 -c "import json,sys; d=json.loads(json.loads(sys.stdin.read())['content'][0]['text']); print(d.get('nodes',0))" 2>/dev/null || echo "0")
104+
EDGES=$(echo "$RESULT" | python3 -c "import json,sys; d=json.loads(json.loads(sys.stdin.read())['content'][0]['text']); print(d.get('edges',0))" 2>/dev/null || echo "0")
105+
106+
echo "nodes=$NODES edges=$EDGES"
107+
108+
if [ "$NODES" -lt 10 ]; then
109+
echo "FAIL: expected at least 10 nodes, got $NODES"
110+
exit 1
111+
fi
112+
if [ "$EDGES" -lt 5 ]; then
113+
echo "FAIL: expected at least 5 edges, got $EDGES"
114+
exit 1
115+
fi
116+
echo "OK: $NODES nodes, $EDGES edges"
117+
118+
echo ""
119+
echo "=== Phase 3: verify queries ==="
120+
121+
# 3a: search_graph — find the compute function
122+
PROJECT=$(echo "$RESULT" | python3 -c "import json,sys; d=json.loads(json.loads(sys.stdin.read())['content'][0]['text']); print(d.get('project',''))" 2>/dev/null || echo "")
123+
124+
SEARCH=$(cli search_graph "{\"project\":\"$PROJECT\",\"name_pattern\":\"compute\"}")
125+
TOTAL=$(echo "$SEARCH" | python3 -c "import json,sys; d=json.loads(json.loads(sys.stdin.read())['content'][0]['text']); print(d.get('total',0))" 2>/dev/null || echo "0")
126+
if [ "$TOTAL" -lt 1 ]; then
127+
echo "FAIL: search_graph for 'compute' returned 0 results"
128+
exit 1
129+
fi
130+
echo "OK: search_graph found $TOTAL result(s) for 'compute'"
131+
132+
# 3b: trace_call_path — verify compute has callers
133+
TRACE=$(cli trace_call_path "{\"project\":\"$PROJECT\",\"function_name\":\"compute\",\"direction\":\"inbound\",\"max_depth\":1}")
134+
CALLERS=$(echo "$TRACE" | python3 -c "import json,sys; d=json.loads(json.loads(sys.stdin.read())['content'][0]['text']); print(len(d.get('callers',[])))" 2>/dev/null || echo "0")
135+
if [ "$CALLERS" -lt 1 ]; then
136+
echo "FAIL: trace_call_path found 0 callers for 'compute'"
137+
exit 1
138+
fi
139+
echo "OK: trace_call_path found $CALLERS caller(s) for 'compute'"
140+
141+
# 3c: get_graph_schema — verify labels exist
142+
SCHEMA=$(cli get_graph_schema "{\"project\":\"$PROJECT\"}")
143+
LABELS=$(echo "$SCHEMA" | python3 -c "import json,sys; d=json.loads(json.loads(sys.stdin.read())['content'][0]['text']); print(len(d.get('node_labels',[])))" 2>/dev/null || echo "0")
144+
if [ "$LABELS" -lt 3 ]; then
145+
echo "FAIL: schema has fewer than 3 node labels"
146+
exit 1
147+
fi
148+
echo "OK: schema has $LABELS node labels"
149+
150+
# 3d: Verify __init__.py didn't clobber Folder node
151+
FOLDERS=$(cli search_graph "{\"project\":\"$PROJECT\",\"label\":\"Folder\"}")
152+
FOLDER_COUNT=$(echo "$FOLDERS" | python3 -c "import json,sys; d=json.loads(json.loads(sys.stdin.read())['content'][0]['text']); print(d.get('total',0))" 2>/dev/null || echo "0")
153+
if [ "$FOLDER_COUNT" -lt 2 ]; then
154+
echo "FAIL: expected at least 2 Folder nodes (src, src/pkg), got $FOLDER_COUNT"
155+
exit 1
156+
fi
157+
echo "OK: $FOLDER_COUNT Folder nodes (init.py didn't clobber them)"
158+
159+
# 3e: delete_project cleanup
160+
cli delete_project "{\"project_name\":\"$PROJECT\"}" > /dev/null
17161

18-
echo "=== smoke-test: passed ==="
162+
echo ""
163+
echo "=== smoke-test: ALL PASSED ==="

0 commit comments

Comments
 (0)