Skip to content

Commit 8906038

Browse files
committed
test(ios): extract snapshot truncation regressions
1 parent 7713654 commit 8906038

6 files changed

Lines changed: 190 additions & 132 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import assert from 'node:assert/strict';
2+
import { test } from 'vitest';
3+
import { createAgentDeviceClient } from '../agent-device-client.ts';
4+
import { createTransport } from './client-transport-fixture.ts';
5+
6+
test('client capture.snapshot preserves unknown truncation as an omitted field', async () => {
7+
const setup = createTransport(async () => ({
8+
ok: true,
9+
data: {
10+
nodes: [],
11+
warnings: ['tree completeness is not independently verified'],
12+
},
13+
}));
14+
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });
15+
16+
const result = await client.capture.snapshot();
17+
18+
assert.equal('truncated' in result, false);
19+
assert.equal(result.truncated, undefined);
20+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { AgentDeviceClientConfig } from '../agent-device-client.ts';
2+
import type { DaemonRequest, DaemonResponse } from '@agent-device/kernel/contracts';
3+
import { mkdtempForTestSync } from './test-utils/tmp-dir.ts';
4+
5+
const TEST_STATE_DIR = mkdtempForTestSync('agent-device-client-test-');
6+
7+
export function createTransport(
8+
handler: (req: Omit<DaemonRequest, 'token'>) => Promise<DaemonResponse> | DaemonResponse,
9+
): {
10+
calls: Array<Omit<DaemonRequest, 'token'>>;
11+
config: AgentDeviceClientConfig;
12+
transport: (req: Omit<DaemonRequest, 'token'>) => Promise<DaemonResponse>;
13+
} {
14+
const calls: Array<Omit<DaemonRequest, 'token'>> = [];
15+
const config: AgentDeviceClientConfig = {
16+
session: 'qa',
17+
stateDir: TEST_STATE_DIR,
18+
cwd: '/tmp/agent-device',
19+
debug: true,
20+
daemonBaseUrl: 'http://daemon.example.test',
21+
daemonAuthToken: 'secret',
22+
daemonTransport: 'http',
23+
tenant: 'acme',
24+
sessionIsolation: 'tenant',
25+
runId: 'run-123',
26+
leaseId: 'lease-123',
27+
};
28+
return {
29+
calls,
30+
config,
31+
transport: async (req) => {
32+
calls.push(req);
33+
return await handler(req);
34+
},
35+
};
36+
}

src/__tests__/client.test.ts

Lines changed: 2 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import type {
1010
import {
1111
createAgentDeviceClient,
1212
type AgentDeviceClient,
13-
type AgentDeviceClientConfig,
1413
type DiffSnapshotCommandResult,
1514
type DoctorCommandResult,
1615
type PrepareCommandResult,
@@ -24,18 +23,12 @@ import {
2423
} from '../agent-device-client.ts';
2524
import { runCommand } from '../commands/command-surface.ts';
2625
import type { CommandResult } from '../core/command-descriptor/command-result.ts';
27-
import type {
28-
DaemonRequest,
29-
DaemonResponse,
30-
DaemonResponseData,
31-
} from '@agent-device/kernel/contracts';
26+
import type { DaemonResponse, DaemonResponseData } from '@agent-device/kernel/contracts';
3227
import { AppError } from '@agent-device/kernel/errors';
3328
import fs from 'node:fs';
3429
import nodePath from 'node:path';
3530
import { mkdtempForTestSync } from './test-utils/tmp-dir.ts';
36-
37-
// Isolated so open/close metro-session-hint file writes never touch the real state dir.
38-
const TEST_STATE_DIR = mkdtempForTestSync('agent-device-client-test-');
31+
import { createTransport } from './client-transport-fixture.ts';
3932

4033
// #1802: replay/test requests carry the script text the CLIENT read, so these cases need real
4134
// files. `cwd` is what the writer resolves the caller's relative path against.
@@ -96,37 +89,6 @@ const closedProjectionResponses: Record<string, DaemonResponseData> = {
9689
},
9790
};
9891

99-
function createTransport(
100-
handler: (req: Omit<DaemonRequest, 'token'>) => Promise<DaemonResponse> | DaemonResponse,
101-
): {
102-
calls: Array<Omit<DaemonRequest, 'token'>>;
103-
config: AgentDeviceClientConfig;
104-
transport: (req: Omit<DaemonRequest, 'token'>) => Promise<DaemonResponse>;
105-
} {
106-
const calls: Array<Omit<DaemonRequest, 'token'>> = [];
107-
const config: AgentDeviceClientConfig = {
108-
session: 'qa',
109-
stateDir: TEST_STATE_DIR,
110-
cwd: '/tmp/agent-device',
111-
debug: true,
112-
daemonBaseUrl: 'http://daemon.example.test',
113-
daemonAuthToken: 'secret',
114-
daemonTransport: 'http',
115-
tenant: 'acme',
116-
sessionIsolation: 'tenant',
117-
runId: 'run-123',
118-
leaseId: 'lease-123',
119-
};
120-
return {
121-
calls,
122-
config,
123-
transport: async (req) => {
124-
calls.push(req);
125-
return await handler(req);
126-
},
127-
};
128-
}
129-
13092
test('client exposes narrowed result types for closed daemon projections', async () => {
13193
const setup = createTransport(async (req) => closedProjectionResponse(req.command));
13294
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });
@@ -1118,22 +1080,6 @@ test('client capture.snapshot preserves visibility metadata from daemon response
11181080
});
11191081
});
11201082

