#include #include #include #include "../src/reading.h" #include "../src/i18n.h" static void test_tarot_determinism(void) { CelticCrossSpread a, b, c; tarot_draw_celtic_cross("2026-07-03", &a); tarot_draw_celtic_cross("2026-07-03", &b); tarot_draw_celtic_cross("2026-07-04", &c); for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { assert(a.positions[i].card == b.positions[i].card); assert(a.positions[i].reversed == b.positions[i].reversed); } int differs = 0; for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { if (a.positions[i].card != c.positions[i].card || a.positions[i].reversed != c.positions[i].reversed) { differs = 1; break; } } assert(differs && "different seeds should (almost always) differ"); /* No repeated cards within a single spread. */ for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { for (int j = i + 1; j < TAROT_SPREAD_SIZE; j++) { assert(a.positions[i].card != a.positions[j].card); } } printf("PASS test_tarot_determinism\n"); } static void test_natal_sun_sign(void) { /* 2000-06-15 noon UTC: the Sun is well inside Gemini (60-90 degrees), * away from any sign boundary, so this is a stable regression check * against the ephemeris math. Birth location doesn't affect the Sun's * geocentric ecliptic longitude, only the Ascendant. */ BirthData birth = { .year = 2000, .month = 6, .day = 15, .hour = 12, .minute = 0, .utc_offset_hours = 0.0, .latitude = 52.5, .longitude = 13.4, }; NatalChart chart; astro_compute_natal_chart(&birth, &chart); assert(chart.bodies[PLANET_SUN].sign == SIGN_GEMINI); /* Whole-sign house invariant: body.house (1-12) must name the house * whose sign (chart.houses[house-1]) is that same body's sign. */ for (int b = 0; b < NUM_BODIES; b++) { assert(chart.bodies[b].house >= 1 && chart.bodies[b].house <= 12); assert(chart.houses[chart.bodies[b].house - 1] == chart.bodies[b].sign); } printf("PASS test_natal_sun_sign\n"); } static void test_reading_generate_smoke(void) { BirthData birth = { .year = 1990, .month = 5, .day = 14, .hour = 14, .minute = 32, .utc_offset_hours = 2.0, .latitude = 52.52, .longitude = 13.405, }; DailyReading r1, r2; reading_generate("smoke-test-seed", &birth, 1751500000, &r1); reading_generate("smoke-test-seed", &birth, 1751500000, &r2); for (int i = 0; i < TAROT_SPREAD_SIZE; i++) { assert(r1.spread.positions[i].card == r2.spread.positions[i].card); assert(r1.spread.positions[i].reversed == r2.spread.positions[i].reversed); } for (int b = 0; b < NUM_BODIES; b++) { assert(r1.natal.bodies[b].ecliptic_longitude == r2.natal.bodies[b].ecliptic_longitude); assert(r1.transits.bodies[b].ecliptic_longitude == r2.transits.bodies[b].ecliptic_longitude); /* Transiting planets' houses are read against the natal chart's * houses, not a fresh chart for "now" - same invariant as the natal * bodies, but checked against r1.natal.houses. */ assert(r1.transits.bodies[b].house >= 1 && r1.transits.bodies[b].house <= 12); assert(r1.natal.houses[r1.transits.bodies[b].house - 1] == r1.transits.bodies[b].sign); } assert(r1.transits.aspect_count == r2.transits.aspect_count); printf("PASS test_reading_generate_smoke\n"); } static void test_reading_print_json_shape(void) { BirthData birth = { .year = 1990, .month = 5, .day = 14, .hour = 14, .minute = 32, .utc_offset_hours = 2.0, .latitude = 52.52, .longitude = 13.405, }; DailyReading r; reading_generate("smoke-test-seed", &birth, 1751500000, &r); FILE *tmp = tmpfile(); assert(tmp); reading_print_json(&r, tmp); rewind(tmp); char buf[16384]; size_t n = fread(buf, 1, sizeof buf - 1, tmp); buf[n] = '\0'; fclose(tmp); /* Loose structural checks - not a full JSON validator, just enough to * catch a broken serializer (unbalanced braces, a missing section, or * a stray astro_*_name()/tarot_*_name() call leaking translated text * into what must stay language-independent regardless of --lang). */ assert(buf[0] == '{'); assert(strstr(buf, "\"natal\"") != NULL); assert(strstr(buf, "\"transits\"") != NULL); assert(strstr(buf, "\"spread\"") != NULL); assert(strstr(buf, "\"aspects\"") != NULL); assert(strstr(buf, "\"body\": \"sun\"") != NULL); /* slug, not the display name */ assert(strstr(buf, "\"Sun\"") == NULL); /* never a *_name() value */ int braces = 0; for (size_t i = 0; i < n; i++) { if (buf[i] == '{') braces++; if (buf[i] == '}') braces--; } assert(braces == 0); printf("PASS test_reading_print_json_shape\n"); } /* Run with cwd == repo root (as `make test` does), so the shipped * engine/i18n/ .lang files are reachable at that path. */ static void test_i18n_fallback_and_translation(void) { /* No catalog loaded yet: every lookup returns the caller's fallback. */ assert(strcmp(i18n_get("does.not.exist", "fallback text"), "fallback text") == 0); bool loaded = i18n_load("engine/i18n/de.lang"); assert(loaded && "expected engine/i18n/de.lang to exist and load"); assert(strcmp(i18n_get("body.sun", "Sun"), "Sonne") == 0); assert(strcmp(i18n_get("does.not.exist", "fallback text"), "fallback text") == 0); /* astro_ and tarot_ accessors route through the same loaded catalog. */ assert(strcmp(astro_body_name(PLANET_MOON), "Mond") == 0); assert(strcmp(astro_sign_name(SIGN_LEO), "Löwe") == 0); assert(strcmp(astro_moon_phase_name(MOON_FULL), "Vollmond") == 0); assert(strcmp(astro_aspect_name(ASPECT_TRINE), "Trigon") == 0); assert(strcmp(tarot_card_name(CARD_WORLD), "Die Welt") == 0); assert(strcmp(tarot_position_name(POSITION_OUTCOME), "Das Ergebnis") == 0); /* Loading a file that doesn't exist fails and leaves the previously * loaded catalog in place, rather than silently clearing it. */ assert(!i18n_load("engine/i18n/does-not-exist.lang")); assert(strcmp(i18n_get("body.sun", "Sun"), "Sonne") == 0); /* Loading a different language replaces the catalog wholesale. */ loaded = i18n_load("engine/i18n/en.lang"); assert(loaded); assert(strcmp(astro_body_name(PLANET_MOON), "Moon") == 0); printf("PASS test_i18n_fallback_and_translation\n"); } int main(void) { test_tarot_determinism(); test_natal_sun_sign(); test_reading_generate_smoke(); test_reading_print_json_shape(); test_i18n_fallback_and_translation(); printf("All smoke tests passed.\n"); return 0; }