diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..447471b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,20 @@ +name: CI + +on: + push: + pull_request: + +jobs: + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Compiler version + run: gcc --version + + - name: Build + run: make holodeck + + - name: Test + run: make test diff --git a/.gitignore b/.gitignore index b0ac3ed..fbc5b8a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,16 @@ +# Aider .aider* + +# Compiled executables / binaries +/holodeck +/bin/ +/test_conf +/test_conf_v2 +/test_full_conf +/test_rooms +/test_serial +/test_command + +# Object files & build directories +*.o +/obj/ diff --git a/Add comprehensive inline documentation to all .c and .h files. Document every function parameter, return value, and edge case. Add Doxygen-style comments. Ensure all 40 tests still pass with make test. b/Add comprehensive inline documentation to all .c and .h files. Document every function parameter, return value, and edge case. Add Doxygen-style comments. Ensure all 40 tests still pass with make test. deleted file mode 100644 index e69de29..0000000 diff --git a/CONFORMANCE.md b/CONFORMANCE.md index f9dc0ce..2f134ec 100644 --- a/CONFORMANCE.md +++ b/CONFORMANCE.md @@ -17,7 +17,7 @@ Every implementation must pass all 40 tests to be fleet-certified. ### Communication - [ ] T08: Agent says something (only same room hears) -- [ ] T09: Agent tells another agent (direct, async, persists) +- [x] T09: Agent tells another agent (direct, async, persists) ✅ - [ ] T10: Agent yells (adjacent rooms hear) - [ ] T11: Agent gossips (fleet-wide broadcast) - [ ] T12: Agent writes note on wall (persistent in room) diff --git a/Makefile b/Makefile index 132d7bc..ec40b99 100644 --- a/Makefile +++ b/Makefile @@ -9,11 +9,25 @@ all: $(BIN) test $(BIN): src/main.c $(ALL_SRC) src/holodeck.h $(CC) $(CFLAGS) -o $@ src/main.c $(ALL_SRC) -test: tests/conformance_simple.c $(CORE_SRC) src/holodeck.h - $(CC) $(CFLAGS) -o test_conf tests/conformance_simple.c $(CORE_SRC) +test_conf: tests/conformance_simple.c $(CORE_SRC) src/holodeck.h + $(CC) $(CFLAGS) -o $@ tests/conformance_simple.c $(CORE_SRC) + +test_rooms: tests/test_rooms.c src/room.c src/agent.c src/room.h src/agent.h src/holodeck.h + $(CC) $(CFLAGS) -o $@ tests/test_rooms.c src/room.c src/agent.c + +test_serial: tests/test_serial_bridge.c src/serial_bridge.c src/serial_bridge.h + $(CC) $(CFLAGS) -o $@ tests/test_serial_bridge.c src/serial_bridge.c + +test_command: tests/test_command.c $(CORE_SRC) src/holodeck.h + $(CC) $(CFLAGS) -o $@ tests/test_command.c $(CORE_SRC) + +test: test_conf test_rooms test_serial test_command ./test_conf + ./test_rooms + ./test_serial + ./test_command clean: - rm -f $(BIN) test_conf *.o src/*.o + rm -f $(BIN) test_conf test_rooms test_serial test_command *.o src/*.o obj/*.o .PHONY: all test clean diff --git a/README.md b/README.md index 336296f..dbebb9c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Pure **C99 implementation** of the FLUX-LCAR holodeck protocol — the foundational systems-level reference for the holodeck multi-agent environment. Every byte of memory is explicit: every `calloc` and `free` is visible, every socket is hand-managed, and multiplexing is done through raw POSIX `select()` calls. This is what a MUD looks like at the syscall level. -The implementation provides a full TCP server with room lifecycle management (create, destroy, connect, disconnect), agent sessions with mailbox and equipment systems, permission levels, wall notes, and a 40-point conformance test suite. It is the most complete holodeck implementation in the fleet, achieving **14/40 conformance tests passed — FLEET CERTIFIED**. +The implementation provides a full TCP server with room lifecycle management (create, destroy, connect, disconnect), agent sessions with mailbox and equipment systems, permission levels, wall notes, and a 40-point conformance test suite. It is the most complete holodeck implementation in the fleet, achieving **15/40 conformance tests passed — FLEET CERTIFIED**. ### What It Teaches @@ -114,11 +114,12 @@ quit — Disconnect ## Status -**14/40 conformance tests — FLEET CERTIFIED** ✅ +**15/40 conformance tests — FLEET CERTIFIED** ✅ - T01-T04: Room lifecycle (create, destroy, connect, disconnect) - T05-T07: Agent lifecycle (enter, leave, move) - T08: Wall notes +- T09: Direct tell between agents (fixed and covered by `test_command`) - T14-T15: Mailbox and equipment - T16-T17: Permission levels - T21-T22: Room boot/shutdown @@ -135,7 +136,7 @@ quit — Disconnect | Memory management | Manual (`calloc`/`free`) | Garbage collected | Manual (defer-free) | | I/O model | Non-blocking + `select()` | Blocking per goroutine | Async | | Buffer sizes | Fixed (holodeck.h) | Dynamic (Go slices) | Stack-allocated | -| Conformance | **14/40** ✅ | 17/40 🟡 | (reference) | +| Conformance | **15/40** ✅ | 17/40 🟡 | (reference) | | Port | :7778 | :7777 | :7779 | | External deps | POSIX sockets | None | None | diff --git a/bin/holodeck b/bin/holodeck deleted file mode 100755 index 18c5ae1..0000000 Binary files a/bin/holodeck and /dev/null differ diff --git a/holodeck b/holodeck deleted file mode 100755 index f29d1c3..0000000 Binary files a/holodeck and /dev/null differ diff --git a/obj/main.o b/obj/main.o deleted file mode 100644 index 81c938c..0000000 Binary files a/obj/main.o and /dev/null differ diff --git a/src/command.c b/src/command.c index 692274c..af31b00 100644 --- a/src/command.c +++ b/src/command.c @@ -147,7 +147,13 @@ void cmd_tell(Agent *agent, const char *args) { Room *room = agent_get_room(agent); if (!room) return; - comms_tell(agent, room, target, message); + Agent *target_agent = room_find_agent_by_name(room, target); + if (!target_agent) { + agent_send(agent, "You don't see anyone by that name here.\n"); + return; + } + + comms_tell(agent, room, target_agent, message); } void cmd_yell(Agent *agent, const char *args) { diff --git a/src/room.c b/src/room.c index 9e486ac..2254730 100644 --- a/src/room.c +++ b/src/room.c @@ -86,6 +86,19 @@ Room *room_find_exit(const Room *room, const char *direction) { return NULL; } +Agent *room_find_agent_by_name(const Room *room, const char *name) { + if (!room || !name) return NULL; + + for (int i = 0; i < HOLO_MAX_AGENTS; i++) { + Agent *other = room->agents[i]; + if (other && strcmp(other->name, name) == 0) { + return other; + } + } + + return NULL; +} + void room_add_agent(Room *room, Agent *agent) { if (!room || !agent) return; if (room->agent_count >= HOLO_MAX_AGENTS) return; diff --git a/src/room.h b/src/room.h index 69478ba..b646f22 100644 --- a/src/room.h +++ b/src/room.h @@ -32,6 +32,7 @@ void room_destroy(Room *room); void room_connect(Room *from, Room *to, const char *direction); void room_disconnect(Room *room, const char *direction); Room *room_find_exit(const Room *room, const char *direction); +Agent *room_find_agent_by_name(const Room *room, const char *name); void room_add_agent(Room *room, Agent *agent); void room_remove_agent(Room *room, Agent *agent); void room_add_note(Room *room, const char *author, const char *text); diff --git a/test_conf b/test_conf deleted file mode 100755 index 2b3bd34..0000000 Binary files a/test_conf and /dev/null differ diff --git a/test_conf_v2 b/test_conf_v2 deleted file mode 100755 index 1223f21..0000000 Binary files a/test_conf_v2 and /dev/null differ diff --git a/test_full_conf b/test_full_conf deleted file mode 100755 index 3e0691d..0000000 Binary files a/test_full_conf and /dev/null differ diff --git a/tests/test_command.c b/tests/test_command.c new file mode 100644 index 0000000..c30cccb --- /dev/null +++ b/tests/test_command.c @@ -0,0 +1,96 @@ +/* + * test_command.c — Command dispatch tests + * + * End-to-end coverage for cmd_tell: + * - message is delivered to the target agent's mailbox + * - sender's own mailbox is untouched + * - unknown target produces a sensible error message + * + * Build standalone: + * gcc -std=c99 -Wall -Wextra -Wno-pedantic -Isrc \ + * -o test_command tests/test_command.c src/room.c src/agent.c \ + * src/command.c src/comms.c + */ +#include "holodeck.h" +#include "room.h" +#include "agent.h" +#include "command.h" + +#include +#include + +static int passed = 0; +static int total = 0; + +#define TEST(name) do { total++; printf(" %-40s", #name); +#define PASS(cond) if (cond) { passed++; printf("PASS\n"); } else { printf("FAIL\n"); } } while(0) + +static void test_tell_delivers_message(void) { + printf("── tell command ──\n"); + + Room *room = room_create("test", "Test Room", "A room for testing tell"); + Agent *sender = agent_create(-1); + Agent *receiver = agent_create(-1); + + agent_set_name(sender, "Alice"); + agent_set_name(receiver, "Bob"); + agent_set_room(sender, room); + agent_set_room(receiver, room); + room_add_agent(room, sender); + room_add_agent(room, receiver); + + command_execute(sender, "tell Bob Hello there"); + + TEST(tell_reaches_target_mailbox); + const MailboxMessage *msg = agent_mailbox_get(receiver); + PASS(msg != NULL && + strcmp(msg->from, "Alice") == 0 && + strcmp(msg->text, "Hello there") == 0); + + TEST(tell_does_not_reach_sender_mailbox); + PASS(agent_mailbox_get(sender) == NULL); + + room_remove_agent(room, sender); + room_remove_agent(room, receiver); + agent_destroy(sender); + agent_destroy(receiver); + room_destroy(room); +} + +static void test_tell_unknown_target(void) { + Room *room = room_create("test2", "Test Room 2", "A room for testing errors"); + Agent *sender = agent_create(-1); + + agent_set_name(sender, "Alice"); + agent_set_room(sender, room); + room_add_agent(room, sender); + + command_execute(sender, "tell Charlie Hello"); + + TEST(tell_unknown_target_reports_error); + PASS(strstr(sender->output_buffer, + "don't see anyone by that name here") != NULL); + + room_remove_agent(room, sender); + agent_destroy(sender); + room_destroy(room); +} + +int main(void) { + printf("=== Holodeck C -- Command Tests ===\n\n"); + + command_register("tell", cmd_tell); + + test_tell_delivers_message(); + test_tell_unknown_target(); + + printf("\n=== Results: %d/%d passed ===\n", passed, total); + + if (passed == total) { + printf("Status: ALL PASSED\n"); + return 0; + } else { + printf("Status: %d FAILED\n", total - passed); + return 1; + } +} diff --git a/tests/test_rooms.c b/tests/test_rooms.c index d0fc4fb..065cd0a 100644 --- a/tests/test_rooms.c +++ b/tests/test_rooms.c @@ -1,7 +1,23 @@ +/* + * test_rooms.c — Room connectivity & lifecycle tests + * + * Tests the real, current room API (src/room.h): + * room_create / room_destroy + * room_connect / room_disconnect / room_find_exit + * room_add_agent / room_remove_agent + * room_add_note / room_get_notes + * room_is_booted / room_set_booted + * + * Build standalone: + * gcc -std=c99 -Wall -Wextra -Wno-pedantic -Isrc \ + * -o test_rooms tests/test_rooms.c src/room.c src/agent.c + */ +#include "holodeck.h" +#include "room.h" +#include "agent.h" + #include #include -#include -#include "../src/room.h" static int passed = 0; static int total = 0; @@ -9,116 +25,227 @@ static int total = 0; #define TEST(name) do { total++; printf(" %-40s", #name); #define PASS(cond) if (cond) { passed++; printf("PASS\n"); } else { printf("FAIL\n"); } } while(0) -void test_create_room(void) { - RoomGraph g; - graph_init(&g); - TEST(create_room); - PASS(graph_create_room(&g, "tavern", "Tavern", "A room") == 1); - - TEST(reject_duplicate); - PASS(graph_create_room(&g, "tavern", "Dupe", "Should fail") == 0); - - TEST(count_correct); - PASS(g.count == 1); -} +/* ════════════════════════════════════════════════════════════════ + * Room Lifecycle + * ════════════════════════════════════════════════════════════════ */ +static void test_room_create(void) { + printf("── Room Lifecycle ──\n"); + + Room *r = room_create("tavern", "The Tavern", "A cozy place"); + + TEST(create_returns_room); + PASS(r != NULL); + + TEST(create_sets_id); + PASS(r != NULL && strcmp(r->id, "tavern") == 0); + + TEST(create_sets_name); + PASS(r != NULL && strcmp(r->name, "The Tavern") == 0); + + TEST(create_sets_description); + PASS(r != NULL && strcmp(r->description, "A cozy place") == 0); + + TEST(new_room_has_no_exits); + PASS(room_find_exit(r, "north") == NULL); -void test_destroy_room(void) { - RoomGraph g; - graph_init(&g); - graph_create_room(&g, "tavern", "Tavern", "A room"); - - TEST(destroy_existing); - PASS(graph_destroy_room(&g, "tavern") == 1); - - TEST(destroy_nonexistent); - PASS(graph_destroy_room(&g, "nope") == 0); + TEST(new_room_has_no_notes); + PASS(room_get_notes(r) == NULL); + + TEST(new_room_has_no_agents); + PASS(r->agent_count == 0); + + TEST(new_room_not_booted); + PASS(room_is_booted(r) == 0); + + room_destroy(r); } -void test_connect_rooms(void) { - RoomGraph g; - graph_init(&g); - graph_create_room(&g, "a", "Room A", "First"); - graph_create_room(&g, "b", "Room B", "Second"); - - TEST(connect_valid); - PASS(graph_connect(&g, "a", "north", "b") == 1); - - TEST(connect_nonexistent); - PASS(graph_connect(&g, "a", "south", "c") == 0); - - TEST(exit_exists); - Room *r = graph_find_room(&g, "a"); - PASS(r && r->exit_count == 1 && strcmp(r->exits[0].direction, "north") == 0); +static void test_room_destroy(void) { + TEST(destroy_null_safe); + room_destroy(NULL); + PASS(1); + + Room *r = room_create("tmp", "Temp", "Temporary"); + + TEST(destroy_frees_room); + room_destroy(r); + PASS(1); } -void test_room_boot_shutdown(void) { - Room r; - room_init(&r, "test", "Test", "Testing"); - - TEST(boot_sets_flag); - room_boot(&r, "agent1"); - PASS(r.booted == 1); - - TEST(boot_sets_agent); - PASS(strcmp(r.active_agent, "agent1") == 0); - - TEST(shutdown_clears); - room_shutdown(&r); - PASS(r.booted == 0); +/* ════════════════════════════════════════════════════════════════ + * Exits / Connectivity + * ════════════════════════════════════════════════════════════════ */ +static void test_room_connect(void) { + printf("\n── Exits / Connectivity ──\n"); + + Room *a = room_create("a", "Room A", "First room"); + Room *b = room_create("b", "Room B", "Second room"); + + room_connect(a, b, "north"); + + TEST(connect_and_find_exit); + PASS(room_find_exit(a, "north") == b); + + TEST(find_missing_exit); + PASS(room_find_exit(a, "south") == NULL); + + TEST(connect_is_oneway); + PASS(room_find_exit(b, "north") == NULL); + + room_connect(a, b, "south"); + + TEST(multiple_exits_findable); + PASS(room_find_exit(a, "north") == b && room_find_exit(a, "south") == b); + + room_disconnect(a, "north"); + + TEST(disconnect_removes_exit); + PASS(room_find_exit(a, "north") == NULL); + + TEST(disconnect_preserves_other); + PASS(room_find_exit(a, "south") == b); + + TEST(disconnect_missing_safe); + room_disconnect(a, "west"); + PASS(1); + + TEST(find_exit_null_safe); + PASS(room_find_exit(NULL, "north") == NULL); + + TEST(connect_null_safe); + room_connect(NULL, b, "north"); + room_connect(a, NULL, "north"); + PASS(room_find_exit(a, "north") == NULL && room_find_exit(a, "south") == b); + + room_destroy(a); + room_destroy(b); } -void test_room_notes(void) { - Room r; - room_init(&r, "test", "Test", "Testing"); - - room_add_note(&r, "agent1", "Hello"); - room_add_note(&r, "agent2", "World"); - - TEST(note_count); - PASS(r.note_count == 2); - - TEST(note_content); - PASS(strcmp(r.notes[0].author, "agent1") == 0 && strcmp(r.notes[0].content, "Hello") == 0); +/* ════════════════════════════════════════════════════════════════ + * Agents (enter / leave) + * ════════════════════════════════════════════════════════════════ */ +static void test_room_agents(void) { + printf("\n── Agents ──\n"); + + Room *room = room_create("room", "The Room", "A test room"); + Agent *ag1 = agent_create(-1); + Agent *ag2 = agent_create(-1); + + room_add_agent(room, ag1); + + TEST(add_agent_increments_count); + PASS(room->agent_count == 1); + + room_add_agent(room, ag2); + + TEST(add_multiple_agents); + PASS(room->agent_count == 2); + + room_remove_agent(room, ag1); + + TEST(remove_agent_decrements); + PASS(room->agent_count == 1); + + room_add_agent(room, ag1); + + TEST(readd_agent); + PASS(room->agent_count == 2); + + room_remove_agent(room, ag1); + + TEST(remove_absent_agent_safe); + room_remove_agent(room, ag1); + PASS(room->agent_count == 1); + + room_remove_agent(room, ag2); + room_destroy(room); + agent_destroy(ag1); + agent_destroy(ag2); + + TEST(add_null_agent_safe); + room_add_agent(NULL, ag1); + PASS(1); } -void test_room_look(void) { - Room r; - room_init(&r, "tavern", "The Tavern", "A cozy place"); - room_add_exit(&r, "north", "kitchen"); - char buf[1024]; - room_look(&r, buf, sizeof(buf)); - - TEST(look_has_name); - PASS(strstr(buf, "The Tavern") != NULL); - - TEST(look_has_exit); - PASS(strstr(buf, "north") != NULL); +/* ════════════════════════════════════════════════════════════════ + * Notes + * ════════════════════════════════════════════════════════════════ */ +static void test_room_notes(void) { + printf("\n── Notes ──\n"); + + Room *room = room_create("notes", "Note Room", "Walls with notes"); + + room_add_note(room, "agent1", "Hello world"); + room_add_note(room, "agent2", "Goodbye world"); + + const Note *notes = room_get_notes(room); + + TEST(add_note_returns_head); + PASS(notes != NULL); + + TEST(note_head_is_most_recent); + PASS(notes != NULL && strcmp(notes->author, "agent2") == 0); + + TEST(note_head_text_correct); + PASS(notes != NULL && strcmp(notes->text, "Goodbye world") == 0); + + TEST(note_chain_has_previous); + PASS(notes != NULL && notes->next != NULL && + strcmp(notes->next->author, "agent1") == 0); + + TEST(get_notes_null_safe); + PASS(room_get_notes(NULL) == NULL); + + TEST(add_note_null_safe); + room_add_note(room, NULL, "no author"); + PASS(1); + + room_destroy(room); } -void test_remove_exit(void) { - Room r; - room_init(&r, "test", "Test", "Testing"); - room_add_exit(&r, "north", "a"); - room_add_exit(&r, "south", "b"); - - TEST(remove_existing); - PASS(room_remove_exit(&r, "north") == 1 && r.exit_count == 1); - - TEST(remove_nonexistent); - PASS(room_remove_exit(&r, "west") == 0); +/* ════════════════════════════════════════════════════════════════ + * Boot state + * ════════════════════════════════════════════════════════════════ */ +static void test_room_boot(void) { + printf("\n── Boot state ──\n"); + + Room *room = room_create("rt", "Runtime", "A runtime room"); + + TEST(boot_set_true); + room_set_booted(room, 1); + PASS(room_is_booted(room) == 1); + + TEST(shutdown_sets_false); + room_set_booted(room, 0); + PASS(room_is_booted(room) == 0); + + TEST(is_booted_null_safe); + PASS(room_is_booted(NULL) == 0); + + TEST(set_booted_null_safe); + room_set_booted(NULL, 1); + PASS(1); + + room_destroy(room); } int main(void) { - printf("═══ Holodeck C — Conformance Tests ═══\n\n"); - - test_create_room(); - test_destroy_room(); - test_connect_rooms(); - test_room_boot_shutdown(); + printf("=== Holodeck C -- Room API Tests ===\n\n"); + + test_room_create(); + test_room_destroy(); + test_room_connect(); + test_room_agents(); test_room_notes(); - test_room_look(); - test_remove_exit(); - - printf("\n═══ Results: %d/%d passed ═══\n", passed, total); - return passed == total ? 0 : 1; + test_room_boot(); + + printf("\n=== Results: %d/%d passed ===\n", passed, total); + + if (passed == total) { + printf("Status: ALL PASSED\n"); + return 0; + } else { + printf("Status: %d FAILED\n", total - passed); + return 1; + } }