diff --git a/src/client.ts b/src/client.ts index fd48487..bed7071 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1438,7 +1438,7 @@ interface NamedFormatter { format: (result: Record) => boolean; } -const resultFormatters: NamedFormatter[] = [ +export const resultFormatters: NamedFormatter[] = [ // Player status { name: 'player_status', @@ -1684,10 +1684,16 @@ const resultFormatters: NamedFormatter[] = [ console.log(`(No wrecks at this location)`); } else { for (const w of wrecks) { - console.log(`\n${c.yellow}Wreck: ${w.wreck_id}${c.reset}`); + console.log(`\n${c.yellow}Wreck: ${w.id}${c.reset}`); console.log(` Ship: ${w.ship_class}`); - console.log(` Expires in: ${w.ticks_remaining} ticks`); - const items = (w.items as Array>) || []; + // expire_tick === 0 means the wreck never expires (ship/pirate/abandoned wrecks); + // only jettisoned junk containers get a finite expiry. + if (w.expire_tick) { + console.log(` Expires at: ${w.expires_at ?? `tick ${w.expire_tick}`}`); + } else { + console.log(` Expires: never`); + } + const items = (w.cargo as Array>) || []; if (items.length) { console.log(` Contents:`); for (const item of items) console.log(` - ${item.quantity}x ${item.item_id}`); @@ -2644,4 +2650,7 @@ async function main(): Promise { } } -main(); +// Only run the CLI when executed directly, so the module can be imported by tests. +if (import.meta.main) { + main(); +} diff --git a/src/wrecks-format.test.ts b/src/wrecks-format.test.ts new file mode 100644 index 0000000..634d9fc --- /dev/null +++ b/src/wrecks-format.test.ts @@ -0,0 +1,68 @@ +/** + * Regression test for gh#1874 — get_wrecks output printed "undefined" for the + * wreck id and expiry because the formatter read fields the server never emits + * (`wreck_id`, `ticks_remaining`, `items`). The server emits `id`, `expire_tick` + * / `expires_at`, and `cargo`. + */ + +import { describe, expect, test } from 'bun:test'; +import { resultFormatters } from './client'; + +const wrecksFormatter = resultFormatters.find((f) => f.name === 'wrecks'); +if (!wrecksFormatter) throw new Error('wrecks formatter not found'); + +function render(result: Record): string { + const lines: string[] = []; + const orig = console.log; + console.log = (...args: unknown[]) => lines.push(args.join(' ')); + try { + wrecksFormatter.format(result); + } finally { + console.log = orig; + } + // Colour codes wrap whole tokens, so the asserted substrings stay contiguous. + return lines.join('\n'); +} + +describe('get_wrecks formatter (gh#1874)', () => { + test('renders the real server fields, never the literal "undefined"', () => { + const out = render({ + wrecks: [ + { + id: 'wreck_abc123', + ship_class: 'eviction_notice', + expire_tick: 0, // ship/pirate/abandoned wrecks never expire + cargo: [{ item_id: 'titanium_ore', quantity: 12 }], + }, + ], + }); + + expect(out).not.toContain('undefined'); + expect(out).toContain('Wreck: wreck_abc123'); + expect(out).toContain('Ship: eviction_notice'); + expect(out).toContain('Expires: never'); + expect(out).toContain('12x titanium_ore'); + }); + + test('shows a finite expiry for jettisoned containers that do expire', () => { + const out = render({ + wrecks: [ + { + id: 'wreck_junk', + ship_class: 'jettison', + expire_tick: 1500, + expires_at: '2026-07-23T00:00:00Z', + cargo: [], + }, + ], + }); + + expect(out).not.toContain('undefined'); + expect(out).toContain('Expires at: 2026-07-23T00:00:00Z'); + }); + + test('handles the empty-wrecks case', () => { + const out = render({ wrecks: [] }); + expect(out).toContain('No wrecks at this location'); + }); +});