Skip to content
Open
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
2 changes: 1 addition & 1 deletion docs/adr/0018-unified-event-journal.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ redaction discipline, and sink (inventoried 2026-07-24):
request via `meta.requestProgress`; disabled under `--json`. Events are written to the wire
**unredacted** today.
4. **Replay timing trace** (`src/daemon/handlers/session-replay-trace.ts`,
`session-test-runtime.ts`, read by `src/replay/test/trace.ts`). Per-**attempt**
`session-test-runtime.ts`, read by `src/cli/replay-test/trace.ts`). Per-**attempt**
`replay-timing.ndjson` files whose paths are created dynamically inside each attempt — written
by **two different helpers, one of which redacts and one of which does not**
(`appendReplayTraceEvent` vs `appendReplayTestTimingEvent`).
Expand Down
2 changes: 1 addition & 1 deletion docs/adr/0020-composable-recorded-fragments.md
Original file line number Diff line number Diff line change
Expand Up @@ -562,7 +562,7 @@ violation before the production change is trusted.

The audited seam tests are `packages/ad-script/src/internal/__tests__/script.test.ts`,
`packages/ad-replay/src/internal/__tests__/plan-digest.test.ts`,
`src/replay/__tests__/script-source-bundle.test.ts`, and
`src/commands/replay/script-source-bundle.test.ts`, and
`packages/maestro/src/internal/__tests__/replay-plan.test.ts`,
`source-closure.test.ts`, `program-loader.test.ts`,
`src/daemon/handlers/__tests__/session-replay-runtime.test.ts`. The planted-red checks below are
Expand Down
2 changes: 1 addition & 1 deletion fallow-baselines/health.json
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,7 @@
"packages/provider-webdriver/src/webdriver-utils.ts:high impact",
"src/utils/keyed-lock.ts:high impact",
"src/daemon/request-progress-protocol.ts:high impact",
"src/replay/test/reporters/format.ts:high impact",
"src/cli/replay-test/reporters/format.ts:high impact",
"src/daemon/handlers/session-test-infrastructure.ts:high impact",
"src/daemon/handlers/session-test-artifacts.ts:high impact",
"packages/platform-android/src/app-parsers.ts:high impact",
Expand Down
2 changes: 1 addition & 1 deletion packages/ad-script/src/internal/__tests__/script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -680,7 +680,7 @@ test('formatDivergenceActionLabel categorically drops fill/type text but keeps t
});

// The property test asserting "serializing a parsed script is a fixed point
// for generated scripts" stays at `src/replay/__tests__/ad-script-round-trip.test.ts`:
// for generated scripts" stays at `src/commands/replay/ad-script-round-trip.test.ts`:
// its script generator (`replayScriptArb`) is derived from the root command
// catalog and selector grammar (`src/__tests__/test-utils/property-arbitraries.ts`),
// which this package cannot import without an R11 package→root-src escape
Expand Down
5 changes: 2 additions & 3 deletions packages/ad-script/src/internal/target-annotation-identity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
* prefix matching over versioned `.ad` target-binding evidence, plus the
* bounded diagnostic diffs built on top of it. Both the writer (over
* `SnapshotNode`-derived values, `src/daemon/session-target-evidence.ts`) and
* replay-time verification (`src/daemon/handlers/session-replay-target-classification.ts`,
* `src/commands/interaction/runtime/selector-wait.ts`, and the shared
* replay-zone tree helpers in `src/replay/`) share this verbatim so both
* replay-time verification (`src/daemon/handlers/session-replay-target-classification.ts` and
* `src/commands/interaction/runtime/selector-wait.ts`) share this verbatim so both
* sides compute the SAME identity/ancestry match by construction (#1478 P5
* review, "genuinely shared recording vocabulary" relocated to its owner).
*
Expand Down
3 changes: 3 additions & 0 deletions scripts/layering/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ import {
import { runtimeExecutionIntegrityViolations } from './runtime-execution-policy.ts';
import { sourceExecutionCompatibilityViolations } from './source-execution-policy.ts';
import { sessionResourceOwnershipViolations } from './session-resource-ownership.ts';
import { replayOwnershipViolations } from './replay-ownership.ts';
import { applicationLifecycleOwnershipViolations } from './application-lifecycle-policy.ts';

const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
Expand Down Expand Up @@ -551,6 +552,7 @@ export const LAYERING_RULE_IDS = [
'package-boundaries',
'platform-package-policy',
'retired-platforms-zone',
'replay-ownership',
] as const;

export type LayeringRuleId = (typeof LAYERING_RULE_IDS)[number];
Expand Down Expand Up @@ -589,6 +591,7 @@ export const LAYERING_RULES: Readonly<Record<LayeringRuleId, LayeringRule>> = {
{ untrackedProductionFiles: listUntrackedProductionTypeScriptFiles(repoRoot) },
),
'retired-platforms-zone': () => checkRetiredPlatformsZone(listTrackedPlatformZoneFiles(repoRoot)),
'replay-ownership': (context) => replayOwnershipViolations(context.sourceFiles),
};

