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
47 changes: 47 additions & 0 deletions src/commands/capture/diff.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,67 @@
import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
import { SNAPSHOT_FLAGS } from '@agent-device/command-registry/flag-groups';
import type { CommandResultMap } from '@agent-device/command-registry/command-result';
import { AppError } from '@agent-device/kernel/errors';
import {
booleanField,
booleanSchema,
constSchema,
enumSchema,
integerField,
jsonSchemaField,
numberSchema,
objectSchema,
requiredField,
stringArraySchema,
stringField,
stringSchema,
} from '../command-input.ts';
import { commonInputFromFlags, direct, requiredDaemonString } from '../cli-grammar/common.ts';
import type { CliReader, DaemonWriter } from '../cli-grammar/types.ts';
import type { JsonSchema } from '../command-contract.ts';
import { defineCommandFacet } from '../family/types.ts';
import { defineFieldCommandMetadata } from '../field-command-contract.ts';

const DIFF_COMMAND_NAME = 'diff';

/**
* This family's advertised MCP `outputSchema`, keyed by daemon command name and projected into
* the command map by `src/mcp/command-output-schemas.ts`. Non-strict like every other entry: no
* `additionalProperties: false`, so additive response fields keep validating. `diff` does not
* carry the post-action observation trait (#1652): it reports a snapshot comparison, not an
* interaction, so no settle-graft copy applies here.
*/
export const DIFF_COMMAND_OUTPUT_SCHEMAS = {
// packages/contracts/src/diff.ts — the public Node command accepts snapshot diffs.
diff: objectSchema(
{
mode: constSchema('snapshot'),
baselineInitialized: booleanSchema(),
summary: objectSchema(
{
additions: numberSchema(),
removals: numberSchema(),
unchanged: numberSchema(),
},
['additions', 'removals', 'unchanged'],
),
lines: {
type: 'array',
items: objectSchema(
{
kind: enumSchema(['added', 'removed', 'unchanged']),
text: stringSchema(),
ref: stringSchema(),
},
['kind', 'text'],
),
},
warnings: stringArraySchema(),
},
['mode', 'baselineInitialized', 'summary', 'lines'],
),
} satisfies Pick<Record<keyof CommandResultMap, JsonSchema>, 'diff'>;

const diffCommandDescription =
'Compare accessibility snapshots or screenshots to identify UI changes. Use snapshot comparisons for semantic tree changes and screenshot comparisons for pixel differences.';

Expand Down
40 changes: 40 additions & 0 deletions src/mcp/__tests__/command-tools-capture-diff-schemas.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import assert from 'node:assert/strict';
import { test } from 'vitest';
import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files';
import { DIFF_COMMAND_OUTPUT_SCHEMAS } from '../../commands/capture/diff.ts';
import { COMMAND_OUTPUT_SCHEMAS } from '../command-output-schemas.ts';

const DIFF_COMMANDS = Object.keys(DIFF_COMMAND_OUTPUT_SCHEMAS) as Array<
keyof typeof DIFF_COMMAND_OUTPUT_SCHEMAS
>;

// diff does not carry the post-action observation trait (#1652): it reports a snapshot
// comparison, not an interaction, so the composed map never copies the entry and reference
// equality must hold.
test('MCP diff family output schemas are the family module entries, not copies', () => {
for (const command of DIFF_COMMANDS) {
assert.equal(
COMMAND_OUTPUT_SCHEMAS[command],
DIFF_COMMAND_OUTPUT_SCHEMAS[command],
`${command} is not reference-equal to the diff module's own schema object`,
);
}
});

test('the projected family map declares exactly the commands its module owns', () => {
for (const command of DIFF_COMMANDS) {
assert.ok(
ownerFilesForCommand(command).includes('src/commands/capture/diff.ts'),
`${command} projects its output schema from this module but does not name it as its owner`,
);
}
});

