ether-to-astro 1.2.0 → 1.3.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 +15 -5
- package/dist/astro-service/chart-output-service.d.ts +44 -0
- package/dist/astro-service/chart-output-service.js +110 -0
- package/dist/astro-service/date-input.d.ts +14 -0
- package/dist/astro-service/date-input.js +30 -0
- package/dist/astro-service/electional-service.d.ts +45 -0
- package/dist/astro-service/electional-service.js +305 -0
- package/dist/astro-service/natal-service.d.ts +41 -0
- package/dist/astro-service/natal-service.js +179 -0
- package/dist/astro-service/rising-sign-service.d.ts +37 -0
- package/dist/astro-service/rising-sign-service.js +137 -0
- package/dist/astro-service/service-types.d.ts +82 -0
- package/dist/astro-service/service-types.js +1 -0
- package/dist/astro-service/shared.d.ts +65 -0
- package/dist/astro-service/shared.js +98 -0
- package/dist/astro-service/sky-service.d.ts +48 -0
- package/dist/astro-service/sky-service.js +144 -0
- package/dist/astro-service/transit-service.d.ts +82 -0
- package/dist/astro-service/transit-service.js +353 -0
- package/dist/astro-service.d.ts +101 -89
- package/dist/astro-service.js +162 -1042
- package/dist/tool-registry.js +1 -1
- package/docs/product/architecture-boundaries.md +8 -0
- package/docs/releases/1.3.0.md +51 -0
- package/docs/releases/README.md +17 -0
- package/package.json +4 -1
- package/src/astro-service/chart-output-service.ts +155 -0
- package/src/astro-service/date-input.ts +40 -0
- package/src/astro-service/electional-service.ts +395 -0
- package/src/astro-service/natal-service.ts +235 -0
- package/src/astro-service/rising-sign-service.ts +181 -0
- package/src/astro-service/service-types.ts +90 -0
- package/src/astro-service/shared.ts +128 -0
- package/src/astro-service/sky-service.ts +191 -0
- package/src/astro-service/transit-service.ts +507 -0
- package/src/astro-service.ts +177 -1386
- package/src/tool-registry.ts +1 -1
- package/tests/README.md +15 -0
- package/tests/property/electional-service.property.test.ts +67 -0
- package/tests/property/helpers/arbitraries.ts +126 -0
- package/tests/property/helpers/config.ts +52 -0
- package/tests/property/helpers/runtime.ts +12 -0
- package/tests/property/houses.property.test.ts +74 -0
- package/tests/property/rising-sign-service.property.test.ts +255 -0
- package/tests/property/service-transits.property.test.ts +154 -0
- package/tests/property/time-utils.property.test.ts +91 -0
- package/tests/property/transits.property.test.ts +113 -0
- package/tests/unit/astro-service/chart-output-service.test.ts +102 -0
- package/tests/unit/astro-service/electional-service.test.ts +182 -0
- package/tests/unit/astro-service/natal-service.test.ts +126 -0
- package/tests/unit/astro-service/rising-sign-service.test.ts +145 -0
- package/tests/unit/astro-service/sky-service.test.ts +130 -0
- package/tests/unit/astro-service/transit-service.test.ts +312 -0
- package/tests/unit/astro-service.test.ts +136 -781
- package/tests/unit/rising-sign-windows.test.ts +93 -0
- package/tests/unit/tool-registry.test.ts +11 -0
- package/tests/validation/README.md +14 -0
- package/tests/validation/adapters/internal.ts +234 -4
- package/tests/validation/compare/electional.ts +151 -0
- package/tests/validation/compare/rising-sign-windows.ts +347 -0
- package/tests/validation/compare/service-transits.ts +205 -0
- package/tests/validation/fixtures/electional/core.ts +88 -0
- package/tests/validation/fixtures/rising-sign-windows/core.ts +57 -0
- package/tests/validation/fixtures/service-transits/core.ts +89 -0
- package/tests/validation/utils/fixtureTypes.ts +139 -1
- package/tests/validation/validation.spec.ts +82 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { TransitService } from '../../../src/astro-service/transit-service.js';
|
|
3
|
+
import type { McpStartupDefaults } from '../../../src/entrypoint.js';
|
|
4
|
+
import type { NatalChart, PlanetPosition } from '../../../src/types.js';
|
|
5
|
+
|
|
6
|
+
function makePlanet(planet: PlanetPosition['planet'], longitude: number): PlanetPosition {
|
|
7
|
+
return {
|
|
8
|
+
planetId: 0,
|
|
9
|
+
planet,
|
|
10
|
+
longitude,
|
|
11
|
+
latitude: 0,
|
|
12
|
+
distance: 1,
|
|
13
|
+
speed: 1,
|
|
14
|
+
sign: 'Aries',
|
|
15
|
+
degree: longitude % 30,
|
|
16
|
+
isRetrograde: false,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function makeNatalChart(): NatalChart {
|
|
21
|
+
return {
|
|
22
|
+
name: 'Test User',
|
|
23
|
+
birthDate: { year: 1990, month: 6, day: 12, hour: 14, minute: 35 },
|
|
24
|
+
location: { latitude: 37.7749, longitude: -122.4194, timezone: 'America/Los_Angeles' },
|
|
25
|
+
planets: [makePlanet('Sun', 10), makePlanet('Moon', 20)],
|
|
26
|
+
julianDay: 2451545,
|
|
27
|
+
houseSystem: 'P',
|
|
28
|
+
utcDateTime: { year: 1990, month: 6, day: 12, hour: 21, minute: 35 },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeTransitService(mcpStartupDefaults: McpStartupDefaults = {}) {
|
|
33
|
+
const ephem = {
|
|
34
|
+
dateToJulianDay: vi.fn((date: Date) => date.getTime() / 86400000 + 2440587.5),
|
|
35
|
+
calculateAspectAngle: vi.fn((a: number, b: number) => {
|
|
36
|
+
const diff = Math.abs(a - b);
|
|
37
|
+
return diff > 180 ? 360 - diff : diff;
|
|
38
|
+
}),
|
|
39
|
+
getAllPlanets: vi.fn(() => [makePlanet('Sun', 204), makePlanet('Moon', 270)]),
|
|
40
|
+
getPlanetPosition: vi.fn((planetId: number) => makePlanet(planetId === 1 ? 'Moon' : 'Sun', 100)),
|
|
41
|
+
};
|
|
42
|
+
const houseCalc = {
|
|
43
|
+
calculateHouses: vi.fn(() => ({
|
|
44
|
+
ascendant: 270,
|
|
45
|
+
mc: 204,
|
|
46
|
+
cusps: [0, 270, 300, 330, 0, 30, 60, 90, 120, 150, 204, 240, 260],
|
|
47
|
+
system: 'W' as const,
|
|
48
|
+
})),
|
|
49
|
+
};
|
|
50
|
+
const transitCalc = {
|
|
51
|
+
findTransits: vi.fn(() => [
|
|
52
|
+
{
|
|
53
|
+
transitingPlanet: 'Mars',
|
|
54
|
+
natalPlanet: 'Sun',
|
|
55
|
+
aspect: 'square',
|
|
56
|
+
orb: 1.25,
|
|
57
|
+
isApplying: true,
|
|
58
|
+
exactTimeStatus: 'within_preview' as const,
|
|
59
|
+
transitLongitude: 100,
|
|
60
|
+
natalLongitude: 10,
|
|
61
|
+
exactTime: new Date('2024-03-27T12:00:00Z'),
|
|
62
|
+
},
|
|
63
|
+
]),
|
|
64
|
+
};
|
|
65
|
+
const now = vi.fn(() => new Date('2024-03-26T12:00:00Z'));
|
|
66
|
+
const formatTimestamp = vi.fn((date: Date, timezone: string) => `${timezone}:${date.toISOString()}`);
|
|
67
|
+
|
|
68
|
+
const transitService = new TransitService({
|
|
69
|
+
ephem: ephem as any,
|
|
70
|
+
transitCalc: transitCalc as any,
|
|
71
|
+
houseCalc: houseCalc as any,
|
|
72
|
+
mcpStartupDefaults,
|
|
73
|
+
now,
|
|
74
|
+
formatTimestamp,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return { transitService, ephem, houseCalc, transitCalc, now, formatTimestamp };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('When using the extracted TransitService', () => {
|
|
81
|
+
it('Given omitted mode and days_ahead 0, then it preserves snapshot semantics', () => {
|
|
82
|
+
const { transitService, transitCalc } = makeTransitService();
|
|
83
|
+
|
|
84
|
+
const result = transitService.getTransits(makeNatalChart(), {});
|
|
85
|
+
|
|
86
|
+
expect(transitCalc.findTransits).toHaveBeenCalledTimes(1);
|
|
87
|
+
expect(result.data).toMatchObject({
|
|
88
|
+
mode: 'snapshot',
|
|
89
|
+
mode_source: 'legacy_default',
|
|
90
|
+
days_ahead: 0,
|
|
91
|
+
window_start: '2024-03-26',
|
|
92
|
+
window_end: '2024-03-26',
|
|
93
|
+
});
|
|
94
|
+
expect(result.text).toContain('Transit snapshot');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('Given omitted mode and days_ahead > 0, then it preserves best-hit semantics', () => {
|
|
98
|
+
const { transitService, transitCalc } = makeTransitService();
|
|
99
|
+
|
|
100
|
+
const result = transitService.getTransits(makeNatalChart(), { days_ahead: 2 });
|
|
101
|
+
|
|
102
|
+
expect(transitCalc.findTransits).toHaveBeenCalledTimes(3);
|
|
103
|
+
expect(result.data).toMatchObject({
|
|
104
|
+
mode: 'best_hit',
|
|
105
|
+
mode_source: 'legacy_default',
|
|
106
|
+
days_ahead: 2,
|
|
107
|
+
window_start: '2024-03-26',
|
|
108
|
+
window_end: '2024-03-28',
|
|
109
|
+
});
|
|
110
|
+
expect(result.text).toContain('Best-hit transits');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('Given explicit snapshot mode with days_ahead, then it still reports a single-day window', () => {
|
|
114
|
+
const { transitService, transitCalc } = makeTransitService();
|
|
115
|
+
|
|
116
|
+
const result = transitService.getTransits(makeNatalChart(), {
|
|
117
|
+
mode: 'snapshot',
|
|
118
|
+
days_ahead: 5,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(transitCalc.findTransits).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(result.data).toMatchObject({
|
|
123
|
+
mode: 'snapshot',
|
|
124
|
+
mode_source: 'explicit',
|
|
125
|
+
days_ahead: 0,
|
|
126
|
+
window_start: '2024-03-26',
|
|
127
|
+
window_end: '2024-03-26',
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('Given a preferred reporting timezone that crosses midnight, then forecast labels use reporting time while payload keeps both zones', () => {
|
|
132
|
+
const { transitService } = makeTransitService({
|
|
133
|
+
preferredTimezone: 'Asia/Tokyo',
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const result = transitService.getTransits(makeNatalChart(), {
|
|
137
|
+
date: '2024-03-26',
|
|
138
|
+
mode: 'forecast',
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(result.data).toMatchObject({
|
|
142
|
+
timezone: 'Asia/Tokyo',
|
|
143
|
+
calculation_timezone: 'America/Los_Angeles',
|
|
144
|
+
reporting_timezone: 'Asia/Tokyo',
|
|
145
|
+
window_start: '2024-03-27',
|
|
146
|
+
window_end: '2024-03-27',
|
|
147
|
+
forecast: [{ date: '2024-03-27' }],
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('Given an exact transit time, then house placement uses exact-time longitude instead of the sampled longitude', () => {
|
|
152
|
+
const { transitService, ephem } = makeTransitService();
|
|
153
|
+
ephem.dateToJulianDay.mockImplementation((date: Date) => {
|
|
154
|
+
if (date.toISOString() === '2024-03-27T12:00:00.000Z') {
|
|
155
|
+
return 9999;
|
|
156
|
+
}
|
|
157
|
+
return date.getTime() / 86400000 + 2440587.5;
|
|
158
|
+
});
|
|
159
|
+
ephem.getPlanetPosition.mockReturnValue(makePlanet('Mars', 330));
|
|
160
|
+
|
|
161
|
+
const result = transitService.getTransits(makeNatalChart(), { mode: 'snapshot' });
|
|
162
|
+
|
|
163
|
+
expect((result.data as any).transits[0]).toMatchObject({
|
|
164
|
+
transitLongitude: 100,
|
|
165
|
+
transitHouse: 3,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('Given a transit near a sign boundary, then serialized placement carries into the next sign', () => {
|
|
170
|
+
const { transitService, transitCalc } = makeTransitService();
|
|
171
|
+
transitCalc.findTransits.mockReturnValue([
|
|
172
|
+
{
|
|
173
|
+
transitingPlanet: 'Sun',
|
|
174
|
+
natalPlanet: 'Sun',
|
|
175
|
+
aspect: 'conjunction',
|
|
176
|
+
orb: 0.01,
|
|
177
|
+
isApplying: true,
|
|
178
|
+
exactTimeStatus: 'within_preview',
|
|
179
|
+
transitLongitude: 29.999,
|
|
180
|
+
natalLongitude: 10,
|
|
181
|
+
exactTime: undefined,
|
|
182
|
+
},
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
const result = transitService.getTransits(makeNatalChart(), { mode: 'snapshot' });
|
|
186
|
+
|
|
187
|
+
expect((result.data as any).transits[0]).toMatchObject({
|
|
188
|
+
transitSign: 'Taurus',
|
|
189
|
+
transitDegree: 0,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('Given forecast mode across multiple days, then each day is deduped independently', () => {
|
|
194
|
+
const { transitService, transitCalc } = makeTransitService();
|
|
195
|
+
transitCalc.findTransits
|
|
196
|
+
.mockReturnValueOnce([
|
|
197
|
+
{
|
|
198
|
+
transitingPlanet: 'Mars',
|
|
199
|
+
natalPlanet: 'Sun',
|
|
200
|
+
aspect: 'square',
|
|
201
|
+
orb: 1.25,
|
|
202
|
+
isApplying: true,
|
|
203
|
+
exactTimeStatus: 'within_preview',
|
|
204
|
+
transitLongitude: 100,
|
|
205
|
+
natalLongitude: 10,
|
|
206
|
+
exactTime: new Date('2024-03-26T12:00:00Z'),
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
transitingPlanet: 'Mars',
|
|
210
|
+
natalPlanet: 'Sun',
|
|
211
|
+
aspect: 'square',
|
|
212
|
+
orb: 0.75,
|
|
213
|
+
isApplying: true,
|
|
214
|
+
exactTimeStatus: 'within_preview',
|
|
215
|
+
transitLongitude: 100.5,
|
|
216
|
+
natalLongitude: 10,
|
|
217
|
+
exactTime: new Date('2024-03-26T13:00:00Z'),
|
|
218
|
+
},
|
|
219
|
+
])
|
|
220
|
+
.mockReturnValueOnce([
|
|
221
|
+
{
|
|
222
|
+
transitingPlanet: 'Mars',
|
|
223
|
+
natalPlanet: 'Sun',
|
|
224
|
+
aspect: 'square',
|
|
225
|
+
orb: 0.5,
|
|
226
|
+
isApplying: false,
|
|
227
|
+
exactTimeStatus: 'within_preview',
|
|
228
|
+
transitLongitude: 101,
|
|
229
|
+
natalLongitude: 10,
|
|
230
|
+
exactTime: new Date('2024-03-27T12:00:00Z'),
|
|
231
|
+
},
|
|
232
|
+
]);
|
|
233
|
+
|
|
234
|
+
const result = transitService.getTransits(makeNatalChart(), { mode: 'forecast', days_ahead: 1 });
|
|
235
|
+
|
|
236
|
+
expect(result.data).toMatchObject({
|
|
237
|
+
mode: 'forecast',
|
|
238
|
+
mode_source: 'explicit',
|
|
239
|
+
days_ahead: 1,
|
|
240
|
+
forecast: [
|
|
241
|
+
{ date: '2024-03-26', transits: [{ orb: 0.75 }] },
|
|
242
|
+
{ date: '2024-03-27', transits: [{ orb: 0.5 }] },
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
expect(((result.data as any).forecast[0].transits as Array<unknown>)).toHaveLength(1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('Given invalid numeric filters, then validation fails before expansion', () => {
|
|
249
|
+
const { transitService } = makeTransitService();
|
|
250
|
+
|
|
251
|
+
expect(() =>
|
|
252
|
+
transitService.getTransits(makeNatalChart(), { include_mundane: true, days_ahead: Number.NaN })
|
|
253
|
+
).toThrow(/days_ahead must be a finite number >= 0/);
|
|
254
|
+
|
|
255
|
+
expect(() => transitService.getTransits(makeNatalChart(), { max_orb: Number.NaN })).toThrow(
|
|
256
|
+
/max_orb must be a finite number >= 0/
|
|
257
|
+
);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('Given an exact-time lookup without a match, then exactTimeStatus is preserved in the payload', () => {
|
|
261
|
+
const { transitService, transitCalc } = makeTransitService();
|
|
262
|
+
transitCalc.findTransits.mockReturnValue([
|
|
263
|
+
{
|
|
264
|
+
transitingPlanet: 'Mars',
|
|
265
|
+
natalPlanet: 'Sun',
|
|
266
|
+
aspect: 'square',
|
|
267
|
+
orb: 1.25,
|
|
268
|
+
isApplying: true,
|
|
269
|
+
exactTimeStatus: 'outside_preview',
|
|
270
|
+
transitLongitude: 100,
|
|
271
|
+
natalLongitude: 10,
|
|
272
|
+
exactTime: undefined,
|
|
273
|
+
},
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
const result = transitService.getTransits(makeNatalChart(), { mode: 'snapshot' });
|
|
277
|
+
expect((result.data as any).transits[0]).toMatchObject({
|
|
278
|
+
exactTimeStatus: 'outside_preview',
|
|
279
|
+
exactTime: undefined,
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('Given no transits, then it preserves the empty-state text branch', () => {
|
|
284
|
+
const { transitService, transitCalc } = makeTransitService();
|
|
285
|
+
transitCalc.findTransits.mockReturnValue([]);
|
|
286
|
+
|
|
287
|
+
const result = transitService.getTransits(makeNatalChart(), { include_mundane: false });
|
|
288
|
+
|
|
289
|
+
expect(result.text).toContain('No transits found');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('Given mundane expansion over multiple days, then it returns an anchored payload plus per-day mundane slices', () => {
|
|
293
|
+
const { transitService, ephem } = makeTransitService();
|
|
294
|
+
ephem.getAllPlanets.mockReturnValue([
|
|
295
|
+
{ ...makePlanet('Sun', 0), speed: 1 },
|
|
296
|
+
{ ...makePlanet('Moon', 90), speed: 1 },
|
|
297
|
+
{ ...makePlanet('Mars', 120), speed: 1 },
|
|
298
|
+
]);
|
|
299
|
+
|
|
300
|
+
const result = transitService.getTransits(makeNatalChart(), {
|
|
301
|
+
include_mundane: true,
|
|
302
|
+
days_ahead: 1,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
expect((result.data as any).mundane).toMatchObject({
|
|
306
|
+
date: '2024-03-26',
|
|
307
|
+
timezone: 'America/Los_Angeles',
|
|
308
|
+
});
|
|
309
|
+
expect((result.data as any).mundane.days).toHaveLength(2);
|
|
310
|
+
expect(result.text).toContain('Current Planetary Positions');
|
|
311
|
+
});
|
|
312
|
+
});
|