feat: route createNewSession through federation proxy for remote devices

- Accept remoteId parameter (defaults to '' via remoteId = remoteId || '')
- Compute endpoint: remoteId ? '/api/federation/{remoteId}/sessions' : '/api/sessions'
- Match sessions in poll loop via sessionKey (with fallback to name)
- Pass { remoteId } option to openSession for remote auto-open
- Add 3 tests verifying federation routing, remoteId passthrough, and sessionKey matching
This commit is contained in:
Brian Krabach
2026-04-04 08:09:02 -07:00
parent c28cb0a7f9
commit 44e47c5228
2 changed files with 69 additions and 4 deletions
+9 -4
View File
@@ -2039,9 +2039,11 @@ function showFabSessionInput() {
* @param {string} name - The session name to create. * @param {string} name - The session name to create.
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function createNewSession(name) { async function createNewSession(name, remoteId) {
remoteId = remoteId || '';
try { try {
const res = await api('POST', '/api/sessions', { name }); var endpoint = remoteId ? '/api/federation/' + encodeURIComponent(remoteId) + '/sessions' : '/api/sessions';
const res = await api('POST', endpoint, { name });
const data = await res.json(); const data = await res.json();
const sessionName = data.name || name; const sessionName = data.name || name;
showToast('Creating session \'' + sessionName + '\'…'); showToast('Creating session \'' + sessionName + '\'…');
@@ -2073,6 +2075,9 @@ async function createNewSession(name) {
return; return;
} }
// Compute expectedKey: for remote sessions, use 'remoteId:sessionName' (sessionKey format)
var expectedKey = remoteId ? (remoteId + ':' + sessionName) : sessionName;
// Poll until the session appears in _currentSessions (max 30s, every 2s) // Poll until the session appears in _currentSessions (max 30s, every 2s)
var attempts = 0; var attempts = 0;
var maxAttempts = 15; var maxAttempts = 15;
@@ -2080,13 +2085,13 @@ async function createNewSession(name) {
attempts++; attempts++;
await pollSessions(); await pollSessions();
var found = _currentSessions && _currentSessions.find(function(s) { var found = _currentSessions && _currentSessions.find(function(s) {
return s.name === sessionName; return (s.sessionKey || s.name) === expectedKey;
}); });
if (found) { if (found) {
clearInterval(pollForSession); clearInterval(pollForSession);
removeLoadingTile(); removeLoadingTile();
showToast('Session \'' + sessionName + '\' ready'); showToast('Session \'' + sessionName + '\' ready');
openSession(sessionName); openSession(sessionName, { remoteId: remoteId });
} else if (attempts >= maxAttempts) { } else if (attempts >= maxAttempts) {
clearInterval(pollForSession); clearInterval(pollForSession);
removeLoadingTile(); removeLoadingTile();
+60
View File
@@ -4231,3 +4231,63 @@ test('showFabSessionInput creates device select when multi_device_enabled with r
'showFabSessionInput must call createNewSession with name and remoteId arguments', 'showFabSessionInput must call createNewSession with name and remoteId arguments',
); );
}); });
// --- createNewSession federation routing (task-4) ---
test('createNewSession accepts remoteId parameter and routes to federation endpoint', () => {
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
// Extract createNewSession function body
const fnStart = source.indexOf('async function createNewSession(');
assert.ok(fnStart !== -1, 'createNewSession function must exist');
const fnBody = source.substring(fnStart, fnStart + 2000);
// Must accept remoteId parameter
assert.ok(
fnBody.includes('remoteId'),
'createNewSession must accept remoteId parameter',
);
// Must include federation endpoint path
assert.ok(
fnBody.includes('/api/federation/'),
'createNewSession must include /api/federation/ endpoint path for remote routing',
);
// Must still have local /api/sessions endpoint
assert.ok(
fnBody.includes('/api/sessions'),
'createNewSession must still use /api/sessions for local sessions',
);
});
test('createNewSession passes remoteId through to openSession for auto-open', () => {
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
const fnStart = source.indexOf('async function createNewSession(');
assert.ok(fnStart !== -1, 'createNewSession function must exist');
// Use 3000 chars to cover the full function including the polling interval callback
const fnBody = source.substring(fnStart, fnStart + 3000);
// Must call openSession with remoteId option
assert.ok(
fnBody.includes('openSession') && fnBody.includes('remoteId'),
'createNewSession must call openSession with remoteId option',
);
// Must pass remoteId as an option object to openSession
assert.ok(
fnBody.includes('{ remoteId') || fnBody.includes('{ remoteId:'),
'createNewSession must pass remoteId as option object to openSession',
);
});
test('createNewSession matches remote sessions by sessionKey in poll loop', () => {
const source = fs.readFileSync(new URL('../app.js', import.meta.url), 'utf8');
const fnStart = source.indexOf('async function createNewSession(');
assert.ok(fnStart !== -1, 'createNewSession function must exist');
const fnBody = source.substring(fnStart, fnStart + 2000);
// Must use sessionKey in the match logic (with fallback to name)
assert.ok(
fnBody.includes('sessionKey'),
'createNewSession poll loop must match remote sessions by sessionKey',
);
// Must use expectedKey or similar computed key for comparison
assert.ok(
fnBody.includes('expectedKey') || (fnBody.includes('sessionKey') && fnBody.includes('remoteId')),
'createNewSession must compute expected sessionKey for remote session matching',
);
});