export function main(): number {
Expand Down
9 changes: 6 additions & 3 deletions scripts/layering/daemon-modularity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,15 @@ test('replay-test rejects request-global and engine-internal imports', () => {
'packages/replay-test/src/internal/scheduler.ts',
[
"import { emitRequestProgress } from '../../../../src/request/progress.ts';",
"import { readReplayScriptMetadata } from '../../../../src/replay/script.ts';",
"import { readReplayScriptMetadata } from '../../../../src/daemon/handlers/session-replay-runtime.ts';",
"import { parseMaestroProgram } from '../../../../src/compat/maestro/program-ir-parser.ts';",
].join('\n'),
],
['src/request/progress.ts', 'export function emitRequestProgress() {}'],
['src/replay/script.ts', 'export function readReplayScriptMetadata() {}'],
[
'src/daemon/handlers/session-replay-runtime.ts',
'export function readReplayScriptMetadata() {}',
],
['src/compat/maestro/program-ir-parser.ts', 'export function parseMaestroProgram() {}'],
]),
);
Expand All @@ -136,7 +139,7 @@ test('replay-test rejects request-global and engine-internal imports', () => {
violations.map(({ message }) => message.replace(/;.*/, '')),
[
'replay-test must not import src/request/progress.ts',
'replay-test must not import src/replay/script.ts',
'replay-test must not import src/daemon/handlers/session-replay-runtime.ts',
'replay-test must not import src/compat/maestro/program-ir-parser.ts',
],
);
Expand Down
5 changes: 1 addition & 4 deletions scripts/layering/daemon-modularity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export const LOGICAL_MODULE_POLICIES: readonly LogicalModulePolicy[] = [
'src/daemon/',
'src/providers/',
'src/request/',
'src/replay/',
'src/compat/',
'packages/maestro/',
'packages/ad-replay/',
Expand All @@ -73,7 +72,6 @@ export const LOGICAL_MODULE_POLICIES: readonly LogicalModulePolicy[] = [
const ENGINE_FILE_PREFIXES = [
'packages/ad-replay/src/',
'packages/maestro/src/',
'src/replay/',
'src/daemon/handlers/session-replay',
'packages/replay-test/src/',
] as const;
Expand Down Expand Up @@ -229,8 +227,7 @@ function checkLogicalModuleImports(edges: readonly ResolvedImportEdge[]): Layeri
}

if (!sourceModule) continue;
// A module's own files are never a forbidden target: `replay-test` sits inside the wider
// `src/replay/` engine root it may not import from.
// A module's own files are never a forbidden target.
if (sourceModule.roots.some((root) => edge.target.startsWith(root))) continue;
if (!sourceModule.forbiddenTargetRoots.some((root) => edge.target.startsWith(root))) continue;
violations.push({
Expand Down
1 change: 0 additions & 1 deletion scripts/layering/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const TARGET_DAG_RANK = new Map([
['contracts', 1],
['maestro', 1],
['recording', 1],
['replay', 1],
['replay-test', 1],
['request', 1],
['screenshot-diff', 1],
Expand Down
29 changes: 29 additions & 0 deletions scripts/layering/replay-ownership.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { REPLAY_OWNERSHIP_RULE, replayOwnershipViolations } from './replay-ownership.ts';

test('R71 rejects a planted production file under retired src/replay by name', () => {
const violations = replayOwnershipViolations(['src/replay/planted-production-file.ts']);

assert.deepEqual(violations, [
{
rule: REPLAY_OWNERSHIP_RULE,
file: 'src/replay/planted-production-file.ts',
line: 1,
message:
'src/replay/ is retired; caller source acquisition belongs under src/commands/replay/ ' +
'and replay-test presentation belongs under src/cli/replay-test/.',
},
]);
});

test('R71 accepts the current replay owners', () => {
assert.deepEqual(
replayOwnershipViolations([
'src/commands/replay/script-source-bundle.ts',
'src/cli/replay-test/reporting.ts',
'src/daemon/replay-script-source.ts',
]),
[],
);
});
17 changes: 17 additions & 0 deletions scripts/layering/replay-ownership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { LayeringViolation } from './model.ts';

const RETIRED_REPLAY_ROOT = 'src/replay/';
export const REPLAY_OWNERSHIP_RULE = 'R71 replay-ownership';

export function replayOwnershipViolations(sourceFiles: readonly string[]): LayeringViolation[] {
return sourceFiles
.filter((file) => file.startsWith(RETIRED_REPLAY_ROOT))
.map((file) => ({
rule: REPLAY_OWNERSHIP_RULE,
file,
line: 1,
message:
`${RETIRED_REPLAY_ROOT} is retired; caller source acquisition belongs under ` +
'src/commands/replay/ and replay-test presentation belongs under src/cli/replay-test/.',
}));
}
21 changes: 21 additions & 0 deletions src/__tests__/cli-startup-import-closure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,27 @@ test('the CLI startup import closure never evaluates the Maestro engine', () =>
).toEqual([]);
});

test('the replay command source closure keeps Maestro behind flow selection', () => {
const replayEntry = path.join(srcRoot, 'commands/replay/index.ts');
const replayClosure = eagerClosureOf(replayEntry);
const sourceBundle = path.join(srcRoot, 'commands/replay/script-source-bundle.ts');
const offenders: string[] = [];

expect(replayClosure).toContain(sourceBundle);
for (const file of replayClosure) {
if (
eagerlyEvaluatedModules(file, fs.readFileSync(file, 'utf8')).includes('@agent-device/maestro')
) {
offenders.push(path.relative(srcRoot, file));
}
}

expect(
offenders,
'The replay command may evaluate @agent-device/maestro only inside flow source collection.',
).toEqual([]);
});

test('the CLI startup import closure is reachable and crosses the package boundary', () => {
// Guards the test above from silently passing because the walk found nothing:
// a resolver that returned null for everything would leave both the src side
Expand Down
3 changes: 1 addition & 2 deletions src/__tests__/test-utils/property-arbitraries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,8 +334,7 @@ type ReplayLinePlan = fc.Arbitrary<string> | { waived: string };
* Commands whose `.ad` line is a bare `<command> <token>…` handled by the
* generic parse/print branch (`appendGenericActionScriptArgs`), whose shape the
* `wait`/`longpress` templates already exercise. A command that grows its own
* branch in src/replay/script.ts or src/replay/script-formatting.ts must move
* to a template.
* branch in the replay command's script formatting must move to a template.
*/
const GENERIC_REPLAY_LINE = {
waived: 'generic line shape, covered by the wait/longpress templates',
Expand Down
19 changes: 7 additions & 12 deletions src/__tests__/test-utils/replay-script-source.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,15 @@
import fs from 'node:fs';
import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay';
import type { MaestroSourceReader } from '@agent-device/maestro';
import { resolveUserPath } from '@agent-device/host-kit/file';
import type { DaemonRequest } from '../../daemon/types.ts';
import {
loadReplayScriptSourceBundle,
readAdScriptSourceBundle,
} from '../../replay/script-source-bundle.ts';
import { discoverReplaySourcePaths } from '../../replay/source-discovery.ts';
import { loadReplayScriptSourceBundle } from '../../commands/replay/script-source-bundle.ts';
import { discoverReplaySourcePaths } from '../../commands/replay/source-discovery.ts';

/**
* A native `.ad` script's bundle, built from a file on disk through the client's own reader
* (#1802) so a test never hand-rolls a shape the CLI would not actually send. Synchronous
* because an `.ad` script has no include grammar and needs no engine; use
* `maestroScriptSourceBundleFor` for a flow.
*/
/** A native `.ad` script's one-entry source bundle for test requests. */
export function replayScriptSourceBundleFor(filePath: string): ReplayScriptSourceBundle {
return readAdScriptSourceBundle({ inputPath: filePath, cwd: process.cwd() });
const entry = resolveUserPath(filePath, { cwd: process.cwd() });
return { entry, files: { [entry]: fs.readFileSync(entry, 'utf8') } };
}

/** A Maestro flow's bundle — async because the engine that walks its includes loads on demand. */
Expand Down
4 changes: 2 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { pathToFileURL } from 'node:url';
import { sendToDaemon } from './daemon/client/daemon-client.ts';
import fs from 'node:fs';
import type { BatchStep } from '@agent-device/contracts/client';
import type { ReplayTestReporterRuntime } from './replay/test/reporting.ts';
import type { ReplayTestReporterRuntime } from './cli/replay-test/reporting.ts';
import {
createAgentDeviceClient,
type AgentDeviceClientConfig,
Expand Down Expand Up @@ -478,7 +478,7 @@ async function createReplayReporterForTest(
if (ctx.command !== 'test') return undefined;
// Lazy: the replay test reporter is only needed by `test`, and its
// static import would put the reporting runtime on every command's path.
const { createReplayTestReporterRuntime } = await import('./replay/test/reporting.ts');
const { createReplayTestReporterRuntime } = await import('./cli/replay-test/reporting.ts');
return createReplayTestReporterRuntime({
debug: ctx.debugOutputEnabled,
verbose: ctx.effectiveFlags.verbose,
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function writeGenericCliOutput(
): Promise<number> | number {
if (command === 'test') {
// Lazy: keeps the replay test reporting runtime off every other command's path.
return import('../../replay/test/reporting.ts').then(({ renderReplayTestResponse }) =>
return import('../replay-test/reporting.ts').then(({ renderReplayTestResponse }) =>
renderReplayTestResponse({
suite: data as ReplaySuiteResult,
debug: options.debug,
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/router-types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CliFlags } from '@agent-device/contracts/command';
import type { AgentDeviceClient } from '../../agent-device-client.ts';
import type { CliCommandName } from '../../command-catalog.ts';
import type { ReplayTestReporterRuntime } from '../../replay/test/reporting.ts';
import type { ReplayTestReporterRuntime } from '../replay-test/reporting.ts';

export type ClientCommandParams = {
positionals: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,16 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import type { ReplaySuiteResult } from '@agent-device/contracts/replay';
import { handleSessionCommands } from './session-command-harness.ts';
import { SessionStore } from '../../session-store.ts';
import type { DaemonRequest, DaemonResponse } from '../../types.ts';
import { handleSessionCommands } from '../../../daemon/handlers/__tests__/session-command-harness.ts';
import { SessionStore } from '../../../daemon/session-store.ts';
import type { DaemonRequest, DaemonResponse } from '../../../daemon/types.ts';
import { withRequestProgressSink } from '@agent-device/host-kit/request';
import {
getReplayTestReporterExitCode,
runReplayTestReporterProgress,
runReplayTestReporters,
} from '../../../replay/test/reporters/registry.ts';
import type {
ReplayTestReporter,
ReplayTestReporterContext,
} from '../../../replay/test/reporters/types.ts';
} from '../reporters/registry.ts';
import type { ReplayTestReporter, ReplayTestReporterContext } from '../reporters/types.ts';

type RecordedHook = {
hook: 'onSuiteStart' | 'onTestStart' | 'onTestStep' | 'onTestResult' | 'onSuiteEnd';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import { expect, test, vi } from 'vitest';
import { mkdtempForTestSync } from '../../../__tests__/test-utils/tmp-dir.ts';

vi.mock('../snapshot-interactor-capture.ts', () => ({
vi.mock('../../../daemon/handlers/snapshot-interactor-capture.ts', () => ({
captureSnapshotWithInteractor: vi.fn(async () => {
throw new Error('no device runner available in this test');
}),
Expand All @@ -23,21 +23,18 @@ import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import type { ReplaySuiteResult } from '@agent-device/contracts/replay';
import { handleSessionCommands } from './session-command-harness.ts';
import { SessionStore } from '../../session-store.ts';
import type { DaemonRequest, DaemonResponse } from '../../types.ts';
import { handleSessionCommands } from '../../../daemon/handlers/__tests__/session-command-harness.ts';
import { SessionStore } from '../../../daemon/session-store.ts';
import type { DaemonRequest, DaemonResponse } from '../../../daemon/types.ts';
import { withRequestProgressSink } from '@agent-device/host-kit/request';
import { withTestDeviceInventoryProvider as withDeviceInventoryProvider } from '../../../__tests__/test-utils/device-inventory-gateways.ts';
import type { DeviceInfo } from '@agent-device/kernel/device';
import {
getReplayTestReporterExitCode,
runReplayTestReporterProgress,
runReplayTestReporters,
} from '../../../replay/test/reporters/registry.ts';
import type {
ReplayTestReporter,
ReplayTestReporterContext,
} from '../../../replay/test/reporters/types.ts';
} from '../reporters/registry.ts';
import type { ReplayTestReporter, ReplayTestReporterContext } from '../reporters/types.ts';

type RecordedHook = {
hook: 'onSuiteStart' | 'onTestStart' | 'onTestStep' | 'onTestResult' | 'onSuiteEnd';
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions src/commands/replay/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ import {
parseReplayCliEnvEntries,
readReplayCliEnvEntries,
} from '@agent-device/ad-script';
import { loadReplayScriptSourceBundle } from '../../replay/script-source-bundle.ts';
import { discoverReplaySourcePaths } from '../../replay/source-discovery.ts';
import { loadReplayScriptSourceBundle } from './script-source-bundle.ts';
import { discoverReplaySourcePaths } from './source-discovery.ts';

const REPLAY_COMMAND_NAME = 'replay';
const TEST_COMMAND_NAME = 'test';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import fs from 'node:fs';
import path from 'node:path';
import { expect, test } from 'vitest';
import { AppError } from '@agent-device/kernel/errors';
import {
loadReplayScriptSourceBundle,
readReplayScriptSourceFile,
REPLAY_SCRIPT_SOURCE_BUNDLE_MAX_BYTES,
} from '../script-source-bundle.ts';
} from './script-source-bundle.ts';
import { mkdtempForTestSync } from '../../__tests__/test-utils/tmp-dir.ts';

// #1802: the caller reads every script file a replay run needs and sends the text. These pin the
Expand Down Expand Up @@ -78,17 +76,3 @@ test('an oversized bundle is refused with the limit named', async () => {
}),
);
});

test('reading a file the bundle does not carry names the file and the entry', () => {
const bundle = { entry: '/flows/login.yaml', files: { '/flows/login.yaml': '---\n- back\n' } };

try {
readReplayScriptSourceFile(bundle, '/flows/missing.yaml');
expect.unreachable('expected a missing bundled source to throw');
} catch (error) {
expect(error).toBeInstanceOf(AppError);
expect((error as AppError).code).toBe('INVALID_ARGS');
expect((error as AppError).message).toContain('/flows/missing.yaml');
expect((error as AppError).details).toMatchObject({ entry: '/flows/login.yaml' });
}
});
Loading
Loading