// The socket is an ADAPTER, so it must take the interface rather than assume it.
//
// `favela-surface/index.js` already states the split this rests on: "favela-peer's `lib/rpc.js`
// exports `dispatch(node, cmd, a)` alongside `createServer(node, sock)`: `dispatch` is the interface
// and the socket is one ADAPTER over it." But `createServer` called `dispatch` directly, so the
// sentence was only half true — a composition that adds verbs could serve them over HTTP, as the
// Android worklet does, and had no way to serve the same set over the socket.
//
// ⛔ THIS TEACHES favela-peer NOTHING. It does not learn what the extra verbs are, or that any
// exist; it learns only that the thing it dispatches to is supplied rather than assumed. The daemon
// still knows nothing about projections, which is what test/boundary.test.js is there to hold.
//
// ⚠ Why it matters: on Android the surface and the daemon are one process and there is no socket at
// all (SELinux refuses an unprivileged app a `sock_file`), so the worklet composes with an in-process
// client. A desktop host wants the SAME composition plus a socket, for `favela-bridge` and the CLI.
// Without this, the desktop could only have the composition by re-implementing the server.
import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import net from '#node:net'
import { join } from '#node:path'
import { createServer } from '../lib/rpc.js'
const sockPath = () => join(fs.mkdtempSync(join(os.tmpdir(), 'favela-rpc-')), 'favela.sock')
function ask (path, cmd, args = {}) {
return new Promise((resolve, reject) => {
const socket = net.connect(path)
let buf = ''
socket.on('error', reject)
socket.on('connect', () => socket.write(JSON.stringify({ id: 1, cmd, args }) + '\n'))
socket.on('data', (chunk) => {
buf += chunk.toString()
const i = buf.indexOf('\n')
if (i === -1) return
socket.destroy()
resolve(JSON.parse(buf.slice(0, i)))
})
})
}
test('a supplied call handles the request instead of dispatch', async (t) => {
const sock = sockPath()
const seen = []
const server = createServer(null, sock, {
call: async (cmd, args) => { seen.push(cmd); return { echoed: cmd, got: args.n } }
})
t.teardown(() => { try { server.close() } catch {} })
const res = await ask(sock, 'a-verb-dispatch-does-not-have', { n: 7 })
t.ok(res.ok, 'the request succeeded, where dispatch would have thrown "unknown command"')
t.is(res.value.echoed, 'a-verb-dispatch-does-not-have', 'the supplied call saw the verb')
t.is(res.value.got, 7, 'and its arguments')
t.alike(seen, ['a-verb-dispatch-does-not-have'], 'exactly once')
})
test('with no call supplied it still dispatches, so nothing that exists changes', async (t) => {
const sock = sockPath()
// ⚠ A stub with a `peer`, because `dispatch` reads `node.peer` before the switch — so a null node
// throws a TypeError on the way IN and would not prove which arm answered. An unknown verb is not
// in MUTATIONS, so nothing else about the node is touched before the default arm refuses it.
const server = createServer({ peer: {} }, sock, {})
t.teardown(() => { try { server.close() } catch {} })
const res = await ask(sock, 'definitely-not-a-verb')
t.absent(res.ok, 'it failed, as before')
t.ok(/unknown command/.test(res.error), 'and failed through dispatch: ' + res.error)
})