Skip to content
8 changes: 4 additions & 4 deletions packages/contracts/src/dispatch-disclosure.fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ export const DISPATCH_DISCLOSURE_TABLE_PATH = path.join(
* followed by `test(\`${row.id}`, which {@link DISPATCH_DISCLOSURE_ROW_LOOP} matches.
*/
export const DISPATCH_DISCLOSURE_DRIVER_OWNERS: Readonly<Record<string, string>> = {
'daemon.': 'src/daemon/interaction/internal/__tests__/interaction-dispatch-disclosure.test.ts',
'daemon.': 'src/__tests__/daemon-composition/interaction/interaction-dispatch-disclosure.test.ts',
'daemon.scroll-no-progress': 'src/daemon/__tests__/scroll-movement.test.ts',
'daemon.route.': 'src/daemon/__tests__/request-dispatch-disclosure.test.ts',
'daemon.route.': 'src/__tests__/daemon-composition/request-dispatch-disclosure.test.ts',
'post-action-guard.':
'src/daemon/interaction/internal/__tests__/interaction-dispatch-disclosure.test.ts',
'src/__tests__/daemon-composition/interaction/interaction-dispatch-disclosure.test.ts',
'ios-runner.': 'packages/platform-apple/src/runner/__tests__/runner-dispatch-disclosure.test.ts',
'ios-runner.pre-send.':
'packages/platform-apple/src/runner/__tests__/runner-lifecycle-dispatch-disclosure.test.ts',
Expand All @@ -56,7 +56,7 @@ export const DISPATCH_DISCLOSURE_DRIVER_OWNERS: Readonly<Record<string, string>>
'web-agent-browser.':
'packages/platform-web/src/__tests__/agent-browser-dispatch-disclosure.test.ts',
'maestro-direct.':
'src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios.test.ts',
'src/__tests__/daemon-composition/interaction/interaction-touch-direct-ios.test.ts',
};

/** The per-row driving loop every driver file carries. */
Expand Down
3 changes: 2 additions & 1 deletion packages/contracts/src/snapshot-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ test('provider snapshot binding presents acquired facts through the supplied hos
// signal is fixed at bind time, but `wait` binds once and polls many times, so
// each poll must be able to cancel its own capture without cancelling the
// binding. These are the contract-level halves of that claim; the poll-deadline
// behaviour itself is proven end to end in `src/daemon/__tests__/wait-runtime.test.ts`.
// behaviour itself is proven end to end in
// `src/__tests__/daemon-composition/wait-runtime.test.ts`.
// ---------------------------------------------------------------------------

test('a capture with no per-capture signal receives the binding signal itself, not a wrapper', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/test-kit/src/boundary-fault-matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ export const BOUNDARY_FAULT_MATRIX = {
'optional-optimization-failure': {
mutation: {
kind: 'covered',
evidence: ['src/daemon/interaction/internal/__tests__/interaction-touch-direct-ios.test.ts'],
evidence: [
'src/__tests__/daemon-composition/interaction/interaction-touch-direct-ios.test.ts',
],
invariants: ['best-effort-degradation'],
},
read: {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { test, expect, vi, beforeEach } from 'vitest';
import { legacyDispatchCapture } from './legacy-snapshot-capture-fixture.ts';
import { handleFindCommands } from '../interaction/index.ts';
import { getRuntimeBindings } from './interaction-get-runtime-fixture.ts';
import { dispatchFindReadOnlyViaRuntime } from '../selector-runtime.ts';
import { dispatchWaitViaRuntime } from '../wait-runtime.ts';
import type { DaemonRequest, DaemonResponse } from '../daemon-request.ts';
import { legacyDispatchCapture } from '../../daemon/__tests__/legacy-snapshot-capture-fixture.ts';
import { handleFindCommands } from '../../daemon/interaction/index.ts';
import { getRuntimeBindings } from '../../daemon/__tests__/interaction-get-runtime-fixture.ts';
import { dispatchFindReadOnlyViaRuntime } from '../../daemon/selector-runtime.ts';
import { dispatchWaitViaRuntime } from '../../daemon/wait-runtime.ts';
import type { DaemonRequest, DaemonResponse } from '../../daemon/daemon-request.ts';
import { ANDROID_SYSTEM_SURFACE_DISCLOSURE } from '@agent-device/contracts/android-system-surface-disclosure';
import { iosSystemSurfaceDisclosure } from '@agent-device/contracts/ios-system-surface';
import { snapshotRuntimeFixture } from './snapshot-runtime-fixture.ts';
import { makeSessionStore } from './support/store-factory.ts';
import { makeAndroidSession, makeIosSession } from './support/session-factories.ts';
import { snapshotRuntimeFixture } from '../../daemon/__tests__/snapshot-runtime-fixture.ts';
import { makeSessionStore } from '../../daemon/__tests__/support/store-factory.ts';
import {
makeAndroidSession,
makeIosSession,
} from '../../daemon/__tests__/support/session-factories.ts';
import { platformResourceCleanup } from '../../platform-runtime-resource-cleanup.ts';
import { testCreateCommandSurface } from './command-surface-fixture.ts';

Expand All @@ -22,20 +25,20 @@ vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal
};
});

vi.mock('../snapshot-interactor-capture.ts', async () => {
const fixture = await import('./legacy-snapshot-capture-fixture.ts');
vi.mock('../../daemon/snapshot-interactor-capture.ts', async () => {
const fixture = await import('../../daemon/__tests__/legacy-snapshot-capture-fixture.ts');
return { captureSnapshotWithInteractor: fixture.captureSnapshotThroughLegacyDispatchFixture };
});

vi.mock('../device/device-ready.ts', () => ({
vi.mock('../../daemon/device/device-ready.ts', () => ({
ensureDeviceReady: vi.fn(async () => {}),
}));

import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve';
import { ANDROID_EMULATOR } from '@agent-device/test-kit/device-fixtures';
import { withSystemSurfaceDisclosure } from '../capture-disclosure.ts';
import { withSystemSurfaceDisclosure } from '../../daemon/capture-disclosure.ts';
import { iosTargetActivationDisclosure } from '@agent-device/contracts/ios-target-activation';
import { TARGET_ACTIVATION_FACT } from './capture-disclosure-target-activation-fact.ts';
import { TARGET_ACTIVATION_FACT } from '../../daemon/__tests__/capture-disclosure-target-activation-fact.ts';

// The occluding-shade capture every scenario below consumes: no application window content, one
// active quick-settings surface. The Android capture route stamps systemSurfaceOnly on both the
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { daemonPlatformServicesFixture } from './platform-services-fixture.ts';
import { daemonPlatformServicesFixture } from '../../daemon/__tests__/platform-services-fixture.ts';
import { beforeEach, expect, test, vi } from 'vitest';
import type { CommandFlags } from '@agent-device/contracts/command';
import type { RawSnapshotNode } from '@agent-device/kernel/snapshot';
Expand All @@ -8,15 +8,15 @@ import {
iosSystemSurfaceTransitionDisclosure,
type IosSystemSurfaceProvenance,
} from '@agent-device/contracts/ios-system-surface';
import { makeIosSession } from './support/session-factories.ts';
import { makeSessionStore } from './support/store-factory.ts';
import { activateCompleteRefFrame, refFrameState } from '../ref-frame.ts';
import { setSessionSnapshot } from '../session-snapshot.ts';
import type { SessionStore } from '../session-store.ts';
import type { DaemonRequest, DaemonResponse } from '../daemon-request.ts';
import type { SessionRef, SessionState } from '../session-state.ts';
import { makeIosSession } from '../../daemon/__tests__/support/session-factories.ts';
import { makeSessionStore } from '../../daemon/__tests__/support/store-factory.ts';
import { activateCompleteRefFrame, refFrameState } from '../../daemon/ref-frame.ts';
import { setSessionSnapshot } from '../../daemon/session-snapshot.ts';
import type { SessionStore } from '../../daemon/session-store.ts';
import type { DaemonRequest, DaemonResponse } from '../../daemon/daemon-request.ts';
import type { SessionRef, SessionState } from '../../daemon/session-state.ts';
import { buildSnapshotState } from '@agent-device/capture-kit/snapshot-state';
import { selectorCaptureFixture } from './selector-capture-fixture.ts';
import { selectorCaptureFixture } from '../../daemon/__tests__/selector-capture-fixture.ts';
import { testCreateCommandSurface } from './command-surface-fixture.ts';

// #1638 `--settle` on the GENERIC daemon route (scroll/back): the settled diff,
Expand All @@ -25,8 +25,8 @@ import { testCreateCommandSurface } from './command-surface-fixture.ts';
// a resolution, and the observation must run after the deferred-outcome
// markers. Quiet windows are tuned down so no test waits real time.

vi.mock('../interaction/index.ts', async (importOriginal) => {
const actual = await importOriginal<typeof import('../interaction/index.ts')>();
vi.mock('../../daemon/interaction/index.ts', async (importOriginal) => {
const actual = await importOriginal<typeof import('../../daemon/interaction/index.ts')>();
return {
...actual,
captureSnapshotForSession: vi.fn(async () => ({
Expand All @@ -38,9 +38,9 @@ vi.mock('../interaction/index.ts', async (importOriginal) => {
};
});

import { captureSnapshotForSession } from '../interaction/index.ts';
import { dispatchGenericCommand } from '../request-generic-dispatch.ts';
import { pendingSurfaceStabilization } from '../deferred-interaction-outcome.ts';
import { captureSnapshotForSession } from '../../daemon/interaction/index.ts';
import { dispatchGenericCommand } from '../../daemon/request-generic-dispatch.ts';
import { pendingSurfaceStabilization } from '../../daemon/deferred-interaction-outcome.ts';

const mockCaptureSnapshotForSession = vi.mocked(captureSnapshotForSession);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { test } from 'vitest';
import { handleLeaseCommands } from '../../../daemon/handlers/lease.ts';
import { LeaseRegistry } from '../../../daemon/lease-registry.ts';
import type { DaemonRequest } from '../../../daemon/daemon-request.ts';
import { makeSessionStore } from '../../../daemon/__tests__/support/store-factory.ts';
import { AppError } from '@agent-device/kernel/errors';
import {
providerCredentialFingerprint,
readDaemonProviderCredentials,
} from '../../../provider-credential-fingerprint.ts';
import { pluginHome, selectPluginManifest } from '../../../plugins/plugin.fixtures.ts';

/**
* Lease refusal compares the client's credential fingerprint against the digest the daemon
* computes from its own startup environment through the real root fingerprint reader and the
* installed-plugin manifest registry. Those digests and manifest lookups are the composition
* this suite pins, so it lives root-owned; lease mechanics stay in the daemon suite.
*/

function allocateRequest(): DaemonRequest {
return {
token: 'test-token',
session: 'lease-ttl-test',
command: 'lease_allocate',
positionals: [],
flags: {},
meta: {
tenantId: 'tenant-a',
runId: 'run-a',
clientId: 'client-a',
leaseBackend: 'android-instance',
leaseProvider: 'cloud',
},
};
}

const LIMRUN_ATTACH_ENV = {
LIMRUN_API_KEY: 'lim-key',
LIM_IOS_INSTANCE_URL: 'https://region.limrun.example/v1/ios_x/api',
LIM_IOS_INSTANCE_TOKEN: 'ios-token',
};
const BROWSERSTACK_ENV = { BROWSERSTACK_USERNAME: 'user', BROWSERSTACK_ACCESS_KEY: 'key-1' };

function providerAllocateRequest(
leaseProvider: string,
providerCredentialFingerprint: string | undefined,
leaseBackend: 'ios-instance' | 'android-instance' = 'ios-instance',
): DaemonRequest {
const request = allocateRequest();
return {
...request,
meta: {
...request.meta,
leaseProvider,
leaseBackend,
providerCredentialFingerprint,
},
};
}

async function allocateWithDaemonEnv(
req: DaemonRequest,
daemonEnv: Record<string, string>,
): Promise<{ allocations: number; error?: AppError }> {
const registry = new LeaseRegistry();
let allocations = 0;
try {
await handleLeaseCommands({
req,
sessionName: req.session,
sessionStore: makeSessionStore('agent-device-provider-credentials-'),
leaseRegistry: registry,
providerCredentials: readDaemonProviderCredentials(daemonEnv, '/tmp/agent device state'),
leaseLifecycleProvider: {
allocate: async () => {
allocations += 1;
return {};
},
},
});
} catch (error) {
assert.deepEqual(registry.listActiveLeases(), []);
return { allocations, error: error as AppError };
}
return { allocations };
}

test('a daemon started with only LIMRUN_API_KEY refuses a shell with instance variables before allocation', async () => {
const outcome = await allocateWithDaemonEnv(
providerAllocateRequest('limrun', providerCredentialFingerprint('limrun', LIMRUN_ATTACH_ENV)),
{ LIMRUN_API_KEY: 'lim-key' },
);

assert.equal(outcome.allocations, 0);
assert.equal(outcome.error?.code, 'INVALID_ARGS');
assert.equal(outcome.error?.details?.reason, 'provider-credentials-changed');
assert.equal(outcome.error?.details?.provider, 'limrun');
assert.match(
String(outcome.error?.details?.hint),
/agent-device daemon stop --state-dir '\/tmp\/agent device state'/,
);
});

test('a Limrun lease compares only the leased platform instance variables', async () => {
const daemonEnv = {
...LIMRUN_ATTACH_ENV,
LIM_ANDROID_INSTANCE_URL: 'https://region.limrun.example/v1/android_x/api',
LIM_ANDROID_INSTANCE_TOKEN: 'android-token',
LIM_ANDROID_INSTANCE_ADB_URL: 'wss://region.limrun.example/v1/android_x/adb',
};
const shellEnv = { ...daemonEnv, LIM_ANDROID_INSTANCE_TOKEN: 'android-token-2' };
const allocate = async (leaseBackend: 'ios-instance' | 'android-instance') =>
await allocateWithDaemonEnv(
providerAllocateRequest(
'limrun',
providerCredentialFingerprint('limrun', shellEnv, leaseBackend),
leaseBackend,
),
daemonEnv,
);

assert.equal((await allocate('ios-instance')).error, undefined);
assert.equal(
(await allocate('android-instance')).error?.details?.reason,
'provider-credentials-changed',
);
});

test('a daemon holding rotated BrowserStack keys refuses before allocation', async () => {
const outcome = await allocateWithDaemonEnv(
providerAllocateRequest(
'browserstack',
providerCredentialFingerprint('browserstack', {
...BROWSERSTACK_ENV,
BROWSERSTACK_ACCESS_KEY: 'key-2',
}),
),
BROWSERSTACK_ENV,
);

assert.equal(outcome.allocations, 0);
assert.equal(outcome.error?.details?.reason, 'provider-credentials-changed');
assert.equal(outcome.error?.details?.provider, 'browserstack');
});

test.for([
['limrun', LIMRUN_ATTACH_ENV],
['browserstack', BROWSERSTACK_ENV],
] as const)(
'a daemon holding the %s credentials of the shell allocates',
async ([provider, env]) => {
const outcome = await allocateWithDaemonEnv(
providerAllocateRequest(provider, providerCredentialFingerprint(provider, env)),
env,
);

assert.equal(outcome.error, undefined);
assert.equal(outcome.allocations, 1);
},
);

test('a daemon holding credentials allocates for a shell with none', async () => {
const outcome = await allocateWithDaemonEnv(
providerAllocateRequest('limrun', providerCredentialFingerprint('limrun', {})),
{ LIMRUN_API_KEY: 'lim-key' },
);

assert.equal(outcome.error, undefined);
assert.equal(outcome.allocations, 1);
});

test('a daemon started without BrowserStack credentials refuses a shell that has them', async () => {
const outcome = await allocateWithDaemonEnv(
providerAllocateRequest(
'browserstack',
providerCredentialFingerprint('browserstack', BROWSERSTACK_ENV),
),
{},
);

assert.equal(outcome.allocations, 0);
assert.equal(outcome.error?.details?.reason, 'provider-credentials-changed');
assert.match(String(outcome.error?.message), /started without the browserstack credentials/);
});

function testMuPluginEnv(): Record<string, string> {
const manifest = path.resolve(
import.meta.dirname,
'../../../../packages/provider-testmu/package.json',
);
const { home, env } = pluginHome();
selectPluginManifest(home, JSON.parse(fs.readFileSync(manifest, 'utf8')));
return env;
}

test('a daemon holding other TestMu AI credentials refuses before allocation', async () => {
const home = testMuPluginEnv();
const daemonEnv = { ...home, LT_USERNAME: 'user', LT_ACCESS_KEY: 'key-1' };
const outcome = await allocateWithDaemonEnv(
providerAllocateRequest(
'testmu',
providerCredentialFingerprint('testmu', { ...daemonEnv, LT_ACCESS_KEY: 'key-2' }),
),
daemonEnv,
);

assert.equal(outcome.allocations, 0);
assert.equal(outcome.error?.code, 'INVALID_ARGS');
assert.equal(outcome.error?.details?.reason, 'provider-credentials-changed');
assert.equal(outcome.error?.details?.provider, 'testmu');
});

test('a daemon holding the TestMu AI credentials of the shell allocates', async () => {
const env = { ...testMuPluginEnv(), LT_USERNAME: 'user', LT_ACCESS_KEY: 'key-1' };
const outcome = await allocateWithDaemonEnv(
providerAllocateRequest('testmu', providerCredentialFingerprint('testmu', env)),
env,
);

assert.equal(outcome.error, undefined);
assert.equal(outcome.allocations, 1);
});

test('a daemon started without TestMu AI credentials refuses a shell that has them', async () => {
const home = testMuPluginEnv();
const outcome = await allocateWithDaemonEnv(
providerAllocateRequest(
'testmu',
providerCredentialFingerprint('testmu', {
...home,
LT_USERNAME: 'user',
LT_ACCESS_KEY: 'key-1',
}),
),
home,
);

assert.equal(outcome.allocations, 0);
assert.equal(outcome.error?.details?.reason, 'provider-credentials-changed');
assert.match(String(outcome.error?.message), /started without the testmu credentials/);
});
Loading
Loading