v1.4.0: per-player snapshot + shared market view with owner info (ISS-112)
CI / build + unit + e2e (pull_request) Successful in 14m29s
CI / build + unit + e2e (pull_request) Successful in 14m29s
This commit is contained in:
@@ -2,6 +2,7 @@ import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { loadAirportsGeoJson } from '@/lib/airports';
|
||||
import { haversineKm } from '@/lib/geo';
|
||||
import { FUEL_PRICE_PER_L, PLANE_TYPES, SELL_FRACTION } from '@/lib/game/catalog';
|
||||
@@ -58,8 +59,8 @@ describe('GET /api/state', () => {
|
||||
|
||||
const planes = body.planes as Array<Record<string, unknown>>;
|
||||
expect(planes.length).toBeGreaterThanOrEqual(1);
|
||||
for (const key of ['id', 'model', 'status', 'fuel', 'maxFuel', 'locationIcao', 'locationName', 'destIcao', 'destName', 'lat', 'lon']) {
|
||||
expect(planes[0], `plane.${key}`).toHaveProperty(key);
|
||||
for (const key of ['id', 'model', 'status', 'fuel', 'maxFuel', 'locationIcao', 'locationName', 'destIcao', 'destName', 'lat', 'lon', 'ownerId', 'ownerName']) {
|
||||
expect(planes[0], `plane.${key}`).toHaveProperty(key); // ownerId/ownerName: v1.4.0 (ISS-112)
|
||||
}
|
||||
expect(typeof planes[0].locationName).toBe('string');
|
||||
// The seeded starter is grounded — no flight destination yet.
|
||||
@@ -68,8 +69,8 @@ describe('GET /api/state', () => {
|
||||
|
||||
const packages = body.packages as Array<Record<string, unknown>>;
|
||||
expect(packages.length).toBeGreaterThan(0);
|
||||
for (const key of ['id', 'originIcao', 'destIcao', 'reward', 'cargoType', 'weightKg', 'distanceKm', 'deadlineSimTime', 'status']) {
|
||||
expect(packages[0], `package.${key}`).toHaveProperty(key);
|
||||
for (const key of ['id', 'originIcao', 'destIcao', 'reward', 'cargoType', 'weightKg', 'distanceKm', 'deadlineSimTime', 'status', 'carrierName']) {
|
||||
expect(packages[0], `package.${key}`).toHaveProperty(key); // carrierName: v1.4.0 (ISS-112)
|
||||
}
|
||||
|
||||
// v0.48.0 (ISS-095): distanceKm is computed server-side — it must equal the haversine of the
|
||||
@@ -133,4 +134,52 @@ describe('GET /api/state', () => {
|
||||
expect(body.account.cash).toBeCloseTo(12_345.67, 9); // the session player's row…
|
||||
expect(body.account.reputation).toBeCloseTo(42.5, 9); // …not the seeded local account
|
||||
});
|
||||
|
||||
it('carries ALL planes with owner info — other players\' planes included (v1.4.0, ISS-112)', async () => {
|
||||
const inst = await getGameDb();
|
||||
// A second player with a plane of their own — rows minted like /api/register does.
|
||||
const id = 'test-player-c';
|
||||
await inst.db.insert(schema.users).values({
|
||||
id, username: 'e2e-owner-c', passwordHash: hashPassword('unused'), admin: false, createdAt: Date.now(),
|
||||
});
|
||||
await inst.db.insert(schema.players).values({
|
||||
id, provider: 'local', username: 'e2e-owner-c', cash: 1000, reputation: 1,
|
||||
});
|
||||
const type = PLANE_TYPES.atr72;
|
||||
await inst.db.insert(schema.planes).values({
|
||||
id: 'plane-test-c', ownerId: id, model: 'atr72', status: 'available',
|
||||
fuel: type.maxFuel, maxFuel: type.maxFuel, locationIcao: 'EDDF', lat: 50.03, lon: 8.56,
|
||||
});
|
||||
|
||||
const res = await GET(req()); // the LOCAL session — still sees the whole world's fleet
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { planes: Array<{ id: string; ownerId: string; ownerName: string }> };
|
||||
const starter = body.planes.find((p) => p.id === 'plane-starter');
|
||||
expect(starter?.ownerId).toBe('local'); // seeded fleet backfilled to the local player…
|
||||
expect(starter?.ownerName).toBe('kamilkrzywda'); // …and every row names its owner by username.
|
||||
const foreign = body.planes.find((p) => p.id === 'plane-test-c');
|
||||
expect(foreign?.ownerId).toBe(id);
|
||||
expect(foreign?.ownerName).toBe('e2e-owner-c');
|
||||
});
|
||||
|
||||
it('joins carrierName via package → planeId → planes.owner_id → players.username (v1.4.0, ISS-112)', async () => {
|
||||
const inst = await getGameDb();
|
||||
// Reuse the foreign player/plane from the previous test (same throwaway DB within this file).
|
||||
const before = ((await GET(req())).json()) as Promise<{ packages: Array<Record<string, unknown>> }>;
|
||||
const snapBefore = await before;
|
||||
const openPkg = snapBefore.packages.find((p) => p.status === 'open')!;
|
||||
|
||||
// Assign it to the foreign player's plane directly (loadPackage does exactly this).
|
||||
await inst.db.update(schema.packages)
|
||||
.set({ status: 'loaded', planeId: 'plane-test-c' })
|
||||
.where(eq(schema.packages.id, String(openPkg.id)));
|
||||
|
||||
const snap = ((await GET(req())).json()) as Promise<{ packages: Array<Record<string, unknown>> }>;
|
||||
const after = await snap;
|
||||
const assigned = after.packages.find((p) => p.id === openPkg.id)!;
|
||||
expect(assigned.carrierName).toBe('e2e-owner-c'); // the carrying plane's owner…
|
||||
for (const pkg of after.packages) { // …and every unassigned package stays null.
|
||||
if (pkg.planeId == null) expect(pkg.carrierName, `package ${String(pkg.id)}`).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,9 +2,10 @@ import { requireAuth } from '@/lib/auth';
|
||||
import { tickNow } from '@/lib/sim/engine';
|
||||
import { getStateSnapshot } from '@/lib/game/state';
|
||||
|
||||
// v1.1.0 (ISS-107): guarded — 401 without a live X-Flight-Session. The snapshot shape is
|
||||
// unchanged; per-player snapshots are a later step (#112).
|
||||
// v1.1.0 (ISS-107): guarded — 401 without a live X-Flight-Session.
|
||||
// v1.3.0 (ISS-110): `account` carries the SESSION player's economy row.
|
||||
// v1.4.0 (ISS-112): full shared-world snapshot — ALL planes with owner info, packages with
|
||||
// carrierName; same projection as the SSE frames, so poll and push can never drift.
|
||||
export async function GET(req: Request) {
|
||||
const auth = await requireAuth(req);
|
||||
if (!auth.ok) return auth.response;
|
||||
|
||||
Reference in New Issue
Block a user