test('the diff line kinds include unchanged context lines', () => {
const lines = (
DIFF_COMMAND_OUTPUT_SCHEMAS.diff as unknown as {
properties: { lines: { items: { properties: { kind: { enum: string[] } } } } };
}
).properties.lines.items.properties.kind;
assert.deepEqual(lines.enum, ['added', 'removed', 'unchanged']);
});
2 changes: 2 additions & 0 deletions src/mcp/__tests__/command-tools-replay-schemas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { test } from 'vitest';
import type { ReplayCommandResult, ReplaySuiteResult } from '@agent-device/contracts/replay';
import { ownerFilesForCommand } from '@agent-device/command-registry/owner-files';
import { commandSupportsSettleObservation } from '@agent-device/command-registry/registry';
import { DIFF_COMMAND_OUTPUT_SCHEMAS } from '../../commands/capture/diff.ts';
import { WAIT_COMMAND_OUTPUT_SCHEMAS } from '../../commands/capture/wait.ts';
import { PREPARE_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/prepare.ts';
import { DOCTOR_COMMAND_OUTPUT_SCHEMAS } from '../../commands/management/doctor.ts';
Expand Down Expand Up @@ -80,6 +81,7 @@ const PROJECTED_FAMILIES = [
{ name: 'replay', schemas: REPLAY_COMMAND_OUTPUT_SCHEMAS },
{ name: 'system', schemas: SYSTEM_COMMAND_OUTPUT_SCHEMAS },
{ name: 'interaction', schemas: INTERACTION_COMMAND_OUTPUT_SCHEMAS },
{ name: 'capture-diff', schemas: DIFF_COMMAND_OUTPUT_SCHEMAS },
{ name: 'capture-wait', schemas: WAIT_COMMAND_OUTPUT_SCHEMAS },
{ name: 'management-prepare', schemas: PREPARE_COMMAND_OUTPUT_SCHEMAS },
{ name: 'management-doctor', schemas: DOCTOR_COMMAND_OUTPUT_SCHEMAS },
Expand Down
33 changes: 2 additions & 31 deletions src/mcp/command-output-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import type { CommandResultMap } from '@agent-device/command-registry/command-re
import { commandSupportsSettleObservation } from '@agent-device/command-registry/registry';
import {
booleanSchema,
constSchema,
enumSchema,
numberSchema,
objectSchema,
stringArraySchema,
stringSchema,
} from '../commands/command-input.ts';
import { DIFF_COMMAND_OUTPUT_SCHEMAS } from '../commands/capture/diff.ts';
import { WAIT_COMMAND_OUTPUT_SCHEMAS } from '../commands/capture/wait.ts';
import { PREPARE_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/prepare.ts';
import { DOCTOR_COMMAND_OUTPUT_SCHEMAS } from '../commands/management/doctor.ts';
Expand Down Expand Up @@ -183,39 +182,11 @@ const BASE_COMMAND_OUTPUT_SCHEMAS = {
['direction'],
),

// packages/contracts/src/diff.ts — the public Node command accepts snapshot diffs.
diff: objectSchema(
{
mode: constSchema('snapshot'),
baselineInitialized: booleanSchema(),
summary: objectSchema(
{
additions: numberSchema(),
removals: numberSchema(),
unchanged: numberSchema(),
},
['additions', 'removals', 'unchanged'],
),
lines: {
type: 'array',
items: objectSchema(
{
kind: enumSchema(['added', 'removed']),
text: stringSchema(),
ref: stringSchema(),
},
['kind', 'text'],
),
},
warnings: stringArraySchema(),
},
['mode', 'baselineInitialized', 'summary', 'lines'],
),

// A family that owns its commands authors their advertised response shape beside the
// command surface and projects it here. This spread stays last: a hand-written entry for
// a projected command then fails as TS2783 instead of quietly overriding the family's,
// and this map's `satisfies` still refuses a missing `CommandResultMap` key.
...DIFF_COMMAND_OUTPUT_SCHEMAS,
...WAIT_COMMAND_OUTPUT_SCHEMAS,
...PREPARE_COMMAND_OUTPUT_SCHEMAS,
...DOCTOR_COMMAND_OUTPUT_SCHEMAS,
Expand Down
Loading