save/e2e: #51 — wire generateRing attach into resolveJumpTask + spread-limit e2e

- resolveJumpTask: build existingEdgeKeys from all jump routes (both
  endpoints in the save's system set), pass all systems except the
  target as attachCandidates, insert closing routes for each attach.
- save.test.ts: jump test now expects 1–4 fresh systems (was pinned
  to exactly 4); A/B snapshot test stays green (both paths share the
  same resolveJumpTask code).
- e2e/helpers.ts: update stale comment ('4 fresh neighbours' →
  '1–4 new systems').
- e2e/smoke.spec.ts: new test verifying 1–4 mints, all undiscovered,
  each routed to the target, and BFS connectivity of the full graph.
This commit is contained in:
2026-10-06 12:18:33 +00:00
parent 90de8818f8
commit ab22a7e68b
4 changed files with 71 additions and 11 deletions
+7 -6
View File
@@ -49,12 +49,13 @@ export async function resolveAction(page: Page, path: string, data: Record<strin
}
// Jump to an adjacent system, preferring one that is still undiscovered. Every jump into an
// undiscovered system generates 4 fresh neighbours (lib/game/world.ts), so fresh territory is
// always available — picking the FIRST edge instead ping-pongs between exhausted systems, because
// edges come back in insertion order and the oldest route out of a system is usually the one that
// led here. Falls back to any non-current adjacent system if none is undiscovered. The jump is
// queued with a default one-person crew and ticked to completion (resolveAction) before this
// returns, so the caller is already standing in the target system, not merely en route to it.
// undiscovered system mints a seeded-random 1–4 new systems (at least one always minted —
// lib/game/world.ts #51), so fresh territory is always available — picking the FIRST edge
// instead ping-pongs between exhausted systems, because edges come back in insertion order and
// the oldest route out of a system is usually the one that led here. Falls back to any
// non-current adjacent system if none is undiscovered. The jump is queued with a default
// one-person crew and ticked to completion (resolveAction) before this returns, so the caller
// is already standing in the target system, not merely en route to it.
export async function jumpToFreshSystem(page: Page): Promise<WorldState> {
const world = (await (await page.request.get('/api/world')).json()) as WorldState;
const currentId = world.systems.find((s) => s.current)!.id;
+40
View File
@@ -199,6 +199,46 @@ test('jumping to an adjacent system updates the world state and turn counter', a
expect(after.systems.find((s) => s.current)?.id).toBe(targetId);
});
test('#51: jumping into undiscovered territory mints 1–4 new systems, each routed to the target, and the graph stays connected', async ({ page }) => {
await drainQueue(page);
const before = (await (await page.request.get('/api/world')).json()) as WorldState;
const after = await jumpToFreshSystem(page);
const target = after.systems.find((s) => s.current)!;
const beforeIds = new Set(before.systems.map((s) => s.id));
const fresh = after.systems.filter((s) => !beforeIds.has(s.id));
// #51: seeded-random 1–4 new systems (the frontier guarantee ensures at least one)
expect(fresh.length).toBeGreaterThanOrEqual(1);
expect(fresh.length).toBeLessThanOrEqual(4);
// all new systems are undiscovered
expect(fresh.every((s) => !s.discovered)).toBe(true);
// each new system is directly routed to the jump target
for (const n of fresh) {
expect(after.edges.some(([a, b]) => (a === target.id && b === n.id) || (a === n.id && b === target.id))).toBe(true);
}
// the whole graph stays connected: BFS from the target reaches every system
const adj = new Map<number, number[]>();
for (const [a, b] of after.edges) {
if (!adj.has(a)) adj.set(a, []);
if (!adj.has(b)) adj.set(b, []);
adj.get(a)!.push(b);
adj.get(b)!.push(a);
}
const seen = new Set([target.id]);
const queue = [target.id];
while (queue.length > 0) {
const cur = queue.shift()!;
for (const nb of adj.get(cur) ?? []) {
if (!seen.has(nb)) {
seen.add(nb);
queue.push(nb);
}
}
}
expect(seen.size).toBe(after.systems.length); // no orphaned systems
});
test('local system shows the new current system after a jump, without a page reload (#30)', async ({ page }) => {
await drainQueue(page); // retry-safety: a failed earlier attempt may have left a row queued (an in-transit JUMP would 409 every new queue)
const before = (await (await page.request.get('/api/world')).json()) as WorldState;