diff --git a/docs/adr/0033-collocation-decision-table.md b/docs/adr/0033-collocation-decision-table.md index dd0a854062..5a1dd88ec6 100644 --- a/docs/adr/0033-collocation-decision-table.md +++ b/docs/adr/0033-collocation-decision-table.md @@ -201,16 +201,13 @@ its owner: the route owner-file claims sit beside the per-command claims in `@agent-device/command-registry/owner-files`, and the wire request and response types come from `@agent-device/contracts/command` and `@agent-device/kernel/contracts`. -Root tests that still drive daemon internals reach them through transitional `./testing/` -entries: named `./testing/` entries for production modules, named `./testing/` -entries, and per fixture folder a `./testing//*` pattern. Each batch of tests that moves into -`packages/daemon/src`, where they import their subject relatively, deletes the entries it frees. -R11 refuses any `./testing/` import from a non-test file, so root production code cannot import -a module the package publishes for tests (a package test double reached through the boundary is -production code depending on a fake). `resolveWorkspaceSpecifier` (layering model) owns Node's -exports semantics — the layering edges, the depgraph, mutation ownership, and R11's named-surface -check call it, and the Stryker alias table orders its Vite first-match list to resolve in exactly -its order — so every lane gives a specifier the module id Node's exports resolution gives it. +The transitional `./testing/` entries are gone, last of them behind the consumers that moved off +them in [#3728](https://github.com/callstack/agent-device/pull/3728), +[#3733](https://github.com/callstack/agent-device/pull/3733), and +[#3742](https://github.com/callstack/agent-device/pull/3742). +`scripts/layering/package-entry-surface.ts` owns the surface under R83, so this document states the +decision and not the check. Package tests import their subject relatively; composed root tests reach +the daemon through `startDaemonRuntime` from the entry. ## Refuted alternatives diff --git a/packages/daemon/package.json b/packages/daemon/package.json index 0a76e7357c..419c2a6cf0 100644 --- a/packages/daemon/package.json +++ b/packages/daemon/package.json @@ -35,78 +35,6 @@ ".": { "types": "./src/index.ts", "default": "./src/index.ts" - }, - "./testing/__tests__/support/session-factories": { - "types": "./src/__tests__/support/session-factories.ts", - "default": "./src/__tests__/support/session-factories.ts" - }, - "./testing/__tests__/support/store-factory": { - "types": "./src/__tests__/support/store-factory.ts", - "default": "./src/__tests__/support/store-factory.ts" - }, - "./testing/__tests__/support/unavailable-device-runtime-gateway": { - "types": "./src/__tests__/support/unavailable-device-runtime-gateway.ts", - "default": "./src/__tests__/support/unavailable-device-runtime-gateway.ts" - }, - "./testing/artifact-tracking": { - "types": "./src/resources/artifact-tracking.ts", - "default": "./src/resources/artifact-tracking.ts" - }, - "./testing/context": { - "types": "./src/core/context.ts", - "default": "./src/core/context.ts" - }, - "./testing/interaction/index": { - "types": "./src/execution/interaction/index.ts", - "default": "./src/execution/interaction/index.ts" - }, - "./testing/lease-registry": { - "types": "./src/resources/lease-registry.ts", - "default": "./src/resources/lease-registry.ts" - }, - "./testing/provider-device-admission": { - "types": "./src/core/provider-device-admission.ts", - "default": "./src/core/provider-device-admission.ts" - }, - "./testing/provider-lease-expiry": { - "types": "./src/server/provider-lease-expiry.ts", - "default": "./src/server/provider-lease-expiry.ts" - }, - "./testing/ref-frame": { - "types": "./src/core/ref-frame.ts", - "default": "./src/core/ref-frame.ts" - }, - "./testing/request-dispatch-ledger": { - "types": "./src/core/request-dispatch-ledger.ts", - "default": "./src/core/request-dispatch-ledger.ts" - }, - "./testing/request-finalization": { - "types": "./src/server/request-finalization.ts", - "default": "./src/server/request-finalization.ts" - }, - "./testing/request-runtime-binding": { - "types": "./src/core/request-runtime-binding.ts", - "default": "./src/core/request-runtime-binding.ts" - }, - "./testing/server/http-server": { - "types": "./src/server/http/http-server.ts", - "default": "./src/server/http/http-server.ts" - }, - "./testing/session-action-recorder": { - "types": "./src/core/session-action-recorder.ts", - "default": "./src/core/session-action-recorder.ts" - }, - "./testing/session-snapshot": { - "types": "./src/execution/session-snapshot.ts", - "default": "./src/execution/session-snapshot.ts" - }, - "./testing/session-state": { - "types": "./src/core/session-state.ts", - "default": "./src/core/session-state.ts" - }, - "./testing/session-store": { - "types": "./src/core/session-store.ts", - "default": "./src/core/session-store.ts" } } } diff --git a/packages/daemon/src/server/__tests__/request-router-screenshot.test.ts b/packages/daemon/src/server/__tests__/request-router-screenshot.test.ts index 9c6c77165c..286bab4bad 100644 --- a/packages/daemon/src/server/__tests__/request-router-screenshot.test.ts +++ b/packages/daemon/src/server/__tests__/request-router-screenshot.test.ts @@ -341,7 +341,7 @@ test('non-iOS screenshot response tolerates malformed PNG metadata', async () => } }); -test('iOS simulator screenshot omits logical density metadata after --scale downscale', async () => { +test('iOS simulator screenshot suppresses logical density metadata when --scale is set', async () => { const screenshotPath = path.join( mkdtempForTestSync('agent-device-ios-scale'), `agent-device-ios-scale-${Date.now()}.png`, diff --git a/scripts/layering/check.ts b/scripts/layering/check.ts index dd1f39ef20..45bc916381 100644 --- a/scripts/layering/check.ts +++ b/scripts/layering/check.ts @@ -144,7 +144,7 @@ import { checkPackageExportSurface, packageExportSurfaceSummary, } from './package-export-surface.ts'; -import { checkTestKitIsolation } from './test-kit-isolation.ts'; +import { checkPackageEntrySurfaces } from './package-entry-surface.ts'; import { checkTestColocation, testColocationSummary } from './test-colocation.ts'; import { checkDaemonLayers, DAEMON_ROOT } from './daemon-layers.ts'; import { checkSessionAuthorityOverlay, handlerOwnedOverlay } from './session-authority-overlay.ts'; @@ -624,7 +624,7 @@ export const LAYERING_RULES: Readonly> = { context.ratchets.testOnlyExports, context.reference.testOnlyExports, ), - ...checkTestKitIsolation( + ...checkPackageEntrySurfaces( parseManifests(readTrackedPackageManifests(repoRoot)), context.allTypeScriptSources, ), diff --git a/scripts/layering/test-kit-isolation.test.ts b/scripts/layering/package-entry-surface.test.ts similarity index 62% rename from scripts/layering/test-kit-isolation.test.ts rename to scripts/layering/package-entry-surface.test.ts index 59aa0a4944..3efb9d23a4 100644 --- a/scripts/layering/test-kit-isolation.test.ts +++ b/scripts/layering/package-entry-surface.test.ts @@ -4,7 +4,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { parseManifests } from './package-isolation.ts'; -import { checkTestKitIsolation } from './test-kit-isolation.ts'; +import { checkPackageEntrySurfaces } from './package-entry-surface.ts'; type Manifest = Record; @@ -25,7 +25,10 @@ function workspace(overrides: Record = {}): Map = {}): Map = {}, sources = SOURCE) { - return checkTestKitIsolation(parseManifests(workspace(overrides)), sources); + return checkPackageEntrySurfaces(parseManifests(workspace(overrides)), sources); } test('the test-kit shape holds: one entry, built only on packages below it', () => { @@ -63,10 +70,41 @@ test('test-kit exports exactly "." and nothing else', () => { assert.equal(check({ 'test-kit': { exports: { './tmp-dir': entry('./x.ts') } } }).length, 1); }); +test('daemon exports exactly "." and nothing else', () => { + const extra = check({ + daemon: { + exports: { + '.': entry('./src/index.ts'), + './testing/context': entry('./src/core/context.ts'), + }, + }, + }); + assert.equal(extra.length, 1); + assert.match(extra[0]!.file, /packages\/daemon\/package\.json/); + assert.match(extra[0]!.message, /exports exactly "\."/); + assert.deepEqual(check(), []); +}); + +test('every daemon condition of "." points at src/index.ts', () => { + for (const exports of [ + { '.': entry('./src/other.ts') }, + { '.': { types: './src/index.ts', default: './src/other.ts' } }, + { '.': './src/other.ts' }, + ]) { + const found = check({ daemon: { exports } }); + assert.equal(found.length, 1, JSON.stringify(exports)); + assert.match(found[0]!.file, /packages\/daemon\/package\.json/); + assert.match(found[0]!.message, /every condition of it is \.\/src\/index\.ts/); + } + assert.deepEqual(check({ daemon: { exports: { '.': './src/index.ts' } } }), []); +}); + test('the entry names its exports: export * is refused', () => { for (const body of [ "export * from './a.ts';", "export { a } from './a.ts';\nexport * as ns from './b.ts';", + "export type * from './a.ts';", + "export { a } from './a.ts';\nexport type * as ns from './b.ts';", ]) { const found = check({}, new Map([['packages/test-kit/src/index.ts', body]])); assert.equal(found.length, 1, body); @@ -78,6 +116,23 @@ test('the entry names its exports: export * is refused', () => { ); }); +test('text that only mentions a star is not an export', () => { + const body = + "// export * from './a.ts' is forbidden here\n" + + "const note = `export * from './b.ts'`;\nexport { note };\nexport { a } from './a.ts';"; + assert.deepEqual(check({}, new Map([['packages/test-kit/src/index.ts', body]])), []); +}); + +test('the daemon entry names its exports: export * is refused', () => { + const sources = new Map(SOURCE); + sources.set('packages/daemon/src/index.ts', "export * from './server/http/daemon-runtime.ts';"); + const found = check({}, sources); + assert.equal(found.length, 1); + assert.equal(found[0]!.file, 'packages/daemon/src/index.ts'); + assert.match(found[0]!.message, /names every export explicitly/); + assert.deepEqual(check(), []); +}); + test('every condition of the "." entry must point at src/index.ts', () => { for (const exports of [ { '.': entry('./src/other.ts') }, diff --git a/scripts/layering/package-entry-surface.ts b/scripts/layering/package-entry-surface.ts new file mode 100644 index 0000000000..ea83d44a16 --- /dev/null +++ b/scripts/layering/package-entry-surface.ts @@ -0,0 +1,121 @@ +// Catches: a pinned package entry growing a sibling subpath, pointing somewhere other than its +// declared entry file, or re-exporting a module's whole surface with `export *`. test-kit is the +// one home of test helpers shared across packages, so a second entry there publishes a fixture's +// surface into every test's import graph. The daemon's entry is its package boundary and the one +// place a barrel is allowed, so a subpath or a star re-export there publishes the daemon graph +// under an unowned name (ADR 0033). +// Evidence: both packages held the shape by hand until now; test-kit's single entry and the +// daemon's `.` are each asserted by the same declaration. +// Cost: one pass over the pinned manifests and their entry sources. +// Kill criterion: a maintainer decision that a pinned package may expose subpath entries or a star +// re-export from its entry. +// +// Part of R83 package-export-surface. The dependency, import and reference-cycle half is the +// shared walk in package-isolation.ts under TEST_KIT_ISOLATION. + +import { parseSync } from 'oxc-parser'; +import { visitAst } from './layering-ast.ts'; +import type { LayeringViolation } from './model.ts'; +import { PACKAGE_EXPORT_SURFACE_RULE, TEST_KIT_PACKAGE } from './package-export-surface.ts'; +import { + checkPackageIsolation, + TEST_KIT_ISOLATION, + type ParsedManifests, +} from './package-isolation.ts'; + +const TEST_KIT_DIR = TEST_KIT_ISOLATION.dir; +const TEST_KIT_ENTRY = './src/index.ts'; +const DAEMON_DIR = 'packages/daemon'; +const DAEMON_PACKAGE = '@agent-device/daemon'; + +const ENTRY_SURFACES = [ + { packageName: TEST_KIT_PACKAGE, dir: TEST_KIT_DIR, entry: TEST_KIT_ENTRY }, + { packageName: DAEMON_PACKAGE, dir: DAEMON_DIR, entry: './src/index.ts' }, +] as const; + +function stringLeaves(value: unknown): string[] { + if (typeof value === 'string') return [value]; + if (value && typeof value === 'object') return Object.values(value).flatMap(stringLeaves); + return []; +} + +function violation(file: string, message: string, line = 1): LayeringViolation { + return { rule: PACKAGE_EXPORT_SURFACE_RULE, file, line, message }; +} + +/** Keeps test-kit and daemon on one named public entry; test-kit also runs the isolation walk. */ +export function checkPackageEntrySurfaces( + manifests: ParsedManifests, + sources: ReadonlyMap, +): LayeringViolation[] { + const testKitFile = `${TEST_KIT_DIR}/package.json`; + const violations = ENTRY_SURFACES.flatMap((entrySurface) => + checkEntrySurface(entrySurface, manifests, sources), + ); + if (manifests.has(testKitFile)) { + violations.push(...checkPackageIsolation(manifests, sources, TEST_KIT_ISOLATION)); + } + return violations; +} + +function checkEntrySurface( + entrySurface: (typeof ENTRY_SURFACES)[number], + manifests: ParsedManifests, + sources: ReadonlyMap, +): LayeringViolation[] { + const manifestFile = `${entrySurface.dir}/package.json`; + const manifest = manifests.get(manifestFile); + if (!manifest) return []; + + const violations: LayeringViolation[] = []; + const exported = (manifest.exports as Record | undefined) ?? {}; + const exportKeys = Object.keys(exported); + const entryTargets = stringLeaves(exported['.']); + if ( + exportKeys.length !== 1 || + exportKeys[0] !== '.' || + entryTargets.length === 0 || + entryTargets.some((target) => target !== entrySurface.entry) + ) { + violations.push( + violation( + manifestFile, + `${entrySurface.packageName} exports exactly "." and every condition of it is ` + + `${entrySurface.entry} (found ${exportKeys.join(', ') || 'none'} -> ` + + `${entryTargets.join(', ') || 'nothing'}).`, + ), + ); + } + + const entryFile = `${entrySurface.dir}/src/index.ts`; + const entrySource = sources.get(entryFile); + const starLine = entrySource === undefined ? null : findStarExportLine(entryFile, entrySource); + if (starLine !== null) { + violations.push( + violation( + entryFile, + `${entrySurface.packageName} names every export explicitly: \`export *\` is forbidden.`, + starLine, + ), + ); + } + return violations; +} + +/** + * The line of the first star re-export in an entry's source, or null when it names everything. + * Read as syntax rather than text, for the reason `facade-exports.ts` records from #1555: a form + * scanner has to enumerate every export FORM by hand and the one it forgets slips through. + * `ExportAllDeclaration` is the one node every star shape builds — `export *`, `export * as ns`, + * and their `export type` forms, which publish a module's surface under one or zero names — while a + * comment or template literal that merely mentions a star declares no node at all. + */ +function findStarExportLine(entryFile: string, source: string): number | null { + let start: number | null = null; + visitAst(parseSync(entryFile, source).program, (node) => { + if (start === null && node.type === 'ExportAllDeclaration' && typeof node.start === 'number') { + start = node.start; + } + }); + return start === null ? null : source.slice(0, start).split('\n').length; +} diff --git a/scripts/layering/test-kit-isolation.ts b/scripts/layering/test-kit-isolation.ts deleted file mode 100644 index 42b760c020..0000000000 --- a/scripts/layering/test-kit-isolation.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Catches: `@agent-device/test-kit` growing a second entry, a star re-export, or a dependency on -// the code it exists to test. test-kit is the one home of test helpers shared across packages and -// every helper is a named export of its single entry, so a fixture that needs a platform, -// provider, daemon or root module's code is a test of that package and belongs in it. -// Evidence: the single-entry test-kit loads every helper eagerly, so a second entry or a star -// re-export would publish a module's whole surface into every test's import graph. -// Cost: one pass over the tracked manifests and test-kit's entry. -// Kill criterion: a maintainer decision that test-kit may expose subpath entries or depend on -// platform, provider, daemon or root code. -// -// Part of R83 package-export-surface. The dependency, import and reference-cycle half is the -// shared walk in package-isolation.ts under TEST_KIT_ISOLATION. - -import type { LayeringViolation } from './model.ts'; -import { PACKAGE_EXPORT_SURFACE_RULE, TEST_KIT_PACKAGE } from './package-export-surface.ts'; -import { - checkPackageIsolation, - TEST_KIT_ISOLATION, - type ParsedManifests, -} from './package-isolation.ts'; - -const TEST_KIT_DIR = TEST_KIT_ISOLATION.dir; -const TEST_KIT_ENTRY = './src/index.ts'; - -function stringLeaves(value: unknown): string[] { - if (typeof value === 'string') return [value]; - if (value && typeof value === 'object') return Object.values(value).flatMap(stringLeaves); - return []; -} - -function violation(file: string, message: string, line = 1): LayeringViolation { - return { rule: PACKAGE_EXPORT_SURFACE_RULE, file, line, message }; -} - -/** - * The shape of the one shared test package: a single `.` entry that names its exports, plus the - * shared isolation walk (no platform, provider, daemon or root package reached directly, through - * other packages or by import; no package it reaches declaring it back). - */ -export function checkTestKitIsolation( - manifests: ParsedManifests, - sources: ReadonlyMap, -): LayeringViolation[] { - const violations: LayeringViolation[] = []; - const testKitFile = `${TEST_KIT_DIR}/package.json`; - const testKit = manifests.get(testKitFile); - if (!testKit) return violations; - - const exported = (testKit.exports as Record | undefined) ?? {}; - const exportKeys = Object.keys(exported); - const entryTargets = stringLeaves(exported['.']); - if ( - exportKeys.length !== 1 || - exportKeys[0] !== '.' || - entryTargets.length === 0 || - entryTargets.some((target) => target !== TEST_KIT_ENTRY) - ) { - violations.push( - violation( - testKitFile, - `${TEST_KIT_PACKAGE} exports exactly "." and every condition of it is ${TEST_KIT_ENTRY} ` + - `(found ${exportKeys.join(', ') || 'none'} -> ${entryTargets.join(', ') || 'nothing'}): a ` + - `shared helper is a named export of src/index.ts, not a subpath.`, - ), - ); - } - violations.push(...checkPackageIsolation(manifests, sources, TEST_KIT_ISOLATION)); - - const entrySource = sources.get(`${TEST_KIT_DIR}/src/index.ts`); - const starExport = entrySource === undefined ? null : /^[ \t]*export\s*\*/m.exec(entrySource); - if (entrySource !== undefined && starExport) { - const line = entrySource.slice(0, starExport.index).split('\n').length; - violations.push( - violation( - `${TEST_KIT_DIR}/src/index.ts`, - `${TEST_KIT_PACKAGE} names every export explicitly: \`export *\` would publish a module's whole surface.`, - line, - ), - ); - } - return violations; -} diff --git a/src/commands/capture/runtime/screenshot.test.ts b/src/commands/capture/runtime/screenshot.test.ts index 49efcb169b..147122abc0 100644 --- a/src/commands/capture/runtime/screenshot.test.ts +++ b/src/commands/capture/runtime/screenshot.test.ts @@ -34,8 +34,53 @@ test('runtime screenshot applies scale and forwards the capture rotation', async assert.equal(result.displayRotation, 'landscape-left'); }); +test('runtime screenshot scales the already-cropped backend capture', async () => { + const outPath = path.join(mkdtempForTestSync('runtime-screenshot-crop-order-'), 'cropped.png'); + const device = createAgentDevice({ + backend: { + platform: 'ios', + captureScreenshot: async (_context, requestedPath) => { + const source = solidPngBuffer(100, 50); + fs.writeFileSync( + requestedPath, + cropPngBuffer(source, { x: 10, y: 10, width: 40, height: 20 }), + ); + }, + } satisfies AgentDeviceBackend, + artifacts: createLocalArtifactAdapter(), + sessions: { get: () => undefined, set: () => {} }, + policy: localCommandPolicy(), + }); + + fs.writeFileSync(outPath, solidPngBuffer(100, 50)); + await device.capture.screenshot({ + out: { kind: 'path', path: outPath }, + scale: 0.5, + }); + const scaledCrop = PNG.sync.read(fs.readFileSync(outPath)); + + assert.deepEqual( + { width: scaledCrop.width, height: scaledCrop.height }, + { width: 20, height: 10 }, + ); +}); + function solidPngBuffer(width: number, height: number): Buffer { const png = new PNG({ width, height }); for (let index = 3; index < png.data.length; index += 4) png.data[index] = 255; return PNG.sync.write(png); } + +function cropPngBuffer( + source: Buffer, + crop: Readonly<{ x: number; y: number; width: number; height: number }>, +): Buffer { + const image = PNG.sync.read(source); + const cropped = new PNG({ width: crop.width, height: crop.height }); + for (let row = 0; row < crop.height; row += 1) { + const sourceStart = ((crop.y + row) * image.width + crop.x) * 4; + const targetStart = row * crop.width * 4; + image.data.copy(cropped.data, targetStart, sourceStart, sourceStart + crop.width * 4); + } + return PNG.sync.write(cropped); +} diff --git a/test/integration/provider-scenarios/harness-proxy.ts b/test/integration/provider-scenarios/harness-proxy.ts index 4d8bfc4a70..7c118eca51 100644 --- a/test/integration/provider-scenarios/harness-proxy.ts +++ b/test/integration/provider-scenarios/harness-proxy.ts @@ -1,11 +1,14 @@ import type { DaemonWireRequest } from '@agent-device/contracts/command'; -import type { DaemonResponse } from '@agent-device/kernel/contracts'; import http from 'node:http'; import { createDaemonProxyServer, type DaemonProxyAuthenticate } from '@agent-device/proxy'; import { buildHttpRpcPayload } from '../../../src/daemon-client/daemon-client-rpc.ts'; import { withClientReplayScriptSources } from '../../../src/__tests__/test-utils/replay-script-source.fixtures.ts'; import { closeLoopbackServer, listenOnLoopback } from '@agent-device/test-kit'; -import { commandRequest, responseToRpcResult } from './harness-rpc.ts'; +import { + commandRequest, + envelopeToRpcResult, + type JsonRpcResponseEnvelope, +} from './harness-rpc.ts'; import { type ProviderScenarioHarness, type ProviderScenarioRpcResult } from './harness.ts'; export type ProviderScenarioForwardedRequest = { @@ -46,11 +49,6 @@ export type ProviderScenarioProxyOptions = | { clientToken: string; authenticate?: never } | { clientToken?: never; authenticate: DaemonProxyAuthenticate }; -type JsonRpcResponseEnvelope = { - result?: DaemonResponse; - error?: { code: number; message: string; data?: Record }; -}; - export async function createProviderScenarioProxy( daemon: ProviderScenarioHarness, options: ProviderScenarioProxyOptions, @@ -98,15 +96,7 @@ export async function createProviderScenarioProxy( const result = await requestRpc(authToken, payload); const envelope = result.json as JsonRpcResponseEnvelope; const id = String(payload.id); - if (envelope.result) return responseToRpcResult(envelope.result, id, result.statusCode); - return { - statusCode: result.statusCode, - json: { - jsonrpc: '2.0', - id, - error: envelope.error ?? { code: result.statusCode, message: '' }, - }, - }; + return envelopeToRpcResult(envelope, id, result.statusCode); }, close: async () => { if (closed) return; diff --git a/test/integration/provider-scenarios/harness-rpc.ts b/test/integration/provider-scenarios/harness-rpc.ts index 0fbcf68b77..34d3e86505 100644 --- a/test/integration/provider-scenarios/harness-rpc.ts +++ b/test/integration/provider-scenarios/harness-rpc.ts @@ -2,6 +2,11 @@ import type { DaemonWireRequest } from '@agent-device/contracts/command'; import type { DaemonResponse } from '@agent-device/kernel/contracts'; import type { ProviderScenarioRpcResult } from './harness.ts'; +export type JsonRpcResponseEnvelope = { + result?: DaemonResponse; + error?: { code: number; message: string; data?: Record }; +}; + export function commandRequest( token: string, command: string, @@ -26,7 +31,7 @@ export function commandRequest( }; } -export function responseToRpcResult( +function responseToRpcResult( response: DaemonResponse, id: string, statusCode: number, @@ -50,3 +55,19 @@ export function responseToRpcResult( }, }; } + +export function envelopeToRpcResult( + envelope: JsonRpcResponseEnvelope, + id: string | number, + statusCode: number, +): ProviderScenarioRpcResult { + if (envelope.result) return responseToRpcResult(envelope.result, String(id), statusCode); + return { + statusCode, + json: { + jsonrpc: '2.0', + id, + error: envelope.error ?? { code: statusCode, message: '' }, + }, + }; +} diff --git a/test/integration/provider-scenarios/harness.ts b/test/integration/provider-scenarios/harness.ts index 9f76509ef9..187f41b587 100644 --- a/test/integration/provider-scenarios/harness.ts +++ b/test/integration/provider-scenarios/harness.ts @@ -21,7 +21,11 @@ import { buildHttpRpcPayload, handleDaemonHttpResponseBody, } from '../../../src/daemon-client/daemon-client-rpc.ts'; -import { commandRequest, responseToRpcResult } from './harness-rpc.ts'; +import { + commandRequest, + envelopeToRpcResult, + type JsonRpcResponseEnvelope, +} from './harness-rpc.ts'; import { createDaemonRuntimeDeps } from '../../../src/daemon-composition.ts'; import { createPlatformRuntimeGateway, @@ -52,11 +56,6 @@ const PROVIDER_SCENARIO_TEMP_REMOVE_OPTIONS = { export type ProviderScenarioRpcResult = { statusCode: number; json: any }; -type JsonRpcResponseEnvelope = { - result?: DaemonResponse; - error?: { code: number; message: string; data?: Record }; -}; - export type ProviderScenarioHarness = { callCommand: ( command: string, @@ -264,17 +263,7 @@ export async function createProviderScenarioHarness( }); const envelope = JSON.parse(body) as JsonRpcResponseEnvelope; if (envelope.result) assertNoInternalChromeProvenance(envelope.result); - if (envelope.result) { - return responseToRpcResult(envelope.result, String(payload.id), statusCode); - } - return { - statusCode, - json: { - jsonrpc: '2.0', - id: payload.id, - error: envelope.error ?? { code: statusCode, message: '' }, - }, - }; + return envelopeToRpcResult(envelope, payload.id, statusCode); }, client: () => client, hasSession: async (name = 'default') =>