mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
2ec4e91b11
* refactor(core): move the command descriptor registry into its own package `src/core/command-descriptor/`, `src/command-catalog.ts`, `src/core/wait-positionals.ts` and `src/core/parse-timeout.ts` move as git renames into a new private package `@agent-device/command-registry` (deps: contracts, selectors). One subpath per module points straight at the moved file; no `index.ts`, no re-export at the old path. Every consumer switches to the owning specifier. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz * test(host-kit): pin the command-registry package inside the daemon code graph The daemon reaches the registry and its catalog only by workspace specifier. A walk that stopped at the package boundary would report an unchanged signature after a descriptor edit, and the client would keep reusing a daemon running the superseded policy. The manifest is asserted beside the sources because its `exports` map is what chose them. The cache doc comment quoting the old ~800-module graph is corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz * chore(gates): point the descriptor-registry gates at the package path R66's `COMMAND_DESCRIPTOR_MODULE`, R16's record-runtime join subject and the Fallow `AssertTrue` totality-guard key follow the registry to its package. The two descriptor hubs leave `HUB_ENTRY_FILES` because the package manifest now publishes them, so the eager-closure gate discovers them as facades and one entry gets one rule; this also flips `denyPlatformImplementations` from false (hub) to true (package entry) for both, which is intentional and stricter. `command-registry` joins the ranked spine at rank 1. No `APPROVED_OVER_CEILING` row: rename detection carries every moved entry's merge-base baseline, so all twelve fall under the no-growth rule rather than a ceiling. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jqfa11D8QsCMuL17SsLvDz --------- Co-authored-by: Claude <noreply@anthropic.com>
261 lines
9.3 KiB
TypeScript
261 lines
9.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog';
|
|
import { runSourceCliJsonSync } from '../cli-json.ts';
|
|
import { assertJsonContains } from '../live-device-e2e/assertions.ts';
|
|
import {
|
|
collectPagedEventTimeline,
|
|
type EventTimelinePage,
|
|
} from '../live-device-e2e/event-timeline.ts';
|
|
import {
|
|
createLiveDeviceContext,
|
|
createLiveDeviceHarness,
|
|
type LiveDeviceContext,
|
|
} from '../live-device-e2e/runtime.ts';
|
|
import { runLiveReplayTestSuite } from '../live-device-e2e/replay-suite.ts';
|
|
import { writeCoverageReport } from '../live-device-e2e/coverage.ts';
|
|
import {
|
|
LINUX_COMMAND_EVIDENCE_COMMANDS,
|
|
LINUX_COMMAND_EVIDENCE_SCRIPT,
|
|
} from './command-evidence.ts';
|
|
|
|
const C = PUBLIC_COMMANDS;
|
|
const LINUX_CALCULATOR_LANDMARK =
|
|
'appname=gnome-calculator || windowtitle=Calculator || label=Calculator || label=0 || label=1 || label=5';
|
|
const SCENARIO_ID = 'linux-command-evidence';
|
|
const SCRIPT_PATH = path.resolve(LINUX_COMMAND_EVIDENCE_SCRIPT);
|
|
|
|
type LinuxBehavior = 'command-evidence';
|
|
type LinuxContext = LiveDeviceContext<LinuxBehavior>;
|
|
|
|
const harness = createLiveDeviceHarness<LinuxContext, LinuxBehavior>({
|
|
behaviorsForScenario: () => [],
|
|
commandsForScenario: () => LINUX_COMMAND_EVIDENCE_COMMANDS,
|
|
commonFlags: (context, args) => [
|
|
...args,
|
|
'--platform',
|
|
'linux',
|
|
'--session',
|
|
context.session,
|
|
'--json',
|
|
],
|
|
runCli: async (args, env, options) =>
|
|
runSourceCliJsonSync(args, { env, timeoutMs: options?.timeoutMs }),
|
|
writeCoverageReport: (context) =>
|
|
writeCoverageReport(context, {
|
|
platform: 'linux',
|
|
script: SCRIPT_PATH,
|
|
}),
|
|
});
|
|
|
|
const { runScenario, runStep, sessionExists, verifyCommand } = harness;
|
|
|
|
async function runLinuxCommandEvidence(): Promise<void> {
|
|
const context = createLiveDeviceContext<LinuxBehavior>({
|
|
artifactRoot: 'test/artifacts/linux-command-evidence',
|
|
session: `${SCENARIO_ID}-${process.pid}`,
|
|
});
|
|
context.env = {
|
|
...context.env,
|
|
AGENT_DEVICE_STATE_DIR: path.join(context.artifactDir, 'daemon-state'),
|
|
};
|
|
const primaryError = await captureError(() =>
|
|
runScenario(context, {
|
|
id: SCENARIO_ID,
|
|
run: runCommandEvidence,
|
|
}),
|
|
);
|
|
const cleanupError = await finalize(context);
|
|
throwErrors(primaryError, cleanupError);
|
|
}
|
|
|
|
async function runCommandEvidence(context: LinuxContext): Promise<void> {
|
|
await runCommandDiscoveryEvidence(context);
|
|
await runSnapshotEvidence(context);
|
|
await runEventEvidence(context);
|
|
}
|
|
|
|
async function runCommandDiscoveryEvidence(context: LinuxContext): Promise<void> {
|
|
const capabilities = await runStep(context, 'read Linux capabilities', ['capabilities']);
|
|
assertJsonContains(capabilities, 'find', 'Linux capabilities should expose find');
|
|
assertJsonContains(capabilities, 'swipe', 'Linux capabilities should expose swipe');
|
|
verifyCommand(
|
|
context,
|
|
C.capabilities,
|
|
'Linux capabilities reports command admission for the selected desktop',
|
|
);
|
|
|
|
const doctor = await runStep(context, 'run Linux doctor', ['doctor']);
|
|
assertJsonContains(doctor, 'linux', 'Linux doctor should report the selected platform');
|
|
verifyCommand(context, C.doctor, 'Linux doctor returns platform-specific diagnostics');
|
|
|
|
const suite = await runLiveReplayTestSuite({
|
|
context,
|
|
runStep,
|
|
step: 'run the command-evidence script as a test suite',
|
|
scripts: [SCRIPT_PATH],
|
|
artifactName: 'command-evidence-test',
|
|
});
|
|
assert.equal(suite.commandsByScript.get(SCRIPT_PATH)?.includes('find'), true);
|
|
verifyCommand(context, C.test, 'the Linux command-evidence script passes through test');
|
|
|
|
const replay = await runStep(context, 'replay the command-evidence script', [
|
|
'replay',
|
|
SCRIPT_PATH,
|
|
'--keep-session',
|
|
]);
|
|
assert.equal(replay.json?.data?.sessionActive, true, JSON.stringify(replay.json));
|
|
assert.equal(typeof replay.json?.data?.session, 'string', JSON.stringify(replay.json));
|
|
context.session = replay.json.data.session;
|
|
context.sessionOpen = true;
|
|
verifyCommand(context, C.replay, 'the Linux command-evidence script passes through replay');
|
|
}
|
|
|
|
async function runSnapshotEvidence(context: LinuxContext): Promise<void> {
|
|
await runFindEvidence(context);
|
|
await runDiffEvidence(context);
|
|
await runSwipeEvidence(context);
|
|
await runBatchEvidence(context);
|
|
}
|
|
|
|
async function runFindEvidence(context: LinuxContext): Promise<void> {
|
|
const find = await runStep(context, 'find a calculator button', [
|
|
'find',
|
|
'role',
|
|
'button',
|
|
'exists',
|
|
'--first',
|
|
]);
|
|
assert.equal(find.json?.data?.found, true, JSON.stringify(find.json));
|
|
verifyCommand(context, C.find, 'find resolves a live AT-SPI role match');
|
|
}
|
|
|
|
async function runDiffEvidence(context: LinuxContext): Promise<void> {
|
|
await closeIfOpen(context);
|
|
await runStep(context, 'reset the calculator before diff', [
|
|
'open',
|
|
'gnome-calculator',
|
|
'--relaunch',
|
|
]);
|
|
await runStep(context, 'capture the diff baseline', ['snapshot', '-i']);
|
|
await runStep(context, 'mutate the calculator before diff', ['click', 'role=button label=1']);
|
|
const diff = await runStep(context, 'read the live snapshot diff', ['diff', 'snapshot', '-i']);
|
|
const summary = diff.json!.data!.summary!;
|
|
const additions = Number(summary.additions);
|
|
const removals = Number(summary.removals);
|
|
assert.ok(
|
|
additions + removals > 0,
|
|
`expected a non-empty Linux snapshot diff: ${JSON.stringify(diff.json)}`,
|
|
);
|
|
verifyCommand(context, C.diff, 'snapshot diff observes the calculator mutation');
|
|
}
|
|
|
|
async function runSwipeEvidence(context: LinuxContext): Promise<void> {
|
|
await runStep(context, 'swipe across the Linux desktop', ['swipe', '100', '400', '100', '300']);
|
|
verifyCommand(context, C.swipe, 'coordinate swipe reaches the Linux input runtime');
|
|
}
|
|
|
|
async function runBatchEvidence(context: LinuxContext): Promise<void> {
|
|
const batchSteps = JSON.stringify([
|
|
{ command: 'is', input: { predicate: 'exists', selector: LINUX_CALCULATOR_LANDMARK } },
|
|
{ command: 'snapshot', input: { interactiveOnly: true } },
|
|
]);
|
|
const batch = await runStep(context, 'run a Linux batch of live reads', [
|
|
'batch',
|
|
'--steps',
|
|
batchSteps,
|
|
]);
|
|
assertLinuxBatchResults(batch.json);
|
|
verifyCommand(context, C.batch, 'batch executes two successful Linux session reads');
|
|
}
|
|
|
|
function assertLinuxBatchResults(json: any): void {
|
|
const data = json?.data;
|
|
assert.ok(data, JSON.stringify(json));
|
|
assert.equal(data.executed, 2, JSON.stringify(json));
|
|
const results = data.results;
|
|
assert.ok(Array.isArray(results), JSON.stringify(json));
|
|
assert.equal(results.length, 2, JSON.stringify(json));
|
|
const first = results[0] as { command?: string; data?: { pass?: boolean } };
|
|
const second = results[1] as { command?: string };
|
|
assert.equal(first.command, 'is', JSON.stringify(json));
|
|
assert.equal(first.data?.pass, true, JSON.stringify(json));
|
|
assert.equal(second.command, 'snapshot', JSON.stringify(json));
|
|
}
|
|
|
|
async function runEventEvidence(context: LinuxContext): Promise<void> {
|
|
const timeline = await collectPagedEventTimeline((cursor) => readLinuxEventPage(context, cursor));
|
|
for (const command of [C.open, C.snapshot]) {
|
|
assert.ok(
|
|
timeline.commands.includes(command),
|
|
`Linux events missing ${command}: ${JSON.stringify(timeline)}`,
|
|
);
|
|
}
|
|
verifyCommand(context, C.events, 'event timeline names open and snapshot from the Linux session');
|
|
}
|
|
|
|
async function readLinuxEventPage(
|
|
context: LinuxContext,
|
|
cursor: string | undefined,
|
|
): Promise<EventTimelinePage> {
|
|
const args = ['events', '100'];
|
|
if (cursor !== undefined) args.push(cursor);
|
|
const events = await runStep(context, 'read Linux session events', args);
|
|
const json = events.json as { data?: EventTimelinePage };
|
|
assert.ok(json.data, JSON.stringify(events.json));
|
|
return json.data;
|
|
}
|
|
|
|
async function finalize(context: LinuxContext): Promise<unknown> {
|
|
const closeError = await captureError(() => closeIfOpen(context));
|
|
const reportError = await captureError(() => {
|
|
writeCoverageReport(context, {
|
|
platform: 'linux',
|
|
script: SCRIPT_PATH,
|
|
});
|
|
});
|
|
return combineErrors([closeError, reportError]);
|
|
}
|
|
|
|
async function closeIfOpen(context: LinuxContext): Promise<void> {
|
|
if (!context.sessionOpen && !(await sessionExists(context))) return;
|
|
await runStep(context, 'close the Linux command-evidence session', ['close']);
|
|
}
|
|
|
|
async function captureError(action: () => Promise<void> | void): Promise<unknown | undefined> {
|
|
try {
|
|
await action();
|
|
} catch (error) {
|
|
return error;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function combineErrors(errors: readonly (unknown | undefined)[]): unknown {
|
|
const failures = errors.filter((error): error is unknown => error !== undefined);
|
|
if (failures.length === 0) return undefined;
|
|
if (failures.length === 1) return failures[0];
|
|
return new AggregateError(failures, 'Linux command evidence cleanup failed');
|
|
}
|
|
|
|
function throwErrors(primaryError: unknown | undefined, cleanupError: unknown): void {
|
|
if (primaryError === undefined) {
|
|
if (cleanupError !== undefined) throw cleanupError;
|
|
return;
|
|
}
|
|
if (cleanupError === undefined) throw primaryError;
|
|
throw new AggregateError(
|
|
[primaryError, cleanupError],
|
|
'Linux command evidence failed and cleanup also failed',
|
|
);
|
|
}
|
|
|
|
if (
|
|
process.argv[1] !== undefined &&
|
|
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
) {
|
|
await runLinuxCommandEvidence();
|
|
}
|