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
4 changes: 2 additions & 2 deletions scripts/layering/daemon-modularity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const LARGEST_TYPE_CYCLE_ZONE_CEILINGS: Readonly<Record<string, number>> = {

export const DAEMON_MODULARITY_BASELINE = {
sessionState: {
writerOwnedFields: 29,
ownerFileClaims: 40,
writerOwnedFields: 22,
ownerFileClaims: 28,
},
largestTypeCycle: {
zoneMembers: LARGEST_TYPE_CYCLE_ZONE_CEILINGS,
Expand Down
37 changes: 12 additions & 25 deletions scripts/layering/session-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,35 +49,23 @@ export const SESSION_STATE_FIELD_OWNERS: Readonly<Record<string, readonly string
lastComparisonSafeSnapshot: ['src/daemon/session-snapshot.ts'],
androidSnapshotFreshness: ['src/daemon/android-snapshot-freshness.ts'],

// ADR 0016 active-session publication. `scriptRecordingState` tracks the armed -> published
// lifecycle; `recordSession` is the broader "record actions" flag and is deliberately set
// on its own by paths that record without arming a publication.
scriptRecordingState: [
'src/daemon/handlers/session-open.ts',
'src/daemon/handlers/session-script-publication.ts',
// #1478 P4a script publication. The tagged aggregate replaced the eight co-resident
// `saveScript*`/`scriptRecordingState`/`repair*` fields; its ONLY writers are the two
// daemon-private projections (`session-replay-transaction.ts`,
// `session-script-publication-capability.ts`) and the writer's commit transition.
// `recordSession` is the broader "record actions" flag and is deliberately set on its own by
// paths that record without arming a publication.
scriptPublication: [
'src/daemon/session-replay-transaction.ts',
'src/daemon/session-script-publication-capability.ts',
'src/daemon/session-script-writer.ts',
],
recordSession: [
'src/daemon/handlers/session-close-script.ts',
'src/daemon/handlers/session-open.ts',
'src/daemon/handlers/session-replay-runtime.ts',
'src/daemon/handlers/session-script-publication.ts',
'src/daemon/session-action-recorder.ts',
'src/daemon/session-replay-transaction.ts',
'src/daemon/session-script-publication-capability.ts',
],
saveScriptPath: [
'src/daemon/handlers/session-replay-runtime.ts',
'src/daemon/handlers/session-script-publication.ts',
'src/daemon/session-action-recorder.ts',
],
saveScriptForce: [
'src/daemon/handlers/session-replay-runtime.ts',
'src/daemon/handlers/session-script-publication.ts',
'src/daemon/session-action-recorder.ts',
],
saveScriptBoundary: ['src/daemon/handlers/session-replay-runtime.ts'],
saveScriptCommitted: ['src/daemon/session-script-writer.ts'],
repairSourcePath: ['src/daemon/handlers/session-replay-runtime.ts'],
pendingRecordAndHeal: ['src/daemon/handlers/session-replay-resume.ts'],
repairPlatformCloseReceipt: ['src/daemon/handlers/session-close.ts'],

trace: ['src/daemon/handlers/record-trace.ts'],
recording: ['src/daemon/handlers/record-trace-recording.ts'],
Expand All @@ -93,7 +81,6 @@ export const SESSION_STATE_FIELD_OWNERS: Readonly<Record<string, readonly string
appName: ['src/daemon/snapshot-runtime.ts'],
lease: ['src/daemon/handlers/session-open.ts'],
deviceClaim: ['src/daemon/handlers/session-open.ts'],
saveScriptComplete: ['src/daemon/handlers/session-replay-runtime.ts'],
};

/**
Expand Down
79 changes: 63 additions & 16 deletions src/__tests__/test-utils/session-factories.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
import type { SessionState } from '../../daemon/types.ts';
import type {
SessionScriptPublicationState,
SessionScriptRepairStatus,
} from '../../daemon/session-script-publication-state.ts';
import { IOS_SIMULATOR, ANDROID_EMULATOR, MACOS_DEVICE } from './device-fixtures.ts';

/** A repair-variant `scriptPublication` literal for test setups. */
export function repairPublication(
status: SessionScriptRepairStatus,
opts?: { boundary?: number; path?: string; force?: boolean; sourcePath?: string },
): SessionScriptPublicationState {
return {
kind: 'repair',
status,
target:
opts?.path !== undefined
? { kind: 'explicit', path: opts.path, force: opts?.force === true }
: { kind: 'default', force: opts?.force === true },
boundary: opts?.boundary ?? 0,
...(opts?.sourcePath !== undefined ? { sourcePath: opts.sourcePath } : {}),
};
}

/** An authoring-variant `scriptPublication` literal for test setups. */
export function authoringPublication(
status: 'armed' | 'aborted' | 'published',
opts?: { path?: string; force?: boolean },
): SessionScriptPublicationState {
return {
kind: 'authoring',
status,
target:
opts?.path !== undefined
? { kind: 'explicit', path: opts.path, force: opts?.force === true }
: { kind: 'default', force: opts?.force === true },
};
}

export function makeSession(name: string, overrides?: Partial<SessionState>): SessionState {
return {
name,
Expand All @@ -25,22 +61,18 @@ export function makeMacOsSession(name: string, overrides?: Partial<SessionState>

// --- Script-authoring session states ---
//
// The three factories below name the states a session-script session can be in,
// instead of leaving each test to re-derive them from a pile of `saveScript*`
// booleans. They exist because the fields are NOT independent: `recordSession`
// without a boundary is an ordinary recording, a boundary without
// `saveScriptComplete` is an ARMED-but-uncommittable repair, and only the
// COMPLETE combination publishes. Spelling that out per test made the
// distinction the tests are actually about (ordinary vs repair, armed vs
// complete) the hardest thing to see in them.
// The three factories below name the states a session-script session can be in.
// Since #1478 P4a those states are structural (`scriptPublication` is a tagged
// aggregate), but the factories keep naming the states production actually
// produces: ordinary recording, an ARMED-but-uncommittable repair, and the
// COMPLETE combination that publishes.
//
// A test that deliberately exercises an odd combination (a boundary with no
// recording, say) should still build it inline — these are for the states
// production actually produces.
// A test that deliberately exercises an odd combination (a repair variant with
// no recording, say) should still build it inline.

/**
* ADR 0016 ordinary authoring recording: `recordSession` armed, NO repair
* boundary. This is a plain `open --save-script` / `close --save-script`
* variant. This is a plain `open --save-script` / `close --save-script`
* session, and the baseline for every authoring-side handler test (target-v1
* evidence, parameterized fills, landmark waits) that only needs the session to
* be recording its actions.
Expand All @@ -57,8 +89,7 @@ export function makeAuthoringSession(

/**
* ADR 0012 decision 6: a session ARMED for repair by `replay --save-script` —
* recording plus the repair-run boundary watermark, which is what the writer's
* `repairArmed` actually keys off (`saveScriptBoundary !== undefined`).
* recording plus the repair variant with its boundary watermark at 0.
*
* ARMED, not COMPLETE: a writer handed this session ABORTS (publishes no
* prefix) rather than committing. Use `makeRepairCompleteSession` for a
Expand All @@ -68,7 +99,15 @@ export function makeRepairArmedSession(
name: string,
overrides?: Partial<SessionState>,
): SessionState {
return makeAuthoringSession(name, { saveScriptBoundary: 0, ...overrides });
return makeAuthoringSession(name, {
scriptPublication: {
kind: 'repair',
status: 'armed',
target: { kind: 'default', force: false },
boundary: 0,
},
...overrides,
});
}

/**
Expand All @@ -80,5 +119,13 @@ export function makeRepairCompleteSession(
name: string,
overrides?: Partial<SessionState>,
): SessionState {
return makeRepairArmedSession(name, { saveScriptComplete: true, ...overrides });
return makeAuthoringSession(name, {
scriptPublication: {
kind: 'repair',
status: 'complete',
target: { kind: 'default', force: false },
boundary: 0,
},
...overrides,
});
}
9 changes: 7 additions & 2 deletions src/daemon/__tests__/request-router-repair-expired.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,13 @@ function tombstonedSession(name: string): SessionState {
device: { platform: 'apple', id: 'sim-1', name: 'iPhone', kind: 'simulator', booted: true },
createdAt: Date.now(),
actions: [],
saveScriptBoundary: 0,
repairSourcePath: '/flows/login.ad',
scriptPublication: {
kind: 'repair',
status: 'armed',
target: { kind: 'default', force: false },
boundary: 0,
sourcePath: '/flows/login.ad',
},
};
}

Expand Down
6 changes: 5 additions & 1 deletion src/daemon/__tests__/request-router-typed-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,11 @@ test('#1391: an ordinary close-time script-save failure surfaces details.reason/
`agent-device-router-typed-error-${Date.now()}-${Math.random().toString(36).slice(2)}.ad`,
);
fs.writeFileSync(targetPath, 'pre-existing\n');
session.saveScriptPath = targetPath;
session.scriptPublication = {
kind: 'authoring',
status: 'armed',
target: { kind: 'explicit', path: targetPath, force: false },
};
sessionStore.set('typed-error', session);

try {
Expand Down
5 changes: 3 additions & 2 deletions src/daemon/__tests__/request-save-script-transports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* `.ad` artifact behind.
*/
import fs from 'node:fs';
import { NO_SCRIPT_PUBLICATION, scriptTargetPath } from '../session-script-publication-state.ts';
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
Expand Down Expand Up @@ -192,7 +193,7 @@ for (const [transport, send] of TRANSPORTS) {
expect(session.actions).toEqual([]);
// No arming: neither the recording marker nor the publication target moved.
expect(session.recordSession).toBe(undefined);
expect(session.saveScriptPath).toBe(undefined);
expect(session.scriptPublication).toBe(undefined);
// No artifact: the write a later close/teardown would attempt publishes nothing.
expect(sessionStore.writeSessionLog(session)).toEqual({ written: false });
expect(listAdArtifacts(root)).toEqual([]);
Expand Down Expand Up @@ -283,7 +284,7 @@ test('an owner-armed session still records its target and publishes its script',
result: { session: SESSION },
});
expect(session.recordSession).toBe(true);
expect(session.saveScriptPath).toBe(target);
expect(scriptTargetPath(session.scriptPublication ?? NO_SCRIPT_PUBLICATION)).toBe(target);

const result = sessionStore.writeSessionLog(session);
expect(result).toEqual({ written: true, path: target, actionCount: 1 });
Expand Down
29 changes: 25 additions & 4 deletions src/daemon/__tests__/selector-recording.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ function planStepReq(command: string, flags: DaemonRequest['flags'] = {}): Daemo

test('a repair-armed session excludes get/is/find by default but keeps recording wait', () => {
const store = makeStore();
const session = makeIosSession('default', { saveScriptBoundary: 0 });
const session = makeIosSession('default', {
scriptPublication: {
kind: 'repair',
status: 'armed',
target: { kind: 'default', force: false },
boundary: 0,
},
});
store.set('default', session);

recordIfSession(store, 'default', req('get'), {});
Expand All @@ -50,7 +57,14 @@ test('a repair-armed session excludes get/is/find by default but keeps recording
// with --record to keep them.
test('a repair-armed session still records get/is/find dispatched as replay plan steps (authored provenance)', () => {
const store = makeStore();
const session = makeIosSession('default', { saveScriptBoundary: 0 });
const session = makeIosSession('default', {
scriptPublication: {
kind: 'repair',
status: 'armed',
target: { kind: 'default', force: false },
boundary: 0,
},
});
store.set('default', session);

recordIfSession(store, 'default', planStepReq('get'), {});
Expand All @@ -62,7 +76,14 @@ test('a repair-armed session still records get/is/find dispatched as replay plan

test('--record forces get/is/find through even while repair-armed', () => {
const store = makeStore();
const session = makeIosSession('default', { saveScriptBoundary: 0 });
const session = makeIosSession('default', {
scriptPublication: {
kind: 'repair',
status: 'armed',
target: { kind: 'default', force: false },
boundary: 0,
},
});
store.set('default', session);

recordIfSession(store, 'default', req('get', { record: true }), {});
Expand All @@ -75,7 +96,7 @@ test('--record forces get/is/find through even while repair-armed', () => {
test('outside a repair-armed session, get/is/find/wait all record normally', () => {
const store = makeStore();
const session = makeIosSession('default');
expect(session.saveScriptBoundary).toBeUndefined();
expect(session.scriptPublication).toBeUndefined();
store.set('default', session);

recordIfSession(store, 'default', req('get'), {});
Expand Down
47 changes: 41 additions & 6 deletions src/daemon/__tests__/session-action-recorder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ import { recordActionEntry } from '../session-action-recorder.ts';
import { makeIosSession } from '../../__tests__/test-utils/session-factories.ts';

test('an observation-only action is excluded while repair-armed and no --record is given', () => {
const session = makeIosSession('default', { saveScriptBoundary: 0 });
const session = makeIosSession('default', {
scriptPublication: {
kind: 'repair',
status: 'armed',
target: { kind: 'default', force: false },
boundary: 0,
},
});
const action = recordActionEntry(session, {
command: 'get',
positionals: ['attrs', 'id="save"'],
Expand All @@ -23,7 +30,14 @@ test('an observation-only action is excluded while repair-armed and no --record
});

test('--record forces an observation-only action through while repair-armed', () => {
const session = makeIosSession('default', { saveScriptBoundary: 0 });
const session = makeIosSession('default', {
scriptPublication: {
kind: 'repair',
status: 'armed',
target: { kind: 'default', force: false },
boundary: 0,
},
});
const action = recordActionEntry(session, {
command: 'get',
positionals: ['attrs', 'id="save"'],
Expand All @@ -37,7 +51,7 @@ test('--record forces an observation-only action through while repair-armed', ()

test('an observation-only action records normally outside a repair-armed session (ordinary authoring recording is unchanged)', () => {
const session = makeIosSession('default');
expect(session.saveScriptBoundary).toBeUndefined();
expect(session.scriptPublication).toBeUndefined();
const action = recordActionEntry(session, {
command: 'is',
positionals: ['visible', 'id="save"'],
Expand All @@ -50,7 +64,14 @@ test('an observation-only action records normally outside a repair-armed session
});

test('a mutating action is never excluded, repair-armed or not', () => {
const session = makeIosSession('default', { saveScriptBoundary: 0 });
const session = makeIosSession('default', {
scriptPublication: {
kind: 'repair',
status: 'armed',
target: { kind: 'default', force: false },
boundary: 0,
},
});
const action = recordActionEntry(session, {
command: 'press',
positionals: ['@e5'],
Expand All @@ -62,7 +83,14 @@ test('a mutating action is never excluded, repair-armed or not', () => {
});

test('a command explicitly marked NOT observation-only (e.g. the top-level `wait`) always records, even while repair-armed', () => {
const session = makeIosSession('default', { saveScriptBoundary: 0 });
const session = makeIosSession('default', {
scriptPublication: {
kind: 'repair',
status: 'armed',
target: { kind: 'default', force: false },
boundary: 0,
},
});
const action = recordActionEntry(session, {
command: 'wait',
positionals: ['500'],
Expand All @@ -75,7 +103,14 @@ test('a command explicitly marked NOT observation-only (e.g. the top-level `wait
});

test('--no-record still takes precedence over an observation-only action, repair-armed or not', () => {
const session = makeIosSession('default', { saveScriptBoundary: 0 });
const session = makeIosSession('default', {
scriptPublication: {
kind: 'repair',
status: 'armed',
target: { kind: 'default', force: false },
boundary: 0,
},
});
const action = recordActionEntry(session, {
command: 'get',
positionals: ['attrs', 'id="save"'],
Expand Down
Loading
Loading