1121-
test('client capture.snapshot preserves unknown truncation as an omitted field', async () => {
1122-
const setup = createTransport(async () => ({
1123-
ok: true,
1124-
data: {
1125-
nodes: [],
1126-
warnings: ['tree completeness is not independently verified'],
1127-
},
1128-
}));
1129-
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });
1130-
1131-
const result = await client.capture.snapshot();
1132-
1133-
assert.equal('truncated' in result, false);
1134-
assert.equal(result.truncated, undefined);
1135-
});
1136-
11371083
test('client capture.snapshot preserves refsGeneration from daemon responses (ADR 0014)', async () => {
11381084
const setup = createTransport(async () => ({
11391085
ok: true,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import path from 'node:path';
2+
import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts';
3+
import type { ProviderDeviceRuntime } from '@agent-device/contracts/device';
4+
import { SessionStore } from '../../session-store.ts';
5+
import type { SessionState } from '../../types.ts';
6+
7+
export function makeSessionStore(): SessionStore {
8+
const root = mkdtempForTestSync('agent-device-snapshot-handler-');
9+
return new SessionStore(path.join(root, 'sessions'));
10+
}
11+
12+
export function makeSession(
13+
name: string,
14+
device: SessionState['device'],
15+
extra?: Partial<SessionState>,
16+
): SessionState {
17+
return { name, device, createdAt: Date.now(), actions: [], ...extra };
18+
}
19+
20+
export function makeProviderRuntimeOwning(
21+
device: SessionState['device'],
22+
provider = 'browserstack',
23+
): ProviderDeviceRuntime {
24+
return {
25+
provider,
26+
leaseLifecycle: {},
27+
deviceInventoryProvider: async () => [device],
28+
ownsDevice: (candidate) => candidate.id === device.id,
29+
getInteractor: () => undefined,
30+
shutdown: async () => undefined,
31+
};
32+
}

src/daemon/handlers/__tests__/snapshot-handler.test.ts

Lines changed: 5 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,30 @@ import {
66
resetGetRuntimeFixture,
77
} from './interaction-get-runtime-fixture.ts';
88
import fs from 'node:fs';
9-
import path from 'node:path';
109
import { handleSnapshotCommands as handleProductionSnapshotCommands } from '../snapshot.ts';
1110
import { captureSnapshot } from '../snapshot-capture.ts';
1211
import { SessionStore } from '../../session-store.ts';
1312
import { setActiveProviderDeviceRuntimes } from '../../../provider-device-runtime.ts';
14-
import type { ProviderDeviceRuntime } from '@agent-device/contracts/device';
1513
import type { DaemonResponse, SessionState } from '../../types.ts';
1614
import { AppError } from '@agent-device/kernel/errors';
1715
import { platformResourceCleanup } from '../../../platform-runtime-resource-cleanup.ts';
1816
import { buildSnapshotSignatures } from '../../../snapshot/snapshot-freshness/index.ts';
1917
import { buildInteractionSurfaceSignature } from '../../interaction-outcome-policy.ts';
2018
import { buildSnapshotPresentationKey } from '@agent-device/kernel/snapshot';
21-
import { attachSnapshotPresentationEvidence } from '@agent-device/contracts/capture';
2219
import { snapshotCliOutput } from '../../../commands/capture/output.ts';
2320
import type { CaptureSnapshotResult } from '@agent-device/contracts/client';
24-
import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts';
2521
import {
2622
fixtureScreenshotCaptures,
2723
fixtureSettingsMutations,
2824
resetSnapshotRuntimeFixture,
2925
snapshotRuntimeFixture,
3026
} from '../../__tests__/snapshot-runtime-fixture.ts';
3127
import type { BindDeviceRuntime } from '../../request-runtime-binding.ts';
28+
import {
29+
makeProviderRuntimeOwning,
30+
makeSession,
31+
makeSessionStore,
32+
} from './snapshot-handler-fixture.ts';
3233

3334
vi.mock('../snapshot-interactor-capture.ts', async () => {
3435
const fixture = await import('../../__tests__/legacy-snapshot-capture-fixture.ts');
@@ -75,15 +76,6 @@ function handleSnapshotCommands(
7576
});
7677
}
7778

78-
function makeSessionStore(): SessionStore {
79-
const root = mkdtempForTestSync('agent-device-snapshot-handler-');
80-
return new SessionStore(path.join(root, 'sessions'));
81-
}
82-
83-
type SessionExtra = Partial<SessionState>;
84-
function makeSession(name: string, d: SessionState['device'], extra?: SessionExtra): SessionState {
85-
return { name, device: d, createdAt: Date.now(), actions: [], ...extra };
86-
}
8779
// An Apple wait runs inside an opened app: that bundle id is XCUITest's attach target, and
8880
// without one the plan asks for the without-active-app row local Apple refuses.
8981
const appAttach = (d: SessionState['device']): Partial<SessionState> =>
@@ -125,20 +117,6 @@ const providerIosDevice: SessionState['device'] = {
125117
booted: true,
126118
};
127119

128-
function makeProviderRuntimeOwning(
129-
device: SessionState['device'],
130-
provider = 'browserstack',
131-
): ProviderDeviceRuntime {
132-
return {
133-
provider,
134-
leaseLifecycle: {},
135-
deviceInventoryProvider: async () => [device],
136-
ownsDevice: (candidate) => candidate.id === device.id,
137-
getInteractor: () => undefined,
138-
shutdown: async () => undefined,
139-
};
140-
}
141-
142120
afterEach(() => {
143121
setActiveProviderDeviceRuntimes([]);
144122
});
@@ -488,55 +466,6 @@ test('snapshot on provider-backed iOS runs without a tracked app', async () => {
488466
expect(bindCount).toBe(1);
489467
});
490468

491-
test('Limrun unknown truncation stays omitted through daemon and public output', async () => {
492-
const sessionStore = makeSessionStore();
493-
const sessionName = 'limrun-ios-unknown-truncation';
494-
const limrunDevice: SessionState['device'] = {
495-
platform: 'apple',
496-
appleOs: 'ios',
497-
id: 'limrun:ios:lease-a',
498-
name: 'Limrun iOS',
499-
kind: 'simulator',
500-
target: 'mobile',
501-
booted: true,
502-
};
503-
sessionStore.set(sessionName, makeSession(sessionName, limrunDevice));
504-
setActiveProviderDeviceRuntimes([makeProviderRuntimeOwning(limrunDevice, 'limrun')]);
505-
legacyDispatchCapture.mockResolvedValue(
506-
attachSnapshotPresentationEvidence(
507-
{
508-
nodes: [{ index: 0, depth: 0, type: 'Application', label: 'Demo' }],
509-
backend: 'xctest',
510-
producer: 'limrun-ios-tree',
511-
warnings: ['tree completeness is not independently verified'],
512-
},
513-
{ owner: 'ios-snapshot-engine' },
514-
),
515-
);
516-
517-
const response = await handleSnapshotCommands({
518-
req: {
519-
token: 't',
520-
session: sessionName,
521-
command: 'snapshot',
522-
positionals: [],
523-
flags: {},
524-
},
525-
sessionName,
526-
logPath: '/tmp/daemon.log',
527-
sessionStore,
528-
});
529-
530-
expect(response?.ok).toBe(true);
531-
if (!response?.ok) return;
532-
expect(response.data).not.toHaveProperty('truncated');
533-
534-
const cliOutput = await snapshotCliOutput({
535-
result: response.data as unknown as CaptureSnapshotResult,
536-
});
537-
expect(cliOutput.jsonData).not.toHaveProperty('truncated');
538-
});
539-
540469
test('diff on local iOS still requires a tracked app', async () => {
541470
const sessionStore = makeSessionStore();
542471
const sessionName = 'ios-sim-no-app-diff';

0 commit comments

Comments
 (0)