| … | … | @@ -0,0 +1,71 @@ |
| 1 | // The socket is an ADAPTER, so it must take the interface rather than assume it. |
| 2 | // |
| 3 | // `favela-surface/index.js` already states the split this rests on: "favela-peer's `lib/rpc.js` |
| 4 | // exports `dispatch(node, cmd, a)` alongside `createServer(node, sock)`: `dispatch` is the interface |
| 5 | // and the socket is one ADAPTER over it." But `createServer` called `dispatch` directly, so the |
| 6 | // sentence was only half true — a composition that adds verbs could serve them over HTTP, as the |
| 7 | // Android worklet does, and had no way to serve the same set over the socket. |
| 8 | // |
| 9 | // ⛔ THIS TEACHES favela-peer NOTHING. It does not learn what the extra verbs are, or that any |
| 10 | // exist; it learns only that the thing it dispatches to is supplied rather than assumed. The daemon |
| 11 | // still knows nothing about projections, which is what test/boundary.test.js is there to hold. |
| 12 | // |
| 13 | // ⚠ Why it matters: on Android the surface and the daemon are one process and there is no socket at |
| 14 | // all (SELinux refuses an unprivileged app a `sock_file`), so the worklet composes with an in-process |
| 15 | // client. A desktop host wants the SAME composition plus a socket, for `favela-bridge` and the CLI. |
| 16 | // Without this, the desktop could only have the composition by re-implementing the server. |
| 17 | |
| 18 | import test from 'brittle' |
| 19 | import fs from '#node:fs' |
| 20 | import os from '#node:os' |
| 21 | import net from '#node:net' |
| 22 | import { join } from '#node:path' |
| 23 | import { createServer } from '../lib/rpc.js' |
| 24 | |
| 25 | const sockPath = () => join(fs.mkdtempSync(join(os.tmpdir(), 'favela-rpc-')), 'favela.sock') |
| 26 | |
| 27 | function ask (path, cmd, args = {}) { |
| 28 | return new Promise((resolve, reject) => { |
| 29 | const socket = net.connect(path) |
| 30 | let buf = '' |
| 31 | socket.on('error', reject) |
| 32 | socket.on('connect', () => socket.write(JSON.stringify({ id: 1, cmd, args }) + '\n')) |
| 33 | socket.on('data', (chunk) => { |
| 34 | buf += chunk.toString() |
| 35 | const i = buf.indexOf('\n') |
| 36 | if (i === -1) return |
| 37 | socket.destroy() |
| 38 | resolve(JSON.parse(buf.slice(0, i))) |
| 39 | }) |
| 40 | }) |
| 41 | } |
| 42 | |
| 43 | test('a supplied call handles the request instead of dispatch', async (t) => { |
| 44 | const sock = sockPath() |
| 45 | const seen = [] |
| 46 | const server = createServer(null, sock, { |
| 47 | call: async (cmd, args) => { seen.push(cmd); return { echoed: cmd, got: args.n } } |
| 48 | }) |
| 49 | t.teardown(() => { try { server.close() } catch {} }) |
| 50 | |
| 51 | const res = await ask(sock, 'a-verb-dispatch-does-not-have', { n: 7 }) |
| 52 | |
| 53 | t.ok(res.ok, 'the request succeeded, where dispatch would have thrown "unknown command"') |
| 54 | t.is(res.value.echoed, 'a-verb-dispatch-does-not-have', 'the supplied call saw the verb') |
| 55 | t.is(res.value.got, 7, 'and its arguments') |
| 56 | t.alike(seen, ['a-verb-dispatch-does-not-have'], 'exactly once') |
| 57 | }) |
| 58 | |
| 59 | test('with no call supplied it still dispatches, so nothing that exists changes', async (t) => { |
| 60 | const sock = sockPath() |
| 61 | // ⚠ A stub with a `peer`, because `dispatch` reads `node.peer` before the switch — so a null node |
| 62 | // throws a TypeError on the way IN and would not prove which arm answered. An unknown verb is not |
| 63 | // in MUTATIONS, so nothing else about the node is touched before the default arm refuses it. |
| 64 | const server = createServer({ peer: {} }, sock, {}) |
| 65 | t.teardown(() => { try { server.close() } catch {} }) |
| 66 | |
| 67 | const res = await ask(sock, 'definitely-not-a-verb') |
| 68 | |
| 69 | t.absent(res.ok, 'it failed, as before') |
| 70 | t.ok(/unknown command/.test(res.error), 'and failed through dispatch: ' + res.error) |
| 71 | }) |