Files
callstack__agent-device/test/integration/provider-scenarios/daemon-http-server.test.ts
devin-ai-integration[bot] 0a8ea3a57b refactor: consolidate architecture ownership and client results (#1210)
* refactor: consolidate architecture ownership and client results

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: keep selector parse chunk grouping current

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: update moved architecture breadcrumbs

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: enforce moved selector architecture

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix: keep selector guarantee ownership current

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs: update selector ownership references

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Michał Pierzchała <thymikee@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-07-11 09:40:24 +02:00

990 lines
31 KiB
TypeScript

import assert from 'node:assert/strict';
import crypto from 'node:crypto';
import fs from 'node:fs';
import http from 'node:http';
import os from 'node:os';
import path from 'node:path';
import { test } from 'vitest';
import { AppError } from '../../../src/kernel/errors.ts';
import {
cleanupUploadedArtifact,
prepareUploadedArtifact,
trackDownloadableArtifact,
} from '../../../src/daemon/artifact-tracking.ts';
import { DAEMON_RPC_PROTOCOL_VERSION } from '../../../src/daemon/http-health.ts';
import { createDaemonHttpServer } from '../../../src/daemon/server/http-server.ts';
import { emitRequestProgress } from '../../../src/request/progress.ts';
import { getRequestSignal, isRequestCanceled } from '../../../src/request/cancel.ts';
import type { DaemonRequest, DaemonResponse } from '../../../src/daemon/types.ts';
import {
closeLoopbackServer,
listenOnLoopback,
skipWhenLoopbackUnavailable,
} from '../../../src/__tests__/test-utils/loopback.ts';
import { restoreEnv } from './harness.ts';
type RpcResponse = {
status: number;
body: {
result?: DaemonResponse;
error?: {
code: number;
message: string;
data?: Record<string, unknown>;
};
};
};
type JsonRpcResponseEnvelope = {
type: 'response';
response: {
jsonrpc: '2.0';
id: string | number | null;
result?: DaemonResponse;
error?: {
code: number;
message: string;
data?: Record<string, unknown>;
};
};
};
const RPC_BODY_CAP_BYTES = 1024 * 1024;
test('Provider-backed integration daemon HTTP server maps RPC methods, auth, and request cancellation through the real transport', async (t) => {
if (await skipWhenLoopbackUnavailable(t, 'daemon HTTP integration coverage')) {
return;
}
const observedRequests: DaemonRequest[] = [];
let observedCanceled: boolean | undefined;
const server = await createDaemonHttpServer({
token: 'provider-scenario-token',
handleRequest: async (req): Promise<DaemonResponse> => {
observedRequests.push(req);
if (req.command === 'session_list') {
await new Promise((resolve) => setTimeout(resolve, 25));
observedCanceled = isRequestCanceled(req.meta?.requestId);
}
if (req.command === 'fail_me') {
throw new AppError('INVALID_ARGS', 'real transport rejected the request', {
command: req.command,
});
}
return {
ok: true,
data: {
command: req.command,
session: req.session,
meta: req.meta,
flags: req.flags,
},
};
},
});
try {
const port = await listenOnLoopback(server);
const health = await fetch(`http://127.0.0.1:${port}/health`);
assert.equal(health.status, 200);
const healthPayload = (await health.json()) as Record<string, unknown>;
assert.equal(healthPayload.ok, true);
assert.equal(healthPayload.service, 'agent-device-daemon');
assert.equal(typeof healthPayload.version, 'string');
assert.equal(healthPayload.rpcProtocolVersion, DAEMON_RPC_PROTOCOL_VERSION);
const command = await callRpc(port, {
jsonrpc: '2.0',
id: 'rpc-command',
method: 'agent_device.command',
params: {
session: 'default',
command: 'session_list',
positionals: [],
meta: { requestId: 'req-command' },
},
});
assert.equal(command.status, 200);
assert.equal(command.body.result?.ok, true);
assert.equal(observedCanceled, false);
const installFromUrl = await callRpc(port, {
jsonrpc: '2.0',
id: 'rpc-install-url',
method: 'agent_device.install_from_source',
params: {
session: 'bootstrap',
platform: 'android',
requestId: 'req-install-url',
retainPaths: true,
retentionMs: 30000,
source: {
kind: 'url',
url: 'https://example.com/app.apk',
headers: { authorization: 'Bearer signed-token' },
},
},
});
assert.equal(installFromUrl.status, 200);
const installFromGitHub = await callRpc(port, {
jsonrpc: '2.0',
id: 'rpc-install-github',
method: 'agent_device.install_from_source',
params: {
session: 'bootstrap',
platform: 'ios',
source: {
kind: 'github-actions-artifact',
owner: 'acme',
repo: 'mobile',
runId: '1234567890',
artifactName: 'ios-debug',
},
},
});
assert.equal(installFromGitHub.status, 200);
const lease = await callRpc(port, {
jsonrpc: '2.0',
id: 'rpc-lease',
method: 'agent_device.lease.allocate',
params: {
tenantId: 'Tenant A',
runId: 'run-1',
ttlMs: 60000,
backend: 'android-instance',
},
});
assert.equal(lease.status, 200);
const release = await callRpc(port, {
jsonrpc: '2.0',
id: 'rpc-release',
method: 'agent_device.release_materialized_paths',
params: {
session: 'bootstrap',
requestId: 'req-release',
materializationId: 'materialized-1',
},
});
assert.equal(release.status, 200);
const failure = await callRpc(port, {
jsonrpc: '2.0',
id: 'rpc-failure',
method: 'agent_device.command',
params: {
command: 'fail_me',
positionals: [],
},
});
assert.equal(failure.status, 400);
assert.equal(failure.body.error?.data?.code, 'INVALID_ARGS');
assert.equal(
(failure.body.error?.data?.details as Record<string, unknown> | undefined)?.command,
'fail_me',
);
const unsupported = await callRpc(port, {
jsonrpc: '2.0',
id: 'rpc-missing',
method: 'agent_device.missing',
params: {},
});
assert.equal(unsupported.status, 404);
assert.equal(unsupported.body.error?.code, -32601);
const installUrlRequest = observedRequests.find(
(req) => req.meta?.requestId === 'req-install-url',
);
assert.equal(installUrlRequest?.command, 'install_source');
assert.equal(installUrlRequest?.session, 'bootstrap');
assert.equal(installUrlRequest?.flags?.platform, 'android');
assert.equal(installUrlRequest?.meta?.retainMaterializedPaths, true);
assert.equal(installUrlRequest?.meta?.materializedPathRetentionMs, 30000);
assert.deepEqual(installUrlRequest?.meta?.installSource, {
kind: 'url',
url: 'https://example.com/app.apk',
headers: { authorization: 'Bearer signed-token' },
});
const githubRequest = observedRequests.find(
(req) => req.command === 'install_source' && req.flags?.platform === 'ios',
);
assert.deepEqual(githubRequest?.meta?.installSource, {
kind: 'github-actions-artifact',
owner: 'acme',
repo: 'mobile',
runId: 1234567890,
artifactName: 'ios-debug',
});
const leaseRequest = observedRequests.find((req) => req.command === 'lease_allocate');
assert.equal(leaseRequest?.meta?.tenantId, 'Tenant A');
assert.equal(leaseRequest?.meta?.leaseTtlMs, 60000);
assert.equal(leaseRequest?.meta?.leaseBackend, 'android-instance');
const releaseRequest = observedRequests.find(
(req) => req.command === 'release_materialized_paths',
);
assert.equal(releaseRequest?.meta?.materializationId, 'materialized-1');
} finally {
await closeLoopbackServer(server);
}
});
test('Provider-backed integration daemon HTTP server cancels progress streams when the client disconnects', async (t) => {
if (await skipWhenLoopbackUnavailable(t, 'daemon HTTP integration coverage')) {
return;
}
const requestId = 'req-http-progress-cancel';
let observedCanceledDuringAbort: boolean | undefined;
let handlerError: unknown;
let resolveHandlerDone: () => void = () => {};
const handlerDone = new Promise<void>((resolve) => {
resolveHandlerDone = resolve;
});
const server = await createDaemonHttpServer({
token: 'provider-scenario-token',
handleRequest: async (req): Promise<DaemonResponse> => {
try {
assert.equal(req.meta?.requestId, requestId);
emitRequestProgress({
type: 'replay-test',
file: 'cancel.ad',
status: 'pass',
index: 1,
total: 1,
});
const signal = getRequestSignal(requestId);
assert.ok(signal, 'request abort signal should be registered during streaming');
await waitForAbort(signal);
observedCanceledDuringAbort = isRequestCanceled(requestId);
return { ok: true, data: { canceled: observedCanceledDuringAbort } };
} catch (error) {
handlerError = error;
throw error;
} finally {
resolveHandlerDone();
}
},
});
try {
const port = await listenOnLoopback(server);
await withTimeout(
Promise.all([
abortStreamingRpcAfterFirstChunk(port, {
jsonrpc: '2.0',
id: 'rpc-cancel-progress',
method: 'agent_device.command',
params: {
command: 'session_list',
meta: { requestId, requestProgress: 'replay-test' },
},
}),
handlerDone,
]),
'streaming request cancellation',
);
if (handlerError) throw handlerError;
assert.equal(observedCanceledDuringAbort, true);
assert.equal(isRequestCanceled(requestId), false);
assert.equal(getRequestSignal(requestId), undefined);
} finally {
await closeLoopbackServer(server);
}
});
test('Provider-backed integration daemon HTTP server destroys oversized RPC request bodies', async (t) => {
if (await skipWhenLoopbackUnavailable(t, 'daemon HTTP integration coverage')) {
return;
}
let handled = false;
const server = await createDaemonHttpServer({
token: 'provider-scenario-token',
handleRequest: async (): Promise<DaemonResponse> => {
handled = true;
return { ok: true, data: {} };
},
});
try {
const port = await listenOnLoopback(server);
const oversizedBody = JSON.stringify({
jsonrpc: '2.0',
id: 'rpc-body-cap',
method: 'agent_device.command',
params: {
command: 'session_list',
padding: 'x'.repeat(RPC_BODY_CAP_BYTES),
},
});
assert.ok(oversizedBody.length > RPC_BODY_CAP_BYTES);
const outcome = await sendRawRpcBody(port, oversizedBody);
assert.equal(handled, false);
if (outcome.kind === 'response') {
assert.equal(outcome.status, 400);
assert.equal(JSON.parse(outcome.body).error.code, -32700);
} else {
assert.match(outcome.message, /aborted|reset|socket hang up|request too large/i);
}
const health = await fetch(`http://127.0.0.1:${port}/health`);
assert.equal(health.status, 200);
} finally {
await closeLoopbackServer(server);
}
});
test('Provider-backed integration daemon HTTP server validates malformed JSON-RPC envelopes', async (t) => {
if (await skipWhenLoopbackUnavailable(t, 'daemon HTTP integration coverage')) {
return;
}
let handled = false;
const server = await createDaemonHttpServer({
token: 'provider-scenario-token',
handleRequest: async (): Promise<DaemonResponse> => {
handled = true;
return { ok: true, data: {} };
},
});
try {
const port = await listenOnLoopback(server);
const cases = [
{
name: 'invalid JSON',
body: '{"jsonrpc":"2.0"',
status: 400,
code: -32700,
},
{
name: 'missing jsonrpc',
body: JSON.stringify({
id: 'rpc-missing-jsonrpc',
method: 'agent_device.command',
params: { command: 'session_list' },
}),
status: 400,
code: -32600,
},
{
name: 'unknown method',
body: JSON.stringify({
jsonrpc: '2.0',
id: 'rpc-unknown-method',
method: 'agent_device.nope',
params: {},
}),
status: 404,
code: -32601,
},
{
name: 'non-object params',
body: JSON.stringify({
jsonrpc: '2.0',
id: 'rpc-non-object-params',
method: 'agent_device.command',
params: 'session_list',
}),
status: 400,
code: -32602,
},
];
for (const testCase of cases) {
const response = await callRawRpc(port, testCase.body);
assert.equal(response.status, testCase.status, testCase.name);
assert.equal(response.body.error?.code, testCase.code, testCase.name);
}
assert.equal(handled, false);
} finally {
await closeLoopbackServer(server);
}
});
test('Provider-backed integration daemon HTTP server writes error envelopes after streaming headers are sent', async (t) => {
if (await skipWhenLoopbackUnavailable(t, 'daemon HTTP integration coverage')) {
return;
}
const server = await createDaemonHttpServer({
token: 'provider-scenario-token',
handleRequest: async (): Promise<DaemonResponse> => {
emitRequestProgress({
type: 'replay-test',
file: 'headers-sent.ad',
status: 'fail',
index: 1,
total: 1,
message: 'progress before failure',
});
throw new AppError('COMMAND_FAILED', 'stream failed after headers');
},
});
try {
const port = await listenOnLoopback(server);
const response = await fetch(`http://127.0.0.1:${port}/rpc`, {
method: 'POST',
headers: {
authorization: 'Bearer provider-scenario-token',
'content-type': 'application/json',
},
body: JSON.stringify({
jsonrpc: '2.0',
id: 'rpc-stream-error',
method: 'agent_device.command',
params: {
command: 'test',
meta: { requestId: 'req-stream-error', requestProgress: 'replay-test' },
},
}),
});
assert.equal(response.status, 200);
assert.equal(response.headers.get('content-type'), 'application/x-ndjson');
const envelopes = parseNdjson(await response.text());
assert.equal(envelopes.length, 2);
assert.equal(envelopes[0]?.type, 'progress');
const terminal = envelopes[1] as JsonRpcResponseEnvelope;
assert.equal(terminal.type, 'response');
assert.equal(terminal.response.error?.code, -32000);
assert.equal(terminal.response.error?.data?.code, 'COMMAND_FAILED');
assert.equal(terminal.response.error?.message, 'stream failed after headers');
} finally {
await closeLoopbackServer(server);
}
});
test('Provider-backed integration daemon HTTP server accepts uploads and streams downloadable artifacts', async (t) => {
if (await skipWhenLoopbackUnavailable(t, 'daemon HTTP integration coverage')) {
return;
}
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-http-server-'));
const downloadablePath = path.join(root, 'screen.png');
fs.writeFileSync(downloadablePath, 'png-binary');
const artifactId = trackDownloadableArtifact({
artifactPath: downloadablePath,
artifactType: 'screenshot',
fileName: 'screen.png',
});
const server = await createDaemonHttpServer({
token: 'provider-scenario-token',
handleRequest: async (): Promise<DaemonResponse> => ({ ok: true, data: {} }),
});
try {
const port = await listenOnLoopback(server);
const upload = await fetch(`http://127.0.0.1:${port}/upload`, {
method: 'POST',
headers: {
authorization: 'Bearer provider-scenario-token',
'x-artifact-type': 'file',
'x-artifact-filename': 'demo.apk',
'content-type': 'application/octet-stream',
},
body: Buffer.from('fake-apk'),
});
assert.equal(upload.status, 200);
const uploadBody = (await upload.json()) as { ok?: boolean; uploadId?: string };
assert.equal(uploadBody.ok, true);
assert.equal(typeof uploadBody.uploadId, 'string');
const downloaded = await fetch(`http://127.0.0.1:${port}/artifacts/${artifactId}`, {
headers: { authorization: 'Bearer provider-scenario-token' },
});
assert.equal(downloaded.status, 200);
assert.equal(await downloaded.text(), 'png-binary');
assert.match(downloaded.headers.get('content-disposition') ?? '', /screen\.png/);
const rejectedUpload = await fetch(`http://127.0.0.1:${port}/upload`, {
method: 'POST',
headers: {
authorization: 'Bearer wrong-token',
'x-artifact-type': 'file',
'x-artifact-filename': 'demo.apk',
},
body: Buffer.from('fake-apk'),
});
assert.equal(rejectedUpload.status, 401);
} finally {
await closeLoopbackServer(server);
fs.rmSync(root, { recursive: true, force: true });
}
});
test('Provider-backed integration daemon HTTP server resumes direct uploads before finalize', async (t) => {
if (await skipWhenLoopbackUnavailable(t, 'daemon HTTP integration coverage')) {
return;
}
const content = Buffer.from('fake-apk-resumable-content');
const server = await createDaemonHttpServer({
token: 'provider-scenario-token',
handleRequest: async (): Promise<DaemonResponse> => ({ ok: true, data: {} }),
});
let trackedUploadId = '';
try {
const port = await listenOnLoopback(server);
const preflight = await fetch(`http://127.0.0.1:${port}/upload/preflight`, {
method: 'POST',
headers: {
authorization: 'Bearer provider-scenario-token',
'content-type': 'application/json',
},
body: JSON.stringify({
uploadAttemptId: 'resume-before-finalize',
sha256: crypto.createHash('sha256').update(content).digest('hex'),
fileName: 'demo.apk',
sizeBytes: content.length,
artifactType: 'file',
platform: 'android',
contentType: 'application/octet-stream',
}),
});
assert.equal(preflight.status, 200);
const preflightBody = (await preflight.json()) as {
uploadId?: string;
upload?: { url?: string; headers?: Record<string, string> };
};
assert.equal(typeof preflightBody.uploadId, 'string');
assert.equal(typeof preflightBody.upload?.url, 'string');
if (!preflightBody.upload?.url) throw new Error('missing upload url');
const uploadUrl = preflightBody.upload.url;
const uploadHeaders = preflightBody.upload.headers ?? {};
const firstChunk = await fetch(uploadUrl, {
method: 'PUT',
headers: uploadHeaders,
body: content.subarray(0, 9),
});
assert.equal(firstChunk.status, 308);
assert.equal(firstChunk.headers.get('x-upload-offset'), '9');
const restartFromZero = await fetch(uploadUrl, {
method: 'PUT',
headers: uploadHeaders,
body: content,
});
assert.equal(restartFromZero.status, 308);
assert.equal(restartFromZero.headers.get('x-upload-offset'), '9');
const resumed = await fetch(uploadUrl, {
method: 'PUT',
headers: {
...uploadHeaders,
'content-range': `bytes 9-${content.length - 1}/${content.length}`,
},
body: content.subarray(9),
});
assert.equal(resumed.status, 200);
const finalize = await fetch(`http://127.0.0.1:${port}/upload/finalize`, {
method: 'POST',
headers: {
authorization: 'Bearer provider-scenario-token',
'content-type': 'application/json',
},
body: JSON.stringify({ uploadId: preflightBody.uploadId }),
});
assert.equal(finalize.status, 200);
const finalizeBody = (await finalize.json()) as { uploadId?: string };
trackedUploadId = finalizeBody.uploadId ?? '';
assert.equal(typeof trackedUploadId, 'string');
assert.deepEqual(fs.readFileSync(prepareUploadedArtifact(trackedUploadId)), content);
} finally {
if (trackedUploadId) cleanupUploadedArtifact(trackedUploadId);
await closeLoopbackServer(server);
}
});
test('Provider-backed integration daemon HTTP server isolates same-artifact upload attempts', async (t) => {
if (await skipWhenLoopbackUnavailable(t, 'daemon HTTP integration coverage')) {
return;
}
const content = Buffer.from('same-apk-content');
const server = await createDaemonHttpServer({
token: 'provider-scenario-token',
handleRequest: async (): Promise<DaemonResponse> => ({ ok: true, data: {} }),
});
const trackedUploadIds: string[] = [];
try {
const port = await listenOnLoopback(server);
const requestPreflight = async (uploadAttemptId: string) => {
const response = await fetch(`http://127.0.0.1:${port}/upload/preflight`, {
method: 'POST',
headers: {
authorization: 'Bearer provider-scenario-token',
'content-type': 'application/json',
},
body: JSON.stringify({
uploadAttemptId,
sha256: crypto.createHash('sha256').update(content).digest('hex'),
fileName: 'demo.apk',
sizeBytes: content.length,
artifactType: 'file',
platform: 'android',
contentType: 'application/octet-stream',
}),
});
assert.equal(response.status, 200);
const body = (await response.json()) as {
uploadId?: string;
upload?: { url?: string; headers?: Record<string, string> };
};
assert.equal(typeof body.uploadId, 'string');
assert.equal(typeof body.upload?.url, 'string');
if (!body.uploadId || !body.upload?.url) throw new Error('missing upload ticket');
return {
uploadId: body.uploadId,
uploadUrl: body.upload.url,
uploadHeaders: body.upload.headers ?? {},
};
};
const first = await requestPreflight('same-artifact-first');
const second = await requestPreflight('same-artifact-second');
assert.notEqual(first.uploadId, second.uploadId);
assert.notEqual(first.uploadUrl, second.uploadUrl);
const firstChunk = await fetch(first.uploadUrl, {
method: 'PUT',
headers: first.uploadHeaders,
body: content.subarray(0, 5),
});
assert.equal(firstChunk.status, 308);
assert.equal(firstChunk.headers.get('x-upload-offset'), '5');
const secondComplete = await fetch(second.uploadUrl, {
method: 'PUT',
headers: second.uploadHeaders,
body: content,
});
assert.equal(secondComplete.status, 200);
const secondFinalize = await finalizeUpload(port, second.uploadId);
trackedUploadIds.push(secondFinalize);
assert.deepEqual(fs.readFileSync(prepareUploadedArtifact(secondFinalize)), content);
const firstComplete = await fetch(first.uploadUrl, {
method: 'PUT',
headers: {
...first.uploadHeaders,
'content-range': `bytes 5-${content.length - 1}/${content.length}`,
},
body: content.subarray(5),
});
assert.equal(firstComplete.status, 200);
const firstFinalize = await finalizeUpload(port, first.uploadId);
trackedUploadIds.push(firstFinalize);
assert.deepEqual(fs.readFileSync(prepareUploadedArtifact(firstFinalize)), content);
} finally {
for (const uploadId of trackedUploadIds) cleanupUploadedArtifact(uploadId);
await closeLoopbackServer(server);
}
});
test('Provider-backed integration daemon HTTP server reports unknown upload tickets as expired', async (t) => {
if (await skipWhenLoopbackUnavailable(t, 'daemon HTTP integration coverage')) {
return;
}
const server = await createDaemonHttpServer({
token: 'provider-scenario-token',
handleRequest: async (): Promise<DaemonResponse> => ({ ok: true, data: {} }),
});
try {
const port = await listenOnLoopback(server);
const unknownUploadId = crypto.randomUUID();
const auth = { authorization: 'Bearer provider-scenario-token' };
const directChunk = await fetch(`http://127.0.0.1:${port}/upload/direct/${unknownUploadId}`, {
method: 'PUT',
headers: { ...auth, 'content-type': 'application/octet-stream' },
body: Buffer.from('late-chunk'),
});
assert.equal(directChunk.status, 500);
const directBody = (await directChunk.json()) as {
ok?: boolean;
code?: string;
error?: string;
};
assert.equal(directBody.ok, false);
assert.equal(directBody.code, 'COMMAND_FAILED');
assert.match(directBody.error ?? '', /not found or expired/);
const finalize = await fetch(`http://127.0.0.1:${port}/upload/finalize`, {
method: 'POST',
headers: { ...auth, 'content-type': 'application/json' },
body: JSON.stringify({ uploadId: unknownUploadId }),
});
assert.equal(finalize.status, 500);
const finalizeBody = (await finalize.json()) as {
ok?: boolean;
code?: string;
error?: string;
};
assert.equal(finalizeBody.ok, false);
assert.equal(finalizeBody.code, 'COMMAND_FAILED');
assert.match(finalizeBody.error ?? '', /not found or expired/);
} finally {
await closeLoopbackServer(server);
}
});
test('Provider-backed integration daemon HTTP auth hook can scope tenants and reject requests', async (t) => {
if (await skipWhenLoopbackUnavailable(t, 'daemon HTTP integration coverage')) {
return;
}
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-http-auth-hook-'));
const hookPath = path.join(root, 'auth-hook.mjs');
fs.writeFileSync(
hookPath,
`export default function authHook({ headers }) {
if (headers['x-reject'] === 'yes') return { ok: false, code: 'UNAUTHORIZED', message: 'tenant rejected' };
return { tenantId: headers['x-tenant'] || 'tenant-hook' };
}`,
);
const previousHook = process.env.AGENT_DEVICE_HTTP_AUTH_HOOK;
process.env.AGENT_DEVICE_HTTP_AUTH_HOOK = hookPath;
let observedRequest: DaemonRequest | undefined;
let server: Awaited<ReturnType<typeof createDaemonHttpServer>> | undefined;
try {
server = await createDaemonHttpServer({
handleRequest: async (req): Promise<DaemonResponse> => {
observedRequest = req;
return { ok: true, data: { meta: req.meta } };
},
});
const port = await listenOnLoopback(server);
const accepted = await callRpc(
port,
{
jsonrpc: '2.0',
id: 'rpc-auth-hook',
method: 'agent_device.command',
params: {
command: 'session_list',
flags: { sessionIsolation: 'tenant' },
},
},
{ 'x-tenant': 'tenant-hook' },
);
assert.equal(accepted.status, 200);
assert.equal(observedRequest?.meta?.tenantId, 'tenant-hook');
assert.equal(observedRequest?.meta?.sessionIsolation, 'tenant');
const rejected = await callRpc(
port,
{
jsonrpc: '2.0',
id: 'rpc-auth-reject',
method: 'agent_device.command',
params: {
command: 'session_list',
},
},
{ 'x-reject': 'yes' },
);
assert.equal(rejected.status, 401);
assert.equal(rejected.body.error?.data?.message, 'tenant rejected');
} finally {
if (server) {
await closeLoopbackServer(server);
}
restoreEnv('AGENT_DEVICE_HTTP_AUTH_HOOK', previousHook);
fs.rmSync(root, { recursive: true, force: true });
}
});
async function callRpc(
port: number,
payload: Record<string, unknown>,
headers: Record<string, string> = { authorization: 'Bearer provider-scenario-token' },
): Promise<RpcResponse> {
const response = await fetch(`http://127.0.0.1:${port}/rpc`, {
method: 'POST',
headers: {
'content-type': 'application/json',
...headers,
},
body: JSON.stringify(payload),
});
return {
status: response.status,
body: (await response.json()) as RpcResponse['body'],
};
}
async function finalizeUpload(port: number, uploadId: string): Promise<string> {
const response = await fetch(`http://127.0.0.1:${port}/upload/finalize`, {
method: 'POST',
headers: {
authorization: 'Bearer provider-scenario-token',
'content-type': 'application/json',
},
body: JSON.stringify({ uploadId }),
});
assert.equal(response.status, 200);
const body = (await response.json()) as { uploadId?: string };
const finalizedUploadId = body.uploadId;
if (typeof finalizedUploadId !== 'string') throw new Error('missing finalized upload id');
return finalizedUploadId;
}
async function callRawRpc(port: number, body: string): Promise<RpcResponse> {
const response = await fetch(`http://127.0.0.1:${port}/rpc`, {
method: 'POST',
headers: {
authorization: 'Bearer provider-scenario-token',
'content-type': 'application/json',
},
body,
});
return {
status: response.status,
body: (await response.json()) as RpcResponse['body'],
};
}
function abortStreamingRpcAfterFirstChunk(
port: number,
payload: Record<string, unknown>,
): Promise<void> {
return new Promise((resolve, reject) => {
let sawChunk = false;
let settled = false;
const settle = (error?: unknown) => {
if (settled) return;
settled = true;
if (error) reject(error);
else resolve();
};
const body = JSON.stringify(payload);
const req = http.request(
{
host: '127.0.0.1',
port,
path: '/rpc',
method: 'POST',
headers: {
authorization: 'Bearer provider-scenario-token',
'content-type': 'application/json',
'content-length': Buffer.byteLength(body),
},
},
(res) => {
res.setEncoding('utf8');
res.once('data', () => {
sawChunk = true;
res.destroy();
req.destroy();
});
res.on('close', () => {
if (sawChunk) settle();
});
res.on('error', (error) => {
if (sawChunk) settle();
else settle(error);
});
},
);
req.on('error', (error) => {
if (sawChunk) settle();
else settle(error);
});
req.end(body);
});
}
function sendRawRpcBody(
port: number,
body: string,
): Promise<
{ kind: 'response'; status: number; body: string } | { kind: 'error'; message: string }
> {
return new Promise((resolve) => {
const req = http.request(
{
host: '127.0.0.1',
port,
path: '/rpc',
method: 'POST',
headers: {
authorization: 'Bearer provider-scenario-token',
'content-type': 'application/json',
'content-length': Buffer.byteLength(body),
},
},
(res) => {
res.setEncoding('utf8');
let responseBody = '';
res.on('data', (chunk) => {
responseBody += chunk;
});
res.on('end', () => {
resolve({ kind: 'response', status: res.statusCode ?? 0, body: responseBody });
});
res.on('error', (error) => {
resolve({ kind: 'error', message: error.message });
});
},
);
req.on('error', (error) => {
resolve({ kind: 'error', message: error.message });
});
req.end(body);
});
}
function parseNdjson(body: string): Array<Record<string, unknown>> {
return body
.trim()
.split('\n')
.filter(Boolean)
.map((line) => JSON.parse(line) as Record<string, unknown>);
}
async function waitForAbort(signal: AbortSignal): Promise<void> {
if (signal.aborted) return;
await new Promise<void>((resolve) => {
signal.addEventListener('abort', () => resolve(), { once: true });
});
}
async function withTimeout<T>(promise: Promise<T>, label: string): Promise<T> {
let timeout: NodeJS.Timeout | undefined;
try {
return await Promise.race([
promise,
new Promise<never>((_, reject) => {
timeout = setTimeout(() => {
reject(new Error(`Timed out waiting for ${label}`));
}, 1_000);
}),
]);
} finally {
if (timeout) clearTimeout(timeout);
}
}