diff --git a/scripts/format-scope.json b/scripts/format-scope.json index e2cd43d52..01d96c28b 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -382,6 +382,9 @@ "src/layers/launches/model.js", "src/layers/launches/selection.js", "src/layers/launches/camera.js", + "src/layers/satellites/records.js", + "src/data/layerSnapshot.js", + "src/data/layerSnapshot.test.mjs", "src/data/satellitePass.js", "src/data/satellitePass.test.mjs", "src/data/satellites.js", diff --git a/src/layers/satellites/source.test.mjs b/src/layers/satellites/source.test.mjs index 61913263c..ba0f376ec 100644 --- a/src/layers/satellites/source.test.mjs +++ b/src/layers/satellites/source.test.mjs @@ -136,6 +136,18 @@ test('satellite pass prediction methods report no-tle on cold catalog and coerce assert.equal(issResult.status, 'ok'); assert.deepEqual(issResult, numericResult); + layer._setTrackedSatelliteRefreshStateForTest({ noradId: 25544, name: 'ISS (ZARYA)', satrec, + neighbours: [{ noradId: 10, name: 'STARLINK ONE', satrec }, { noradId: 11, name: 'STARLINK TWO', satrec }], + }); + assert.equal(layer.resolveSatelliteForPass('25544').noradId, 25544); + assert.equal(layer.resolveSatelliteForPass('iss (zarya)').noradId, 25544); + assert.equal(layer.resolveSatelliteForPass('starlink').status, 'ambiguous'); + assert.equal(layer.resolveSatelliteForPass('starlink').totalMatches, 2); + assert.equal(layer.resolveSatelliteForPass('STARLINK ONE').noradId, 10); + assert.equal(layer.resolveSatelliteForPass('missing').status, 'not-found'); + layer._clearDenseCatalogStateForTest(); + assert.equal(layer.resolveSatelliteForPass('25544').status, 'not-found'); + // Non-existent satellite ID in populated catalog assert.deepEqual( layer.getNextSatellitePass(99999, { diff --git a/src/voice/gevActions.js b/src/voice/gevActions.js index 3bf5000e0..0639ca610 100644 --- a/src/voice/gevActions.js +++ b/src/voice/gevActions.js @@ -2067,7 +2067,10 @@ function nextSatellitePass(viewer, dataManager, args) { getNextIssPass: (options) => layer.getNextSatellitePass(identity.noradId, { ...options, requireVisible: args.visibleOnly === true }), } }]]) }; const result = nextIssPass(viewer, adapter, args); - if (result.error) result.error = result.error.replace(/ISS/g, identity.name || String(identity.noradId)); + if (result.error) { + result.error = result.error.replace(/ISS/g, identity.name || String(identity.noradId)); + if (args.visibleOnly === true) result.error += ' Search required estimated illumination under a dark sky.'; + } return { ...result, action: 'next_satellite_pass', noradId: identity.noradId, name: identity.name, visibleOnly: args.visibleOnly === true, horizonHours: 24 }; } diff --git a/src/voice/gevActions.test.mjs b/src/voice/gevActions.test.mjs index 0ced3f9be..3adb086ce 100644 --- a/src/voice/gevActions.test.mjs +++ b/src/voice/gevActions.test.mjs @@ -3245,3 +3245,28 @@ test('analyst_query and get_current_view_state carry stale feed provenance', asy assert.equal(query.coverage.layersQueried[0].feedState, 'stale'); assert.match(query.feedProvenance.note, /do not describe this as live/i); }); + + +test('general satellite pass resolves once, refuses ambiguity, and preserves ISS call semantics', async () => { + const calls = []; + const pass = { riseMs: Date.now() + 60000, setMs: Date.now() + 360000, maxElevMs: Date.now() + 180000, maxElevDeg: 30, riseAzDeg: 90, visible: false }; + const layer = { + resolveSatelliteForPass(target) { return target === 'starlink' ? { status: 'ambiguous', candidates: [{ noradId: 1 }, { noradId: 2 }] } : { status: 'ok', noradId: 25544, name: 'ISS' }; }, + getNextSatellitePass(id, options) { calls.push({ id, ...options }); return { status: 'ok', pass }; }, + getNextIssPass(options) { calls.push(options); return { status: 'ok', pass }; }, + }; + const viewer = { clock: { onTick: { addEventListener: () => () => {} } }, scene: { canvas: { addEventListener() {}, removeEventListener() {} } }, camera: { moveEnd: { addEventListener() {} } } }; + const runner = createGevActionRunner({ viewer, styleManager: {}, dataManager: { layers: new Map([['satellites', { module: layer }]]) } }); + const ambiguous = await runner('next_satellite_pass', { target: 'starlink' }); + assert.equal(ambiguous.status, 'ambiguous'); + assert.equal(calls.length, 0); + const args = { latitude: 30, longitude: -97, minElevationDeg: 15 }; + const general = await runner('next_satellite_pass', { target: '25544', visibleOnly: true, ...args }); + assert.equal(general.action, 'next_satellite_pass'); + assert.deepEqual(calls[0], { id: 25544, latDeg: 30, lonDeg: -97, minElevDeg: 15, requireVisible: true }); + const iss = await runner('next_iss_pass', args); + assert.deepEqual(calls[1], { latDeg: 30, lonDeg: -97, minElevDeg: 15 }); + for (const key of ['observer', 'riseIso', 'minutesFromNow', 'durationMin', 'peakElevationDeg', 'riseDirection']) assert.deepEqual(iss[key], general[key]); + assert.equal(iss.visible, false); + assert.equal(iss.action, 'next_iss_pass'); +});