mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
d4cf02a889
* chore(gates): classify #2278 daemon-platform-runtime edges (R74) and ratchet handler session authority (R75) * chore(gates): discover the R75 shape target from the SessionState declaration * chore(gates): capture dynamic-import bindings in R74 so symbol drift cannot hide * chore(gates): reject dynamic-import destructure residue in R74 * chore(gates): R74 rejects open-ended dynamic imports beside named bindings * chore(gates): renumber daemon-platform-runtime-inventory to R76 (R74 taken on main)
680 lines
24 KiB
TypeScript
680 lines
24 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
import {
|
|
checkDaemonModularityRatchets,
|
|
checkRetiredInteractionPaths,
|
|
checkRetiredSessionLifecyclePaths,
|
|
checkRetiredSessionObservabilityPaths,
|
|
checkRetiredSnapshotExecutionPaths,
|
|
DAEMON_MODULARITY_BASELINE,
|
|
} from './daemon-modularity.ts';
|
|
import { resolveImportEdges, targetDagZone, type ResolvedImportEdge } from './model.ts';
|
|
import type { LayeringRatchets } from './ratchet-reference.ts';
|
|
|
|
function importEdge(file: string, target: string): ResolvedImportEdge {
|
|
return {
|
|
file,
|
|
target,
|
|
spec: target,
|
|
line: 1,
|
|
dynamic: false,
|
|
typeOnly: true,
|
|
symbols: [],
|
|
bindingResidue: false,
|
|
fromZone: targetDagZone(file),
|
|
toZone: targetDagZone(target),
|
|
};
|
|
}
|
|
|
|
function baselineDaemonTypesEdges(): ResolvedImportEdge[] {
|
|
return DAEMON_MODULARITY_BASELINE.externalDaemonTypesImporters.map((file) =>
|
|
importEdge(file, 'src/daemon/daemon-request.ts'),
|
|
);
|
|
}
|
|
|
|
/** Every baseline edge is present and nothing else is forbidden: the quiet state of the ratchets. */
|
|
function baselineEdges(): ResolvedImportEdge[] {
|
|
return baselineDaemonTypesEdges();
|
|
}
|
|
|
|
/** Where a member of `zone` lives, so a zone count can be turned back into file paths. */
|
|
const ZONE_DIRECTORY: Readonly<Record<string, string>> = {
|
|
'(root)': 'src/',
|
|
'daemon-server': 'src/daemon/',
|
|
'ad-replay': 'packages/ad-replay/src/',
|
|
'provider-webdriver': 'packages/provider-webdriver/src/',
|
|
};
|
|
|
|
/** A cycle membership of `count` files per zone, so a zone count becomes file paths. */
|
|
function typeCycleMembers(zones: Readonly<Record<string, number>>): string[] {
|
|
return Object.entries(zones).flatMap(([zone, count]) =>
|
|
Array.from(
|
|
{ length: count },
|
|
(_, index) => `${ZONE_DIRECTORY[zone] ?? `src/${zone}/`}probe-${index}.ts`,
|
|
),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The merge-base measurement every test ratchets against. R9 and R10's R7 counts have no recorded
|
|
* numbers any more, so a test states its own reference tree instead of importing one.
|
|
*/
|
|
const REFERENCE: LayeringRatchets = {
|
|
typeInversions: {},
|
|
largestTypeCycle: typeCycleMembers({ 'provider-webdriver': 6 }),
|
|
sessionState: { writerOwnedFields: 19, ownerFileClaims: 22 },
|
|
};
|
|
|
|
/** The same measurement as the reference except where a test moves one number. */
|
|
function measured(overrides: Partial<LayeringRatchets> = {}): LayeringRatchets {
|
|
return { ...REFERENCE, ...overrides };
|
|
}
|
|
|
|
test('R10 rejects R7 ownership pressure that grew past the merge-base', () => {
|
|
const grownFields = checkDaemonModularityRatchets(
|
|
baselineEdges(),
|
|
measured({ sessionState: { writerOwnedFields: 20, ownerFileClaims: 23 } }),
|
|
REFERENCE,
|
|
);
|
|
assert.deepEqual(
|
|
grownFields.map(({ rule, message }) => ({ rule, message })),
|
|
[
|
|
{
|
|
rule: 'R10 daemon-modularity',
|
|
message:
|
|
'R7 writerOwnedFields grew to 20 (baseline 19 at the merge-base). Route the new write ' +
|
|
'through an existing owner instead.',
|
|
},
|
|
{
|
|
rule: 'R10 daemon-modularity',
|
|
message:
|
|
'R7 ownerFileClaims grew to 23 (baseline 22 at the merge-base). Route the new write ' +
|
|
'through an existing owner instead.',
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
test('R10 banks an R7 shrink with no edit anywhere', () => {
|
|
assert.deepEqual(
|
|
checkDaemonModularityRatchets(
|
|
baselineEdges(),
|
|
measured({ sessionState: { writerOwnedFields: 18, ownerFileClaims: 20 } }),
|
|
REFERENCE,
|
|
),
|
|
[],
|
|
);
|
|
});
|
|
|
|
test('external daemon request/session-state importer membership changes require the baseline to change', () => {
|
|
const edges = resolveImportEdges(
|
|
new Map([
|
|
[
|
|
'src/client/new-importer.ts',
|
|
"import type { SessionState } from '../daemon/session-state.ts';",
|
|
],
|
|
['src/daemon/session-state.ts', 'export type SessionState = { name: string };'],
|
|
]),
|
|
);
|
|
|
|
const violations = checkDaemonModularityRatchets(
|
|
[...baselineEdges(), ...edges],
|
|
REFERENCE,
|
|
REFERENCE,
|
|
);
|
|
assert.equal(violations.length, 1);
|
|
// The recorded list owns its own size (#2342 relocated five client files into it), so the
|
|
// message assertion reads it rather than pinning a literal that the baseline would outgrow.
|
|
assert.match(
|
|
violations[0]!.message,
|
|
new RegExp(
|
|
`may only shrink from the recorded ${DAEMON_MODULARITY_BASELINE.externalDaemonTypesImporters.length}`,
|
|
),
|
|
);
|
|
|
|
const removed = checkDaemonModularityRatchets(
|
|
baselineDaemonTypesEdges().slice(1),
|
|
REFERENCE,
|
|
REFERENCE,
|
|
);
|
|
assert.equal(removed.length, 1);
|
|
assert.match(removed[0]!.message, /delete it from externalDaemonTypesImporters/);
|
|
});
|
|
|
|
test('logical modules reject forbidden imports', () => {
|
|
const edges = resolveImportEdges(
|
|
new Map([
|
|
[
|
|
'packages/replay-test/src/internal/scheduler.ts',
|
|
"import type { Device } from '../../../../src/providers/device.ts';",
|
|
],
|
|
['src/providers/device.ts', 'export type Device = { id: string };'],
|
|
]),
|
|
);
|
|
|
|
const violations = checkDaemonModularityRatchets(
|
|
[...baselineEdges(), ...edges],
|
|
REFERENCE,
|
|
REFERENCE,
|
|
);
|
|
assert.equal(violations.length, 1);
|
|
assert.match(violations[0]!.message, /replay-test must not import/);
|
|
});
|
|
|
|
test('replay-test rejects request-global and engine-internal imports', () => {
|
|
const edges = resolveImportEdges(
|
|
new Map([
|
|
[
|
|
'packages/replay-test/src/internal/scheduler.ts',
|
|
[
|
|
"import { emitRequestProgress } from '../../../../src/request/progress.ts';",
|
|
"import { readReplayScriptMetadata } from '../../../../src/daemon/replay/internal/native-command.ts';",
|
|
"import { parseMaestroProgram } from '../../../../src/compat/maestro/program-ir-parser.ts';",
|
|
].join('\n'),
|
|
],
|
|
['src/request/progress.ts', 'export function emitRequestProgress() {}'],
|
|
[
|
|
'src/daemon/replay/internal/native-command.ts',
|
|
'export function readReplayScriptMetadata() {}',
|
|
],
|
|
['src/compat/maestro/program-ir-parser.ts', 'export function parseMaestroProgram() {}'],
|
|
]),
|
|
);
|
|
|
|
const violations = checkDaemonModularityRatchets(
|
|
[...baselineEdges(), ...edges],
|
|
REFERENCE,
|
|
REFERENCE,
|
|
);
|
|
assert.deepEqual(
|
|
violations.map(({ message }) => message.replace(/;.*/, '')),
|
|
[
|
|
'replay-test must not import src/request/progress.ts',
|
|
"packages/replay-test/src/internal/scheduler.ts must not import daemon-replay's internal tree (src/daemon/replay/internal/native-command.ts)",
|
|
'replay-test must not import src/compat/maestro/program-ir-parser.ts',
|
|
],
|
|
);
|
|
});
|
|
|
|
test('replay-test may still import its own files inside the package', () => {
|
|
const edges = resolveImportEdges(
|
|
new Map([
|
|
[
|
|
'packages/replay-test/src/internal/reporting.ts',
|
|
"import { spec } from './reporters/spec.ts';",
|
|
],
|
|
['packages/replay-test/src/internal/reporters/spec.ts', 'export const spec = 1;'],
|
|
]),
|
|
);
|
|
|
|
assert.deepEqual(
|
|
checkDaemonModularityRatchets([...baselineEdges(), ...edges], REFERENCE, REFERENCE),
|
|
[],
|
|
);
|
|
});
|
|
|
|
test('internal trees reject deep imports globally, including from daemon', () => {
|
|
const edges = resolveImportEdges(
|
|
new Map([
|
|
['src/daemon/adapter.ts', "import type { Plan } from '@agent-device/maestro/internal/plan';"],
|
|
['packages/maestro/src/internal/plan.ts', 'export type Plan = { steps: number };'],
|
|
]),
|
|
);
|
|
|
|
const violations = checkDaemonModularityRatchets(
|
|
[...baselineEdges(), ...edges],
|
|
REFERENCE,
|
|
REFERENCE,
|
|
);
|
|
assert.equal(violations.length, 1);
|
|
assert.match(violations[0]!.message, /must not import maestro's internal tree/);
|
|
});
|
|
|
|
test('daemon replay rejects handler, owner, session-store, and engine deep edges', () => {
|
|
const edges = resolveImportEdges(
|
|
new Map([
|
|
[
|
|
'src/daemon/handlers/session.ts',
|
|
"import { runReplayCommand } from '../replay/internal/native-command.ts';",
|
|
],
|
|
[
|
|
'src/daemon/replay/internal/test-command.ts',
|
|
"import { handleSessionCloseCommands } from '../../session-lifecycle/internal/session-close.ts';",
|
|
],
|
|
[
|
|
'src/daemon/replay/internal/close-command.ts',
|
|
"import { handleSessionCloseCommands } from '../../session-lifecycle/index.ts';",
|
|
],
|
|
[
|
|
'src/daemon/replay/internal/command-types.ts',
|
|
"import { SessionStore } from '../../session-store.ts';",
|
|
],
|
|
[
|
|
'packages/ad-replay/src/internal/step-loop.ts',
|
|
"import { runReplayCommand } from '../../../../src/daemon/replay/internal/native-command.ts';",
|
|
],
|
|
['src/daemon/replay/internal/native-command.ts', 'export function runReplayCommand() {}'],
|
|
[
|
|
'src/daemon/session-lifecycle/internal/session-close.ts',
|
|
'export function handleSessionCloseCommands() {}',
|
|
],
|
|
['src/daemon/session-lifecycle/index.ts', 'export function handleSessionCloseCommands() {}'],
|
|
['src/daemon/session-store.ts', 'export class SessionStore {}'],
|
|
]),
|
|
);
|
|
|
|
const violations = checkDaemonModularityRatchets(
|
|
[...baselineEdges(), ...edges],
|
|
REFERENCE,
|
|
REFERENCE,
|
|
);
|
|
assert.deepEqual(
|
|
violations.map(({ file, line, message }) => ({
|
|
file,
|
|
line,
|
|
message: message.replace(/;.*/, ''),
|
|
})),
|
|
[
|
|
{
|
|
file: 'src/daemon/handlers/session.ts',
|
|
line: 1,
|
|
message:
|
|
"src/daemon/handlers/session.ts must not import daemon-replay's internal tree (src/daemon/replay/internal/native-command.ts)",
|
|
},
|
|
{
|
|
file: 'src/daemon/replay/internal/test-command.ts',
|
|
line: 1,
|
|
message:
|
|
"src/daemon/replay/internal/test-command.ts must not import daemon-session-lifecycle's internal tree (src/daemon/session-lifecycle/internal/session-close.ts)",
|
|
},
|
|
{
|
|
file: 'src/daemon/replay/internal/close-command.ts',
|
|
line: 1,
|
|
message: 'daemon-replay must not import src/daemon/session-lifecycle/index.ts',
|
|
},
|
|
{
|
|
file: 'src/daemon/replay/internal/command-types.ts',
|
|
line: 1,
|
|
message: 'daemon-replay must not import src/daemon/session-store.ts',
|
|
},
|
|
{
|
|
file: 'packages/ad-replay/src/internal/step-loop.ts',
|
|
line: 1,
|
|
message:
|
|
"packages/ad-replay/src/internal/step-loop.ts must not import daemon-replay's internal tree (src/daemon/replay/internal/native-command.ts)",
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
test('session lifecycle rejects handler deep imports in both directions', () => {
|
|
const edges = resolveImportEdges(
|
|
new Map([
|
|
[
|
|
'src/daemon/handlers/session.ts',
|
|
"import { handleSessionInventoryCommands } from '../session-lifecycle/internal/inventory.ts';\nexport function handleSessionCommands() {}",
|
|
],
|
|
[
|
|
'src/daemon/session-lifecycle/internal/inventory.ts',
|
|
"import { handleSessionCommands } from '../../handlers/session.ts';\nexport function handleSessionInventoryCommands() {}",
|
|
],
|
|
[
|
|
'src/daemon/session-lifecycle/internal/session-close.ts',
|
|
"import { handleSessionCommands } from '../../handlers/session.ts';",
|
|
],
|
|
[
|
|
'src/daemon/session-lifecycle/index.ts',
|
|
'export function handleSessionInventoryCommands() {}',
|
|
],
|
|
]),
|
|
);
|
|
|
|
const violations = checkDaemonModularityRatchets(
|
|
[...baselineEdges(), ...edges],
|
|
REFERENCE,
|
|
REFERENCE,
|
|
);
|
|
assert.deepEqual(
|
|
violations.map(({ file, line, message }) => ({
|
|
file,
|
|
line,
|
|
message: message.replace(/;.*/, ''),
|
|
})),
|
|
[
|
|
{
|
|
file: 'src/daemon/handlers/session.ts',
|
|
line: 1,
|
|
message:
|
|
"src/daemon/handlers/session.ts must not import daemon-session-lifecycle's internal tree (src/daemon/session-lifecycle/internal/inventory.ts)",
|
|
},
|
|
{
|
|
file: 'src/daemon/session-lifecycle/internal/inventory.ts',
|
|
line: 1,
|
|
message: 'daemon-session-lifecycle must not import src/daemon/handlers/session.ts',
|
|
},
|
|
{
|
|
file: 'src/daemon/session-lifecycle/internal/session-close.ts',
|
|
line: 1,
|
|
message: 'daemon-session-lifecycle must not import src/daemon/handlers/session.ts',
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
test('interaction rejects handler crossings and deep imports around its facade', () => {
|
|
const edges = resolveImportEdges(
|
|
new Map([
|
|
[
|
|
'src/daemon/handlers/react-native.ts',
|
|
"import { refSnapshotFlagGuardResponse } from '../interaction/internal/interaction-flags.ts';\nexport function handleReactNativeCommands() {}",
|
|
],
|
|
[
|
|
'src/daemon/interaction/internal/interaction-runtime.ts',
|
|
"import { handleReactNativeCommands } from '../../handlers/react-native.ts';\nexport function createInteractionRuntime() {}",
|
|
],
|
|
[
|
|
'src/daemon/generic-settle.ts',
|
|
"import { createInteractionRuntime } from './interaction/internal/interaction-runtime.ts';",
|
|
],
|
|
[
|
|
'src/daemon/selector-runtime.ts',
|
|
"import { refSnapshotFlagGuardResponse } from './interaction/internal/interaction-flags.ts';",
|
|
],
|
|
[
|
|
'src/daemon/selector-runtime-backend.ts',
|
|
"import { readTextForNode } from './interaction/internal/interaction-ref-policy.ts';",
|
|
],
|
|
[
|
|
'src/daemon/interaction/internal/interaction-flags.ts',
|
|
'export function refSnapshotFlagGuardResponse() {}',
|
|
],
|
|
[
|
|
'src/daemon/interaction/internal/interaction-ref-policy.ts',
|
|
'export function readTextForNode() {}',
|
|
],
|
|
]),
|
|
);
|
|
|
|
const violations = checkDaemonModularityRatchets(
|
|
[...baselineEdges(), ...edges],
|
|
REFERENCE,
|
|
REFERENCE,
|
|
);
|
|
assert.equal(violations.length, 5);
|
|
assert.ok(
|
|
violations.some(({ message }) =>
|
|
message.includes(
|
|
"src/daemon/handlers/react-native.ts must not import daemon-interaction's internal tree",
|
|
),
|
|
),
|
|
);
|
|
assert.ok(
|
|
violations.some(({ message }) =>
|
|
message.includes(
|
|
"src/daemon/interaction/internal/interaction-runtime.ts must not import src/daemon/handlers/react-native.ts from daemon-interaction's internal tree",
|
|
),
|
|
),
|
|
);
|
|
assert.equal(
|
|
violations.filter(({ message }) =>
|
|
message.includes("must not import daemon-interaction's internal tree"),
|
|
).length,
|
|
4,
|
|
);
|
|
});
|
|
|
|
test('session observability rejects handler deep imports in both directions', () => {
|
|
const edges = resolveImportEdges(
|
|
new Map([
|
|
[
|
|
'src/daemon/handlers/session.ts',
|
|
"import { handleSessionObservabilityCommands } from '../session-observability/internal/session-observability.ts';\nexport function handleSessionCommands() {}",
|
|
],
|
|
[
|
|
'src/daemon/session-observability/internal/session-observability.ts',
|
|
"import { handleSessionCommands } from '../../handlers/session.ts';\nexport function handleSessionObservabilityCommands() {}",
|
|
],
|
|
[
|
|
'src/daemon/session-observability/index.ts',
|
|
'export function handleSessionObservabilityCommands() {}',
|
|
],
|
|
]),
|
|
);
|
|
|
|
const violations = checkDaemonModularityRatchets(
|
|
[...baselineEdges(), ...edges],
|
|
REFERENCE,
|
|
REFERENCE,
|
|
);
|
|
assert.deepEqual(
|
|
violations.map(({ file, line, message }) => ({
|
|
file,
|
|
line,
|
|
message: message.replace(/;.*/, ''),
|
|
})),
|
|
[
|
|
{
|
|
file: 'src/daemon/handlers/session.ts',
|
|
line: 1,
|
|
message:
|
|
"src/daemon/handlers/session.ts must not import daemon-session-observability's internal tree (src/daemon/session-observability/internal/session-observability.ts)",
|
|
},
|
|
{
|
|
file: 'src/daemon/session-observability/internal/session-observability.ts',
|
|
line: 1,
|
|
message: 'daemon-session-observability must not import src/daemon/handlers/session.ts',
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
test('snapshot execution cannot return to handler-owned support paths', () => {
|
|
const restored = [
|
|
'src/daemon/handlers/snapshot-capture.ts',
|
|
'src/daemon/handlers/snapshot-interactor-capture.ts',
|
|
'src/daemon/handlers/snapshot-session.ts',
|
|
];
|
|
|
|
assert.deepEqual(
|
|
checkRetiredSnapshotExecutionPaths(restored).map(({ file, message }) => ({ file, message })),
|
|
restored.map((file) => ({
|
|
file,
|
|
message:
|
|
`retired snapshot execution handler path was restored: ${file}. ` +
|
|
'Reuse the daemon-owned snapshot execution module instead of restoring shared mechanics beneath a route adapter.',
|
|
})),
|
|
);
|
|
assert.deepEqual(checkRetiredSnapshotExecutionPaths(['src/daemon/handlers/snapshot.ts']), []);
|
|
});
|
|
|
|
test('session lifecycle rejects restored neutral helper paths', () => {
|
|
const violations = checkRetiredSessionLifecyclePaths([
|
|
'src/daemon/session-device-resolution.ts',
|
|
'src/daemon/handlers/session-device-utils.ts',
|
|
'src/daemon/handlers/session-runtime-admission.ts',
|
|
'src/daemon/handlers/session-close-script.ts',
|
|
'src/daemon/handlers/session-close.ts',
|
|
]);
|
|
|
|
assert.deepEqual(
|
|
violations.map(({ message }) => message),
|
|
[
|
|
'retired session lifecycle path was restored: src/daemon/handlers/session-device-utils.ts. Keep the neutral seam at its daemon owner instead of rebuilding a handler grab-bag.',
|
|
'retired session lifecycle path was restored: src/daemon/handlers/session-runtime-admission.ts. Keep the neutral seam at its daemon owner instead of rebuilding a handler grab-bag.',
|
|
'retired session lifecycle path was restored: src/daemon/handlers/session-close-script.ts. Keep the neutral seam at its daemon owner instead of rebuilding a handler grab-bag.',
|
|
'retired session lifecycle path was restored: src/daemon/handlers/session-close.ts. Keep the neutral seam at its daemon owner instead of rebuilding a handler grab-bag.',
|
|
],
|
|
);
|
|
});
|
|
|
|
test('interaction rejects restored handler implementation paths', () => {
|
|
const violations = checkRetiredInteractionPaths([
|
|
'src/daemon/interaction/internal/interaction.ts',
|
|
'src/daemon/handlers/interaction-touch.ts',
|
|
]);
|
|
|
|
assert.deepEqual(violations, [
|
|
{
|
|
rule: 'R10 daemon-modularity',
|
|
file: 'src/daemon/handlers/interaction-touch.ts',
|
|
line: 1,
|
|
message:
|
|
'retired interaction handler path was restored: src/daemon/handlers/interaction-touch.ts. Keep route implementations behind src/daemon/interaction/index.ts instead of rebuilding a handler-owned interaction surface.',
|
|
},
|
|
]);
|
|
});
|
|
|
|
test('interaction rejects renamed handler implementation paths', () => {
|
|
const violations = checkRetiredInteractionPaths([
|
|
'src/daemon/handlers/interaction-touch-v2.ts',
|
|
'src/daemon/handlers/find-next.ts',
|
|
]);
|
|
|
|
assert.deepEqual(
|
|
violations.map(({ file, message }) => ({ file, message })),
|
|
[
|
|
{
|
|
file: 'src/daemon/handlers/interaction-touch-v2.ts',
|
|
message:
|
|
'retired interaction handler path was restored: src/daemon/handlers/interaction-touch-v2.ts. Keep route implementations behind src/daemon/interaction/index.ts instead of rebuilding a handler-owned interaction surface.',
|
|
},
|
|
{
|
|
file: 'src/daemon/handlers/find-next.ts',
|
|
message:
|
|
'retired interaction handler path was restored: src/daemon/handlers/find-next.ts. Keep route implementations behind src/daemon/interaction/index.ts instead of rebuilding a handler-owned interaction surface.',
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
test('session lifecycle rejects any restored open or close handler path', () => {
|
|
const violations = checkRetiredSessionLifecyclePaths([
|
|
'src/daemon/handlers/session-open-regressed.ts',
|
|
'src/daemon/handlers/session-close-regressed.ts',
|
|
]);
|
|
|
|
assert.deepEqual(
|
|
violations.map(({ file, message }) => ({
|
|
rule: 'R10 daemon-modularity',
|
|
file,
|
|
line: 1,
|
|
message,
|
|
})),
|
|
[
|
|
{
|
|
rule: 'R10 daemon-modularity',
|
|
file: 'src/daemon/handlers/session-open-regressed.ts',
|
|
line: 1,
|
|
message:
|
|
'retired session lifecycle path was restored: src/daemon/handlers/session-open-regressed.ts. Keep the neutral seam at its daemon owner instead of rebuilding a handler grab-bag.',
|
|
},
|
|
{
|
|
rule: 'R10 daemon-modularity',
|
|
file: 'src/daemon/handlers/session-close-regressed.ts',
|
|
line: 1,
|
|
message:
|
|
'retired session lifecycle path was restored: src/daemon/handlers/session-close-regressed.ts. Keep the neutral seam at its daemon owner instead of rebuilding a handler grab-bag.',
|
|
},
|
|
],
|
|
);
|
|
});
|
|
|
|
test('session observability rejects restored handler paths', () => {
|
|
const restoredPaths = [
|
|
'src/daemon/handlers/session-observability.ts',
|
|
'src/daemon/handlers/session-perf-runtime.ts',
|
|
'src/daemon/handlers/session-network.ts',
|
|
'src/daemon/handlers/session-audio.ts',
|
|
'src/daemon/handlers/session-network-regressed.ts',
|
|
'src/daemon/handlers/session-perf.ts',
|
|
'src/daemon/handlers/session-logs.ts',
|
|
'src/daemon/handlers/session-events.ts',
|
|
] as const;
|
|
const violations = checkRetiredSessionObservabilityPaths(restoredPaths);
|
|
|
|
assert.deepEqual(
|
|
violations.map(({ file, message }) => ({ file, message })),
|
|
restoredPaths.map((file) => ({
|
|
file,
|
|
message:
|
|
`retired session observability path was restored: ${file}. ` +
|
|
'Keep the neutral seam at its daemon owner instead of rebuilding a handler grab-bag.',
|
|
})),
|
|
);
|
|
});
|
|
|
|
test('R9 holds each zone to the merge-base and keeps engine files outside the component', () => {
|
|
// One commands file and one engine file traded for two provider-webdriver ones, so the
|
|
// total stays at the merge-base's size and only the per-zone claims are on trial.
|
|
const violations = checkDaemonModularityRatchets(
|
|
baselineEdges(),
|
|
measured({
|
|
largestTypeCycle: typeCycleMembers({
|
|
commands: 1,
|
|
'ad-replay': 1,
|
|
'provider-webdriver': 4,
|
|
}),
|
|
}),
|
|
REFERENCE,
|
|
);
|
|
|
|
assert.equal(violations.length, 3);
|
|
assert.ok(violations.some(({ message }) => /contains 1 commands file/.test(message)));
|
|
assert.ok(violations.some(({ message }) => /contains 1 ad-replay file/.test(message)));
|
|
assert.ok(violations.some(({ message }) => /engine file entered/.test(message)));
|
|
});
|
|
|
|
// #1837: the zone violation used to name the alphabetically-first zone member — a file that had
|
|
// been in the cycle all along — so the +1 was found only by diffing member lists between commits.
|
|
// The merge-base carries membership, so the message names exactly the files that joined.
|
|
test('R10 zone overflow names the member that joined the cycle', () => {
|
|
// Sorts after the provider-webdriver probes: a first-member pick could not name it by luck.
|
|
const joined = 'src/daemon/snapshot-interactor-capture.ts';
|
|
const members = [...typeCycleMembers({ 'provider-webdriver': 5 }), joined].sort();
|
|
|
|
const violations = checkDaemonModularityRatchets(
|
|
baselineEdges(),
|
|
measured({ largestTypeCycle: members }),
|
|
REFERENCE,
|
|
);
|
|
|
|
assert.equal(violations.length, 1);
|
|
const [violation] = violations;
|
|
assert.equal(violation!.rule, 'R10 daemon-modularity');
|
|
assert.equal(violation!.file, 'scripts/layering/daemon-modularity.ts');
|
|
assert.match(
|
|
violation!.message,
|
|
/contains 1 daemon-server file\(s\) \(baseline 0 at the merge-base\)/,
|
|
);
|
|
assert.match(
|
|
violation!.message,
|
|
new RegExp(`1 over the merge-base — the daemon-server file\\(s\\) that joined: ${joined}\\.`),
|
|
);
|
|
});
|
|
|
|
test('R9 rejects a cycle grown past the merge-base', () => {
|
|
const violations = checkDaemonModularityRatchets(
|
|
baselineEdges(),
|
|
measured({ largestTypeCycle: typeCycleMembers({ 'provider-webdriver': 7 }) }),
|
|
REFERENCE,
|
|
);
|
|
|
|
assert.equal(violations.length, 2);
|
|
assert.match(violations[0]!.rule, /^R9 /);
|
|
assert.match(violations[0]!.message, /grew to 7 files \(baseline 6 at the merge-base\)/);
|
|
assert.match(violations[1]!.rule, /^R10 /);
|
|
});
|
|
|
|
// The shrink direction used to need an edit in the same change, or the ceiling kept headroom the
|
|
// next change could spend. Measuring the merge-base banks it on merge, with nothing to lower.
|
|
test('R9 banks a shrink with no edit anywhere', () => {
|
|
assert.deepEqual(
|
|
checkDaemonModularityRatchets(
|
|
baselineEdges(),
|
|
measured({ largestTypeCycle: typeCycleMembers({ 'provider-webdriver': 5 }) }),
|
|
REFERENCE,
|
|
),
|
|
[],
|
|
);
|
|
});
|