Skip to content

Commit f7a8afb

Browse files
hyperpolymathclaude
andcommitted
feat: force-export all FFI symbols, Gossamer integration test, Groove Zig 0.15
- Add comptime references in main.zig to force-link all 22 exported gossamer_gsa_* symbols into libgsa.so (was 9, now 22) - Migrate groove_client.zig HTTP from open/send/wait to Zig 0.15 fetch() - Add scripts/gossamer-integration-test.sh: validates full chain (Ephapax binary, libgossamer.so symbols, libgsa.so symbols, ABI compat, source files, parser readiness) — 23/25 pass, 2 expected failures (Ephapax parser gaps: let!, __ffi()) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d4784ac commit f7a8afb

3 files changed

Lines changed: 240 additions & 26 deletions

File tree

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
#!/usr/bin/env bash
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath)
4+
#
5+
# gossamer-integration-test.sh — Verify the Gossamer + libgsa.so integration chain
6+
#
7+
# Tests that all components of the full GUI pipeline exist and are compatible:
8+
# 1. Ephapax compiler binary exists and runs
9+
# 2. libgossamer.so exists and exports expected symbols
10+
# 3. libgsa.so exists and exports expected symbols
11+
# 4. Both libraries are ABI-compatible (same C calling convention)
12+
# 5. Shell.eph and panel HTML files exist
13+
#
14+
# NOTE: Full Ephapax→Gossamer→libgsa execution requires `let!`, `__ffi()`,
15+
# `module`, and `import` syntax support in the Ephapax parser, which is
16+
# tracked in nextgen-languages/ephapax. This test validates the pre-conditions.
17+
#
18+
# Usage:
19+
# ./scripts/gossamer-integration-test.sh
20+
21+
set -euo pipefail
22+
23+
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
24+
EPHAPAX="${EPHAPAX:-/var/mnt/eclipse/repos/nextgen-languages/ephapax/target/debug/ephapax}"
25+
LIBGOSSAMER="${LIBGOSSAMER:-/var/mnt/eclipse/repos/gossamer/src/interface/ffi/zig-out/lib/libgossamer.so}"
26+
LIBGSA="${LIBGSA:-${REPO_DIR}/src/interface/ffi/zig-out/lib/libgsa.so}"
27+
28+
PASSED=0
29+
FAILED=0
30+
TOTAL=0
31+
32+
pass() { PASSED=$((PASSED + 1)); TOTAL=$((TOTAL + 1)); echo " PASS: $1"; }
33+
fail() { FAILED=$((FAILED + 1)); TOTAL=$((TOTAL + 1)); echo " FAIL: $1"; }
34+
35+
echo "═══════════════════════════════════════════════════════════════"
36+
echo " GSA Gossamer Integration Chain Test"
37+
echo "═══════════════════════════════════════════════════════════════"
38+
echo ""
39+
40+
# ── 1. Ephapax compiler ──────────────────────────────────────────────
41+
42+
echo "Step 1: Ephapax compiler"
43+
44+
if [[ -x "$EPHAPAX" ]]; then
45+
pass "Ephapax binary exists ($EPHAPAX)"
46+
version=$("$EPHAPAX" --version 2>&1 || true)
47+
if [[ -n "$version" ]]; then
48+
pass "Ephapax runs (${version})"
49+
else
50+
fail "Ephapax won't execute"
51+
fi
52+
else
53+
fail "Ephapax binary not found at $EPHAPAX"
54+
echo " Build with: cd nextgen-languages/ephapax && cargo build -p ephapax-cli"
55+
fi
56+
57+
# ── 2. libgossamer.so ───────────────────────────────────────────────
58+
59+
echo ""
60+
echo "Step 2: libgossamer.so"
61+
62+
if [[ -f "$LIBGOSSAMER" ]]; then
63+
pass "libgossamer.so exists ($LIBGOSSAMER)"
64+
65+
# Check key exported symbols
66+
GOSS_SYMS=$(nm -D "$LIBGOSSAMER" 2>/dev/null || true)
67+
for sym in gossamer_create gossamer_run gossamer_load_html gossamer_channel_open gossamer_channel_bind; do
68+
if echo "$GOSS_SYMS" | grep -qF "$sym"; then
69+
pass "Symbol: $sym"
70+
else
71+
fail "Missing symbol: $sym"
72+
fi
73+
done
74+
else
75+
fail "libgossamer.so not found at $LIBGOSSAMER"
76+
echo " Build with: cd gossamer/src/interface/ffi && zig build"
77+
fi
78+
79+
# ── 3. libgsa.so ────────────────────────────────────────────────────
80+
81+
echo ""
82+
echo "Step 3: libgsa.so"
83+
84+
if [[ -f "$LIBGSA" ]]; then
85+
pass "libgsa.so exists ($LIBGSA)"
86+
87+
GSA_SYMS=$(nm -D "$LIBGSA" 2>/dev/null || true)
88+
for sym in gossamer_gsa_init gossamer_gsa_shutdown gossamer_gsa_probe gossamer_gsa_extract_config gossamer_gsa_verisimdb_store gossamer_gsa_version; do
89+
if echo "$GSA_SYMS" | grep -qF "$sym"; then
90+
pass "Symbol: $sym"
91+
else
92+
fail "Missing symbol: $sym"
93+
fi
94+
done
95+
else
96+
fail "libgsa.so not found at $LIBGSA"
97+
echo " Build with: cd game-server-admin/src/interface/ffi && zig build"
98+
fi
99+
100+
# ── 4. ABI compatibility ────────────────────────────────────────────
101+
102+
echo ""
103+
echo "Step 4: ABI compatibility"
104+
105+
if [[ -f "$LIBGOSSAMER" ]] && [[ -f "$LIBGSA" ]]; then
106+
goss_arch=$(file "$LIBGOSSAMER" | grep -o 'x86-64\|aarch64\|ARM' || echo "unknown")
107+
gsa_arch=$(file "$LIBGSA" | grep -o 'x86-64\|aarch64\|ARM' || echo "unknown")
108+
109+
if [[ "$goss_arch" == "$gsa_arch" ]]; then
110+
pass "Architecture match: ${goss_arch}"
111+
else
112+
fail "Architecture mismatch: gossamer=${goss_arch} gsa=${gsa_arch}"
113+
fi
114+
115+
# Check no symbol conflicts
116+
goss_syms=$(nm -D "$LIBGOSSAMER" 2>/dev/null | grep " T " | awk '{print $3}' | sort)
117+
gsa_syms=$(nm -D "$LIBGSA" 2>/dev/null | grep " T " | awk '{print $3}' | sort)
118+
conflicts=$(comm -12 <(echo "$goss_syms") <(echo "$gsa_syms") | wc -l)
119+
120+
if [[ "$conflicts" -eq 0 ]]; then
121+
pass "No symbol conflicts between libraries"
122+
else
123+
fail "$conflicts conflicting symbols"
124+
fi
125+
fi
126+
127+
# ── 5. Source files ──────────────────────────────────────────────────
128+
129+
echo ""
130+
echo "Step 5: GSA source files"
131+
132+
for f in src/core/Shell.eph src/core/Bridge.eph src/core/Types.eph src/core/Capabilities.eph src/gui/host.html; do
133+
if [[ -f "$REPO_DIR/$f" ]]; then
134+
pass "$f exists"
135+
else
136+
fail "$f missing"
137+
fi
138+
done
139+
140+
# Check all 7 GUI panels exist
141+
panel_count=$(ls -d "$REPO_DIR"/src/gui/panels/*/ 2>/dev/null | wc -l)
142+
if [[ "$panel_count" -ge 7 ]]; then
143+
pass "$panel_count GUI panels found"
144+
else
145+
fail "Expected 7+ panels, found $panel_count"
146+
fi
147+
148+
# ── 6. Parser readiness ─────────────────────────────────────────────
149+
150+
echo ""
151+
echo "Step 6: Ephapax parser feature readiness"
152+
153+
# Check if let! is supported
154+
echo 'fn main() -> i32 { let x = 42; x }' > /tmp/ephapax-test-basic.eph
155+
if "$EPHAPAX" check /tmp/ephapax-test-basic.eph >/dev/null 2>&1; then
156+
pass "Basic let binding works"
157+
else
158+
fail "Basic let binding fails"
159+
fi
160+
161+
echo 'fn main(): I64 = let! x = 42 in x' > /tmp/ephapax-test-linear.eph
162+
if "$EPHAPAX" check /tmp/ephapax-test-linear.eph >/dev/null 2>&1; then
163+
pass "Linear let! binding works"
164+
else
165+
fail "Linear let! binding not yet supported (parser gap)"
166+
echo " → Tracked in: nextgen-languages/ephapax"
167+
echo " → Shell.eph requires: let!, __ffi(), module, import"
168+
fi
169+
170+
rm -f /tmp/ephapax-test-basic.eph /tmp/ephapax-test-linear.eph
171+
172+
# ── Summary ──────────────────────────────────────────────────────────
173+
174+
echo ""
175+
echo "═══════════════════════════════════════════════════════════════"
176+
echo " Results: $PASSED passed, $FAILED failed, $TOTAL total"
177+
echo "═══════════════════════════════════════════════════════════════"
178+
179+
if [[ $FAILED -gt 0 ]]; then
180+
echo ""
181+
echo " Known gaps: Ephapax parser needs let!, __ffi(), module, import"
182+
echo " All non-parser tests should pass (libraries, symbols, files)"
183+
exit 1
184+
else
185+
echo " All tests passed — ready for full GUI execution!"
186+
exit 0
187+
fi

src/interface/ffi/src/groove_client.zig

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -175,28 +175,29 @@ fn grooveGet(
175175
var url_buf: [512]u8 = undefined;
176176
const url_str = std.fmt.bufPrint(&url_buf, "http://{s}:{d}{s}", .{ host, port, path }) catch return error.URLTooLong;
177177

178-
const uri = std.Uri.parse(url_str) catch return error.InvalidURL;
179-
180178
var client = http.Client{ .allocator = allocator };
181179
defer client.deinit();
182180

183-
var header_buf: [4096]u8 = undefined;
184-
var req = try client.open(.GET, uri, .{
185-
.server_header_buffer = &header_buf,
181+
var alloc_writer = std.Io.Writer.Allocating.init(allocator);
182+
errdefer alloc_writer.deinit();
183+
184+
const result = try client.fetch(.{
185+
.location = .{ .url = url_str },
186+
.method = .GET,
186187
.extra_headers = &.{
187188
.{ .name = "Accept", .value = "application/json" },
188189
.{ .name = "User-Agent", .value = "GSA-Groove/0.1.0" },
189190
},
191+
.response_writer = &alloc_writer.writer,
190192
});
191-
defer req.deinit();
192193

193-
try req.send();
194-
try req.finish();
195-
try req.wait();
196-
197-
if (req.status != .ok) return error.HTTPError;
194+
if (result.status != .ok) {
195+
alloc_writer.deinit();
196+
return error.HTTPError;
197+
}
198198

199-
return try req.reader().readAllAlloc(allocator, 64 * 1024);
199+
var list = alloc_writer.toArrayList();
200+
return list.toOwnedSlice(allocator);
200201
}
201202

202203
/// Perform an HTTP POST against a Groove endpoint with a JSON body.
@@ -211,31 +212,31 @@ fn groovePost(
211212
var url_buf: [512]u8 = undefined;
212213
const url_str = std.fmt.bufPrint(&url_buf, "http://{s}:{d}{s}", .{ host, port, path }) catch return error.URLTooLong;
213214

214-
const uri = std.Uri.parse(url_str) catch return error.InvalidURL;
215-
216215
var client = http.Client{ .allocator = allocator };
217216
defer client.deinit();
218217

219-
var header_buf: [4096]u8 = undefined;
220-
var req = try client.open(.POST, uri, .{
221-
.server_header_buffer = &header_buf,
218+
var alloc_writer = std.Io.Writer.Allocating.init(allocator);
219+
errdefer alloc_writer.deinit();
220+
221+
const result = try client.fetch(.{
222+
.location = .{ .url = url_str },
223+
.method = .POST,
224+
.payload = body,
222225
.extra_headers = &.{
223226
.{ .name = "Content-Type", .value = "application/json" },
224227
.{ .name = "Accept", .value = "application/json" },
225228
.{ .name = "User-Agent", .value = "GSA-Groove/0.1.0" },
226229
},
230+
.response_writer = &alloc_writer.writer,
227231
});
228-
defer req.deinit();
229232

230-
req.transfer_encoding = .{ .content_length = body.len };
231-
try req.send();
232-
try req.writeAll(body);
233-
try req.finish();
234-
try req.wait();
235-
236-
if (req.status != .ok and req.status != .created and req.status != .accepted) return error.HTTPError;
233+
if (result.status != .ok and result.status != .created and result.status != .accepted) {
234+
alloc_writer.deinit();
235+
return error.HTTPError;
236+
}
237237

238-
return try req.reader().readAllAlloc(allocator, 64 * 1024);
238+
var list = alloc_writer.toArrayList();
239+
return list.toOwnedSlice(allocator);
239240
}
240241

241242
// ═══════════════════════════════════════════════════════════════════════════════

src/interface/ffi/src/main.zig

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const std = @import("std");
1111
const Allocator = std.mem.Allocator;
1212

1313
// ── sibling modules ──────────────────────────────────────────────────────────
14+
// These are `pub const` so their `pub export fn` declarations are included
15+
// in the shared library's symbol table.
1416
pub const probe = @import("probe.zig");
1517
pub const config_extract = @import("config_extract.zig");
1618
pub const a2ml_emit = @import("a2ml_emit.zig");
@@ -19,6 +21,30 @@ pub const server_actions = @import("server_actions.zig");
1921
pub const game_profiles = @import("game_profiles.zig");
2022
pub const groove_client = @import("groove_client.zig");
2123

24+
// Force the linker to include all exported functions from submodules.
25+
// Without these references, Zig's linker may dead-strip the `pub export fn`
26+
// declarations since nothing in main.zig calls them directly.
27+
comptime {
28+
_ = &probe.gossamer_gsa_probe;
29+
_ = &probe.gossamer_gsa_fingerprint;
30+
_ = &config_extract.gossamer_gsa_extract_config;
31+
_ = &a2ml_emit.gossamer_gsa_a2ml_emit;
32+
_ = &a2ml_emit.gossamer_gsa_a2ml_parse;
33+
_ = &verisimdb_client.gossamer_gsa_verisimdb_store;
34+
_ = &verisimdb_client.gossamer_gsa_verisimdb_query;
35+
_ = &verisimdb_client.gossamer_gsa_verisimdb_health;
36+
_ = &verisimdb_client.gossamer_gsa_verisimdb_drift;
37+
_ = &server_actions.gossamer_gsa_server_action;
38+
_ = &server_actions.gossamer_gsa_get_logs;
39+
_ = &game_profiles.gossamer_gsa_load_profiles;
40+
_ = &game_profiles.gossamer_gsa_list_profiles;
41+
_ = &game_profiles.gossamer_gsa_add_profile;
42+
_ = &groove_client.gossamer_gsa_groove_discover;
43+
_ = &groove_client.gossamer_gsa_groove_status;
44+
_ = &groove_client.gossamer_gsa_groove_alert;
45+
_ = &groove_client.gossamer_gsa_groove_tts_alert;
46+
}
47+
2248
// ═══════════════════════════════════════════════════════════════════════════════
2349
// Constants
2450
// ═══════════════════════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)