Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -852,3 +852,27 @@ test('capture.screenshot normalizes the default-level result (unchanged)', async
assert.equal(result.path, '/tmp/shot.png');
assert.deepEqual(result.identifiers, { session: 'qa' });
});

test('capture.snapshot passes a digest (non-default level) payload through unnormalized', async () => {
const digest = {
nodeCount: 3,
refs: [{ ref: 'e1', label: 'Login' }],
truncated: false,
};
const setup = createTransport(async (req) => {
assert.equal(req.command, 'snapshot');
assert.equal(req.meta?.responseLevel, 'digest'); // the level reached the daemon
return { ok: true, data: digest };
});
const client = createAgentDeviceClient(setup.config, { transport: setup.transport });

const result = await client.capture.snapshot({ responseLevel: 'digest' });

// The digest SURVIVES the client path: nodeCount/refs are preserved and the
// default normalizer (which expects `nodes` and would yield an empty snapshot
// plus `identifiers`) is skipped.
const asRecord = result as Record<string, unknown>;
assert.deepEqual(asRecord, digest);
assert.equal(asRecord.nodeCount, 3);
assert.ok(!('identifiers' in asRecord));
});
51 changes: 51 additions & 0 deletions src/cli/commands/__tests__/generic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { test } from 'vitest';
import assert from 'node:assert/strict';
import { createAgentDeviceClient } from '../../../client.ts';
import type { DaemonResponse } from '../../../contracts.ts';
import type { CliFlags } from '../../../utils/cli-flags.ts';
import type { ClientBackedCliCommandName } from '../../../command-catalog.ts';
import { runGenericClientBackedCommand } from '../generic.ts';

async function captureStdout(fn: () => Promise<unknown>): Promise<string> {
const chunks: string[] = [];
const original = process.stdout.write.bind(process.stdout);
process.stdout.write = ((chunk: unknown) => {
chunks.push(String(chunk));
return true;
}) as typeof process.stdout.write;
try {
await fn();
} finally {
process.stdout.write = original;
}
return chunks.join('');
}

test('snapshot --level digest --json preserves the digest through the generic CLI path', async () => {
const digest = { nodeCount: 3, refs: [{ ref: 'e1', label: 'Login' }], truncated: false };
const client = createAgentDeviceClient(
{ session: 'qa', responseLevel: 'digest' },
{
transport: async (req): Promise<DaemonResponse> => {
assert.equal(req.command, 'snapshot');
return { ok: true, data: digest };
},
},
);
const flags = { json: true, responseLevel: 'digest' } as CliFlags;

const out = await captureStdout(() =>
runGenericClientBackedCommand({
command: 'snapshot' as ClientBackedCliCommandName,
positionals: [],
flags,
client,
}),
);
const parsed = JSON.parse(out) as { success: boolean; data: Record<string, unknown> };

assert.equal(parsed.success, true);
// nodeCount/refs — the digest fields — are preserved, not collapsed by the
// snapshot formatter that expects `nodes`.
assert.deepEqual(parsed.data, digest);
});
9 changes: 9 additions & 0 deletions src/cli/commands/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { CliOutput } from '../../commands/command-contract.ts';
import type { ReplaySuiteResult } from '../../daemon/types.ts';
import type { CliFlags } from '../../utils/cli-flags.ts';
import { readCommandMessage } from '../../utils/success-text.ts';
import { isNonDefaultResponseLevel } from '../../contracts.ts';
import { writeCommandOutput } from './shared.ts';
import type { ClientBackedCliCommandName } from '../../command-catalog.ts';
import type { ClientCommandParams } from './router-types.ts';
Expand All @@ -23,6 +24,14 @@ export async function runGenericClientBackedCommand({
positionals,
flags,
});
// A non-default responseLevel returns a leveled payload (e.g. the snapshot
// digest { nodeCount, refs }) that the per-command CLI formatters assume away —
// they serialize the default shape and drop the digest fields. Emit the leveled
// payload verbatim instead.
if (isNonDefaultResponseLevel(flags.responseLevel)) {
writeCommandOutput(flags, result, () => JSON.stringify(result, null, 2));
return true;
}
if (cliOutput) {
writeCliOutput(flags, cliOutput);
} else {
Expand Down
6 changes: 6 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,12 @@ export function createAgentDeviceClient(
snapshot: async (options: CaptureSnapshotOptions = {}) => {
const session = resolveRequestSession(options);
const data = await executeCommand<Record<string, unknown>>('snapshot', options);
// A non-default responseLevel returns the leveled snapshot digest
// ({ nodeCount, refs, … }); normalizeSnapshotResult expects the full
// `nodes` tree and would collapse it to an empty snapshot. Pass the
// leveled payload through verbatim. (Mirrors capture.screenshot; the
// caller opted into the level, so the runtime value is the leveled shape.)
if (isLeveledResponse(options)) return data as unknown as CaptureSnapshotResult;
return normalizeSnapshotResult(data, session);
},
screenshot: async (options: CaptureScreenshotOptions = {}) => {
Expand Down
Loading