diff --git a/packages/daemon/src/__tests__/support/handlers/session-test-harness.ts b/packages/daemon/src/__tests__/support/handlers/session-test-harness.ts index 325beec697..a564848752 100644 --- a/packages/daemon/src/__tests__/support/handlers/session-test-harness.ts +++ b/packages/daemon/src/__tests__/support/handlers/session-test-harness.ts @@ -2,7 +2,11 @@ import type { DaemonResponse } from '@agent-device/kernel/contracts'; import { isMacOs } from '@agent-device/kernel/device'; import { legacyDispatchCapture } from '../legacy-snapshot-capture-fixture.ts'; import { expect, vi, beforeEach } from 'vitest'; -import { createTestAppLogLiveHandle, mkdtempForTestSync } from '@agent-device/test-kit'; +import { + createLiveTestHostEnvironment, + createTestAppLogLiveHandle, + mkdtempForTestSync, +} from '@agent-device/test-kit'; import type { AppLogLiveState } from '@agent-device/contracts/app-log-runtime'; import { localRuntimeOwner } from '@agent-device/contracts/platform-runtime'; import { createDurableResourceEnvelope } from '@agent-device/capture-kit'; @@ -178,7 +182,7 @@ beforeEach(() => { export function makeSessionStore(): SessionStore { const root = mkdtempForTestSync('agent-device-session-handler-'); - return new SessionStore(path.join(root, 'sessions')); + return new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); } export function makeSession(name: string, device: SessionState['device']): SessionState { diff --git a/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts b/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts index aeb46471c1..3b35d1b9b9 100644 --- a/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts +++ b/packages/daemon/src/__tests__/support/handlers/snapshot-handler.fixtures.ts @@ -1,5 +1,9 @@ import path from 'node:path'; -import { buildNodes, mkdtempForTestSync } from '@agent-device/test-kit'; +import { + buildNodes, + createLiveTestHostEnvironment, + mkdtempForTestSync, +} from '@agent-device/test-kit'; import type { ProviderDeviceRuntime } from '@agent-device/contracts/device'; import type { RawSnapshotNode, SnapshotNode } from '@agent-device/kernel/snapshot'; import { buildSnapshotSignatures } from '@agent-device/capture-kit/snapshot-freshness'; @@ -16,7 +20,7 @@ import { platformResourceCleanupFixture } from '../platform-resource-cleanup-fix export function makeSessionStore(): SessionStore { const root = mkdtempForTestSync('agent-device-snapshot-handler-'); - return new SessionStore(path.join(root, 'sessions')); + return new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); } export function makeSession( diff --git a/packages/daemon/src/__tests__/support/store-factory.ts b/packages/daemon/src/__tests__/support/store-factory.ts index c6f893d37b..cf2efaa757 100644 --- a/packages/daemon/src/__tests__/support/store-factory.ts +++ b/packages/daemon/src/__tests__/support/store-factory.ts @@ -1,11 +1,11 @@ import path from 'node:path'; import { SessionStore } from '../../core/session-store.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import type { SessionRef, SessionState } from '../../core/session-state.ts'; export function makeSessionStore(prefix = 'agent-device-test-'): SessionStore { const tempRoot = mkdtempForTestSync(prefix); - return new SessionStore(path.join(tempRoot, 'sessions')); + return new SessionStore(path.join(tempRoot, 'sessions'), createLiveTestHostEnvironment()); } export function makeStoredSessionRef(session: SessionState, address = session.name): SessionRef { diff --git a/packages/daemon/src/core/__tests__/selector-recording.test.ts b/packages/daemon/src/core/__tests__/selector-recording.test.ts index f82fbf976e..cecd1ffe6e 100644 --- a/packages/daemon/src/core/__tests__/selector-recording.test.ts +++ b/packages/daemon/src/core/__tests__/selector-recording.test.ts @@ -13,11 +13,11 @@ import { recordIfSession } from '../selector-recording.ts'; import { SessionStore } from '../session-store.ts'; import { makeIosSession } from '../../__tests__/support/session-factories.ts'; import type { DaemonRequest } from '../daemon-request.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; function makeStore(): SessionStore { const root = mkdtempForTestSync('agent-device-selector-recording-'); - return new SessionStore(path.join(root, 'sessions')); + return new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); } function req(command: string, flags: DaemonRequest['flags'] = {}): DaemonRequest { diff --git a/packages/daemon/src/core/__tests__/session-action-recorder.test.ts b/packages/daemon/src/core/__tests__/session-action-recorder.test.ts index 94d2c33467..1dca7015f8 100644 --- a/packages/daemon/src/core/__tests__/session-action-recorder.test.ts +++ b/packages/daemon/src/core/__tests__/session-action-recorder.test.ts @@ -13,6 +13,17 @@ import { type RecordActionEntry, } from '../session-action-recorder.ts'; import { authoringPublication, makeIosSession } from '../../__tests__/support/session-factories.ts'; +import { createLiveTestHostEnvironment } from '@agent-device/test-kit'; + +// #3753: `recordActionEntry` resolves relative --save-script paths against the runtime host's +// working directory. This wrapper keeps the pre-#3753 call shape: the live test host's cwd rides +// in as the third argument without reshaping every fixture below. +function recordAction( + session: Parameters[0], + entry: Parameters[1], +) { + return recordActionEntry(session, entry, createLiveTestHostEnvironment().cwd); +} test.each([ { command: 'wait', replayPlanStep: false, intent: undefined }, @@ -30,7 +41,7 @@ test.each([ positionals: [], internal: { replayPlanStep }, }; - const action = recordActionEntry(session, { + const action = recordAction(session, { command, positionals: [], flags: {}, @@ -51,7 +62,7 @@ test('an observation without --record is excluded while repair-armed', () => { boundary: 0, }, }); - const action = recordActionEntry(session, { + const action = recordAction(session, { command: 'get', positionals: ['attrs', 'id="save"'], flags: {}, @@ -71,7 +82,7 @@ test('--record forces an observation-only action through while repair-armed', () boundary: 0, }, }); - const action = recordActionEntry(session, { + const action = recordAction(session, { command: 'get', positionals: ['attrs', 'id="save"'], flags: { record: true }, @@ -87,7 +98,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.scriptPublication).toBeUndefined(); - const action = recordActionEntry(session, { + const action = recordAction(session, { command: 'is', positionals: ['visible', 'id="save"'], flags: {}, @@ -123,7 +134,8 @@ test('ordinary action history preserves authored commands, positionals, and repl { command: 'home', positionals: [], flags: {}, result: {} }, ] satisfies RecordActionEntry[]; - for (const entry of entries) recordActionEntry(session, entry); + for (const entry of entries) + recordActionEntry(session, entry, createLiveTestHostEnvironment().cwd); expect( session.actions.map(({ command, positionals, flags }) => ({ command, positionals, flags })), @@ -139,7 +151,7 @@ test('a mutating action is never excluded, repair-armed or not', () => { boundary: 0, }, }); - const action = recordActionEntry(session, { + const action = recordAction(session, { command: 'press', positionals: ['@e5'], flags: {}, @@ -158,7 +170,7 @@ test('a command explicitly marked NOT observation-only (e.g. the top-level `wait boundary: 0, }, }); - const action = recordActionEntry(session, { + const action = recordAction(session, { command: 'wait', positionals: ['500'], flags: {}, @@ -178,7 +190,7 @@ test('--no-record still takes precedence over an observation-only action, repair boundary: 0, }, }); - const action = recordActionEntry(session, { + const action = recordAction(session, { command: 'get', positionals: ['attrs', 'id="save"'], flags: { noRecord: true }, @@ -191,7 +203,7 @@ test('--no-record still takes precedence over an observation-only action, repair test('--no-record suppresses a mutating coordinate press from action history', () => { const session = makeIosSession('default'); - const action = recordActionEntry(session, { + const action = recordAction(session, { command: 'press', positionals: ['50', '60'], flags: { noRecord: true }, @@ -231,9 +243,9 @@ test('parameterized fills keep literals out of recording state with deterministi }, }; - recordActionEntry(session, passwordEntry); - recordActionEntry(session, passwordEntry); - recordActionEntry(session, { + recordActionEntry(session, passwordEntry, createLiveTestHostEnvironment().cwd); + recordActionEntry(session, passwordEntry, createLiveTestHostEnvironment().cwd); + recordAction(session, { command: 'fill', positionals: ['id="token"', token], flags: { recordAs: 'API_TOKEN' }, @@ -263,7 +275,7 @@ test('parameterized fills keep literals out of recording state with deterministi test('a one-character parameterized fill preserves structural selector provenance', () => { const session = makeIosSession('default'); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', 'a'], flags: { recordAs: 'LETTER' }, @@ -302,7 +314,7 @@ test.each(['', ' '])( 'parameterized fills preserve the placeholder when the resolved value is %j', (value) => { const session = makeIosSession('default'); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', value], flags: { recordAs: 'PASSWORD' }, @@ -319,7 +331,7 @@ test('an empty parameterized clear rewrites only its own text, never other empty // so it must redact nothing beyond the fill's own semantic text — an unrelated empty field or // empty evidence label is not an echo of the (empty) value. const session = makeIosSession('default'); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="search"', ''], flags: { recordAs: 'QUERY' }, @@ -359,7 +371,7 @@ test('an empty parameterized clear rewrites only its own text, never other empty test('whitespace-only fills collapse ambiguous recorder output and keys', () => { const session = makeIosSession('default'); const whitespace = ' '; - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', whitespace], flags: { recordAs: 'PASSWORD' }, @@ -393,7 +405,7 @@ test('#1533: the --save-script ingress does not re-arm an aborted authoring life // The second `open --save-script` records its own action, carrying the flag // that caused the abort into the recorder's shared ingress. - recordActionEntry(session, { + recordAction(session, { command: 'open', positionals: ['com.example.other'], flags: { saveScript: true }, @@ -408,7 +420,7 @@ test('#1533: an aborted lifecycle cannot be retargeted by a later --save-script= scriptPublication: authoringPublication('aborted', { path: '/tmp/original.ad' }), }); - recordActionEntry(session, { + recordAction(session, { command: 'open', positionals: ['com.example.other'], flags: { saveScript: '/tmp/hijacked.ad', force: true }, @@ -425,7 +437,7 @@ test('#1533: an ARMED authoring lifecycle still takes the ingress (retarget + fo scriptPublication: authoringPublication('armed'), }); - recordActionEntry(session, { + recordAction(session, { command: 'close', positionals: [], flags: { saveScript: '/tmp/out.ad', force: true }, @@ -448,13 +460,13 @@ test('#1398: a landmark-mode wait recorded after a parameterized fill drops iden const session = makeIosSession('default'); const literal = 'hunter2-secret'; - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', literal], flags: { recordAs: 'PASSWORD' }, result: { text: literal }, }); - recordActionEntry(session, { + recordAction(session, { command: 'wait', positionals: ['role="heading"'], flags: {}, @@ -485,13 +497,13 @@ test('#1398: a landmark-mode wait drops identity evidence when the echo is only const session = makeIosSession('default'); const literal = 'hunter2-secret'; - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', literal], flags: { recordAs: 'PASSWORD' }, result: { text: literal }, }); - recordActionEntry(session, { + recordAction(session, { command: 'wait', positionals: ['role="button" label="Continue"'], flags: {}, @@ -514,13 +526,13 @@ test('#1398: action-mode evidence (get/is) redacts an echoed label and downgrade const session = makeIosSession('default'); const literal = 'hunter2-secret'; - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', literal], flags: { recordAs: 'PASSWORD' }, result: { text: literal }, }); - recordActionEntry(session, { + recordAction(session, { command: 'is', positionals: ['visible', 'id="password"'], flags: {}, @@ -549,19 +561,19 @@ test('#1398: action-mode evidence (get/is) redacts an echoed label and downgrade test('#1398: action-mode evidence redacts EVERY registered literal echoed in the same label, not just the first match', () => { const session = makeIosSession('default'); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="username"', 'bob'], flags: { recordAs: 'USERNAME' }, result: { text: 'bob' }, }); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', 'hunter2-secret'], flags: { recordAs: 'PASSWORD' }, result: { text: 'hunter2-secret' }, }); - recordActionEntry(session, { + recordAction(session, { command: 'is', positionals: ['visible', 'id="session-banner"'], flags: {}, @@ -592,19 +604,19 @@ test('#1398: action-mode evidence redacts EVERY registered literal echoed in the test('#1398: two distinct --record-as names sharing the same literal value deterministically keep the first-registered name', () => { const session = makeIosSession('default'); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', 'hunter2'], flags: { recordAs: 'PASSWORD' }, result: { text: 'hunter2' }, }); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="confirm"', 'hunter2'], flags: { recordAs: 'CONFIRM_PASSWORD' }, result: { text: 'hunter2' }, }); - recordActionEntry(session, { + recordAction(session, { command: 'wait', positionals: ['id="confirmation-banner"'], flags: {}, @@ -628,19 +640,19 @@ test('#1398: two distinct --record-as names sharing the same literal value deter test('#1398: redacting one literal never corrupts a placeholder another literal already produced (result payload)', () => { const session = makeIosSession('default'); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="field-one"', 'somethinglong'], flags: { recordAs: 'ABC' }, result: { text: 'somethinglong' }, }); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="field-two"', 'ABC'], flags: { recordAs: 'OTHER' }, result: { text: 'ABC' }, }); - recordActionEntry(session, { + recordAction(session, { command: 'wait', positionals: ['id="confirmation-banner"'], flags: {}, @@ -656,19 +668,19 @@ test('#1398: redacting one literal never corrupts a placeholder another literal test('#1398: redacting one literal never corrupts a placeholder another literal already produced (action-mode target evidence)', () => { const session = makeIosSession('default'); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="field-one"', 'somethinglong'], flags: { recordAs: 'ABC' }, result: { text: 'somethinglong' }, }); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="field-two"', 'ABC'], flags: { recordAs: 'OTHER' }, result: { text: 'ABC' }, }); - recordActionEntry(session, { + recordAction(session, { command: 'is', positionals: ['visible', 'id="confirmation-banner"'], flags: {}, @@ -704,13 +716,13 @@ test('#1398: a literal whose own text is shaped like an existing placeholder is const session = makeIosSession('default'); const literal = '${OTHERVAR}'; - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="field"', literal], flags: { recordAs: 'TOKEN' }, result: { text: literal }, }); - recordActionEntry(session, { + recordAction(session, { command: 'wait', positionals: ['id="confirmation-banner"'], flags: {}, @@ -725,13 +737,13 @@ test('#1398: dual-endpoint (targetEvidences) evidence is protected independently const session = makeIosSession('default'); const literal = 'hunter2-secret'; - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', literal], flags: { recordAs: 'PASSWORD' }, result: { text: literal }, }); - recordActionEntry(session, { + recordAction(session, { command: 'drag', positionals: ['id="source"', 'id="dest"'], flags: {}, @@ -778,19 +790,19 @@ test('#1398: dual-endpoint (targetEvidences) evidence is protected independently test('#1398: multiple registered literals apply longest-first so a shorter value never partially consumes a longer one', () => { const session = makeIosSession('default'); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', 'bob123'], flags: { recordAs: 'PASSWORD' }, result: { text: 'bob123' }, }); - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="username"', 'bob'], flags: { recordAs: 'USERNAME' }, result: { text: 'bob' }, }); - recordActionEntry(session, { + recordAction(session, { command: 'wait', positionals: ['text="Welcome bob123!"'], flags: {}, @@ -804,7 +816,7 @@ test('#1398: a whitespace-only --record-as value keeps only fill-step-scoped pro const session = makeIosSession('default'); const whitespace = ' '; - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', whitespace], flags: { recordAs: 'PASSWORD' }, @@ -812,7 +824,7 @@ test('#1398: a whitespace-only --record-as value keeps only fill-step-scoped pro }); expect(session.recordedFillLiterals).toBeUndefined(); - recordActionEntry(session, { + recordAction(session, { command: 'wait', positionals: ['text="a b c"'], flags: {}, @@ -824,7 +836,7 @@ test('#1398: a whitespace-only --record-as value keeps only fill-step-scoped pro test('#1398: ordinary recordings with no --record-as fill are completely unaffected', () => { const session = makeIosSession('default'); - recordActionEntry(session, { + recordAction(session, { command: 'wait', positionals: ['role="heading" label="Home"'], flags: {}, @@ -854,7 +866,7 @@ test('#1398: ordinary recordings with no --record-as fill are completely unaffec test('a stdin fill without --record-as never enters recording state', () => { const session = makeIosSession('default'); const secret = 'stdin-secret-3260'; - const action = recordActionEntry(session, { + const action = recordAction(session, { command: 'fill', positionals: ['id="password"', secret], flags: { textStdin: true }, @@ -869,7 +881,7 @@ test('a stdin fill without --record-as never enters recording state', () => { test('a stdin fill with --record-as is recorded as its ${VAR} placeholder', () => { const session = makeIosSession('default'); const secret = 'stdin-secret-3260'; - recordActionEntry(session, { + recordAction(session, { command: 'fill', positionals: ['id="password"', secret], flags: { textStdin: true, recordAs: 'PASSWORD' }, diff --git a/packages/daemon/src/core/__tests__/session-script-writer.test.ts b/packages/daemon/src/core/__tests__/session-script-writer.test.ts index fe63725e46..96d100bd84 100644 --- a/packages/daemon/src/core/__tests__/session-script-writer.test.ts +++ b/packages/daemon/src/core/__tests__/session-script-writer.test.ts @@ -14,7 +14,9 @@ import { import { markRepairTransactionComplete } from '../session-replay-transaction.ts'; import { NO_SCRIPT_PUBLICATION, scriptTargetPath } from '../session-script-publication-state.ts'; import { parseReplayScriptDetailed } from '@agent-device/ad-script'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; + +const hostCwd = createLiveTestHostEnvironment().cwd; function action(overrides: Partial = {}): SessionAction { return { ts: Date.now(), command: 'click', positionals: [], flags: {}, ...overrides }; @@ -34,7 +36,7 @@ function writeAndParse( test('write() slices session.actions from saveScriptBoundary onward, excluding pre-watermark actions', () => { const root = mkdtempForTestSync('agent-device-script-writer-boundary-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); // Fix 2: a repair-armed write only publishes once explicitly finalized // (`close --save-script`) — COMPLETE here to isolate THIS test's own concern // (boundary slicing), covered separately below. @@ -55,7 +57,7 @@ test('write() slices session.actions from saveScriptBoundary onward, excluding p test('write() with no boundary set (ordinary open/close --save-script) serializes the full history, unchanged', () => { const root = mkdtempForTestSync('agent-device-script-writer-no-boundary-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeAuthoringSession('default', { actions: [ action({ command: 'open', positionals: ['Demo'] }), @@ -71,7 +73,7 @@ test('write() with no boundary set (ordinary open/close --save-script) serialize test('write() round-trips wait absent positionals without adding an annotation', () => { const root = mkdtempForTestSync('agent-device-script-writer-wait-absent-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeAuthoringSession('default', { actions: [action({ command: 'wait', positionals: ['absent', 'label="Removed"', '2500'] })], }); @@ -86,14 +88,18 @@ test('write() round-trips wait absent positionals without adding an annotation', // recorder's declared-key filter AND the writer's line, then parse back as a flag. test('write() carries a recorded scroll --until through to the script and back', () => { const root = mkdtempForTestSync('agent-device-script-writer-scroll-until-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeAuthoringSession('default'); - recordActionEntry(session, { - command: 'scroll', - positionals: ['down'], - flags: { until: 'label="Sign in"', settle: true }, - result: { direction: 'down' }, - }); + recordActionEntry( + session, + { + command: 'scroll', + positionals: ['down'], + flags: { until: 'label="Sign in"', settle: true }, + result: { direction: 'down' }, + }, + hostCwd, + ); const { parsed } = writeAndParse(writer, session); expect(parsed.actions[0]?.command).toBe('scroll'); @@ -105,14 +111,18 @@ test('write() carries a recorded scroll --until through to the script and back', test('write() carries a recorded wait --raw through to the script and back', () => { const root = mkdtempForTestSync('agent-device-script-writer-wait-raw-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeAuthoringSession('default'); - recordActionEntry(session, { - command: 'wait', - positionals: ['label="Sign in"', '2000'], - flags: { snapshotRaw: true }, - result: {}, - }); + recordActionEntry( + session, + { + command: 'wait', + positionals: ['label="Sign in"', '2000'], + flags: { snapshotRaw: true }, + result: {}, + }, + hostCwd, + ); const { parsed } = writeAndParse(writer, session); expect(parsed.actions[0]?.positionals).toEqual(['label="Sign in"', '2000']); @@ -123,7 +133,7 @@ test.each(['authored', 'explicit-record'] as const)( 'healed output preserves a snapshot with %s publication intent', (intent) => { const root = mkdtempForTestSync('agent-device-script-writer-retained-snapshot-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeRepairCompleteSession('default'); const req = { token: 't', @@ -139,12 +149,16 @@ test.each(['authored', 'explicit-record'] as const)( : { snapshotInteractiveOnly: true }, ...(intent === 'authored' ? { internal: { replayPlanStep: true } } : {}), }; - recordActionEntry(session, { - command: req.command, - positionals: req.positionals, - flags: req.flags, - interactiveObservation: isInteractiveObservation(req), - }); + recordActionEntry( + session, + { + command: req.command, + positionals: req.positionals, + flags: req.flags, + interactiveObservation: isInteractiveObservation(req), + }, + hostCwd, + ); const { parsed } = writeAndParse(writer, session); expect(parsed.actions.map((entry) => entry.command)).toEqual(['snapshot']); expect(parsed.actions[0]?.flags.snapshotInteractiveOnly).toBe(true); @@ -153,7 +167,7 @@ test.each(['authored', 'explicit-record'] as const)( test('a boundary-sliced script still strips diagnostic snapshot actions', () => { const root = mkdtempForTestSync('agent-device-script-writer-snapshot-strip-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeRepairCompleteSession('default', { scriptPublication: repairPublication('complete', { boundary: 1 }), actions: [ @@ -177,7 +191,7 @@ test('a boundary-sliced script still strips diagnostic snapshot actions', () => test('a recorded ref that resolved to a selectorChain writes a clean selector line, never the bare ref', () => { const root = mkdtempForTestSync('agent-device-script-writer-resolved-ref-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeRepairCompleteSession('default', { actions: [ action({ @@ -196,7 +210,7 @@ test('a recorded ref that resolved to a selectorChain writes a clean selector li test('a recorded ref that never resolved to a selectorChain throws instead of emitting a bare @ref', () => { const root = mkdtempForTestSync('agent-device-script-writer-bare-ref-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeRepairCompleteSession('default', { actions: [action({ command: 'press', positionals: ['@e7'] })], }); @@ -218,7 +232,7 @@ test('a recorded drag refuses either unresolved ref endpoint', () => { ['drag', 'id="drag-source"', '@e3'], ]) { const root = mkdtempForTestSync('agent-device-script-writer-drag-ref-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeRepairCompleteSession('default', { actions: [action({ command: 'gesture', positionals })], }); @@ -233,7 +247,7 @@ test('a recorded drag refuses either unresolved ref endpoint', () => { test('a recorded drag writes and parses one targets-v1 annotation for both endpoints', () => { const root = mkdtempForTestSync('agent-device-script-writer-drag-evidence-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const evidence = { role: 'view', ancestry: [], @@ -264,7 +278,7 @@ test('a recorded drag writes and parses one targets-v1 annotation for both endpo test('a bare-@ref fill action also fails loud, not just click-like commands', () => { const root = mkdtempForTestSync('agent-device-script-writer-bare-ref-fill-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeRepairCompleteSession('default', { actions: [action({ command: 'fill', positionals: ['@e9', 'hello'] })], }); @@ -276,7 +290,7 @@ test('a bare-@ref fill action also fails loud, not just click-like commands', () test('a bare @ref later in the same session (after a resolved earlier action) still fails loud, writing nothing', () => { const root = mkdtempForTestSync('agent-device-script-writer-partial-write-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeRepairCompleteSession('default', { actions: [ action({ command: 'open', positionals: ['Demo'] }), @@ -297,7 +311,7 @@ test('a bare @ref later in the same session (after a resolved earlier action) st test('an ordinary (non-repair-armed) recording keeps the existing bare-ref fallback, never throws', () => { const root = mkdtempForTestSync('agent-device-script-writer-ordinary-bare-ref-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); // Ordinary recording (no repair boundary): this session was armed by plain // `open`/`close --save-script`, never by `replay --save-script` — R4 does // not apply. @@ -320,7 +334,7 @@ test('an ordinary (non-repair-armed) recording keeps the existing bare-ref fallb // (a) publishing to an ABSENT target succeeds. test('write() publishes cleanly when the target does not exist yet', () => { const root = mkdtempForTestSync('agent-device-script-writer-absent-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const healedPath = path.join(root, 'flows', 'login.healed.ad'); const session = makeRepairCompleteSession('default', { @@ -341,7 +355,7 @@ test('write() publishes cleanly when the target does not exist yet', () => { // bytes unchanged. test('write() refuses to clobber an existing COMPLETE DEFAULT .healed.ad', () => { const root = mkdtempForTestSync('agent-device-script-writer-clobber-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const healedPath = path.join(root, 'flows', 'login.healed.ad'); fs.mkdirSync(path.dirname(healedPath), { recursive: true }); // A prior, unreviewed, COMPLETE healed script already sits at the default @@ -369,7 +383,7 @@ test('write() refuses to clobber an existing COMPLETE DEFAULT .healed.ad', () => test('write() now refuses to clobber a stale PARTIAL (non-sentinel) .healed.ad at the default path too (behavior change: no more auto-overwrite)', () => { const root = mkdtempForTestSync('agent-device-script-writer-clobber-partial-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const healedPath = path.join(root, 'flows', 'login.healed.ad'); fs.mkdirSync(path.dirname(healedPath), { recursive: true }); // A partial left over from a diverged-and-abandoned repair (pre-Fix-2 bug, @@ -396,7 +410,7 @@ test('write() now refuses to clobber a stale PARTIAL (non-sentinel) .healed.ad a test('write(session, { force: true }) overwrites an existing COMPLETE target atomically', () => { const root = mkdtempForTestSync('agent-device-script-writer-force-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const healedPath = path.join(root, 'flows', 'login.healed.ad'); fs.mkdirSync(path.dirname(healedPath), { recursive: true }); fs.writeFileSync( @@ -421,7 +435,7 @@ test('write(session, { force: true }) overwrites an existing COMPLETE target ato test('write(session, { force: true }) overwrites an existing target for ORDINARY (non-repair) recording too', () => { const root = mkdtempForTestSync('agent-device-script-writer-force-ordinary-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const outPath = path.join(root, 'flows', 'existing.ad'); fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, 'context platform=ios device="x"\nclick id="old"\n'); @@ -440,7 +454,7 @@ test('write(session, { force: true }) overwrites an existing target for ORDINARY test('write(session) without { force: true } still refuses, even when a prior write in the same test used force', () => { const root = mkdtempForTestSync('agent-device-script-writer-force-default-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const outPath = path.join(root, 'flows', 'existing.ad'); fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, 'context platform=ios device="x"\nclick id="old"\n'); @@ -463,7 +477,7 @@ test('write(session) without { force: true } still refuses, even when a prior wr // sees `EEXIST` and is refused. test('two writers racing on the SAME ABSENT target: exactly one linkSync wins, the other is refused (no lock involved)', () => { const root = mkdtempForTestSync('agent-device-script-writer-race-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const healedPath = path.join(root, 'flows', 'login.healed.ad'); fs.mkdirSync(path.dirname(healedPath), { recursive: true }); @@ -521,7 +535,7 @@ test('two writers racing on the SAME ABSENT target: exactly one linkSync wins, t // sitting there. test('write() refuses to clobber an existing COMPLETE artifact at an EXPLICIT --save-script= target too', () => { const root = mkdtempForTestSync('agent-device-script-writer-explicit-complete-clobber-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const explicitOut = path.join(root, 'flows', 'promoted.ad'); fs.mkdirSync(path.dirname(explicitOut), { recursive: true }); fs.writeFileSync( @@ -549,7 +563,7 @@ test('write() refuses to clobber an existing COMPLETE artifact at an EXPLICIT -- // (a non-sentinel/partial file was freely overwritable at an explicit path). test('write() now refuses an explicit --save-script= pointing at an existing (non-sentinel) file too', () => { const root = mkdtempForTestSync('agent-device-script-writer-explicit-out-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const outPath = path.join(root, 'flows', 'explicit.ad'); fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, 'context platform=ios device="x"\nclick id="old"\n'); @@ -579,7 +593,7 @@ test('write() now refuses an explicit --save-script= pointing at an existi // sessions were exercised above. test('an ordinary (non-repair) recording now refuses an existing target too (behavior change: no more rename-replace overwrite)', () => { const root = mkdtempForTestSync('agent-device-script-writer-ordinary-clobber-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const outPath = path.join(root, 'flows', 'existing.ad'); fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, 'context platform=ios device="x"\nclick id="old"\n'); @@ -607,7 +621,7 @@ test('an ordinary (non-repair) recording now refuses an existing target too (beh // the refusal is scoped to an EXISTING target, not a blanket block. test('an ordinary (non-repair) recording still publishes cleanly when its target does not exist yet', () => { const root = mkdtempForTestSync('agent-device-script-writer-ordinary-absent-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const outPath = path.join(root, 'flows', 'fresh.ad'); // Ordinary recording, not a repair. @@ -627,7 +641,7 @@ test('an ordinary (non-repair) recording still publishes cleanly when its target test('close --save-script= re-points a defaulted-healed repair, and a write to that (absent) explicit path succeeds', () => { const root = mkdtempForTestSync('agent-device-script-writer-close-explicit-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const defaultedHealed = path.join(root, 'flows', 'login.healed.ad'); const explicitOut = path.join(root, 'flows', 'promoted.ad'); @@ -641,11 +655,15 @@ test('close --save-script= re-points a defaulted-healed repair, a // used to wrongly refuse the explicit target). Refusal is now uniform // regardless of how the path was chosen, but a redirected session must // still publish cleanly to its own (absent) explicit target. - recordActionEntry(session, { - command: 'close', - positionals: [], - flags: { saveScript: explicitOut }, - }); + recordActionEntry( + session, + { + command: 'close', + positionals: [], + flags: { saveScript: explicitOut }, + }, + hostCwd, + ); expect(scriptTargetPath(session.scriptPublication ?? NO_SCRIPT_PUBLICATION)).toBe(explicitOut); // `recordActionEntry` is the low-level action recorder `close`'s handler // calls on its way to setting the finalize signal (Fix 2) — set here to @@ -666,7 +684,7 @@ test('close --save-script= re-points a defaulted-healed repair, a test('C2 abort-before-complete: a repair-armed but NOT-complete write discards — no file, no prefix', () => { const root = mkdtempForTestSync('agent-device-script-writer-incomplete-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); // ARMED but never COMPLETE: the plan never ran to its last executable step // (a `close`/`close --save-script` reached after a divergence, a daemon // teardown, or an idle-reap of an in-flight repair). @@ -683,7 +701,7 @@ test('C2 abort-before-complete: a repair-armed but NOT-complete write discards test('C2 commit-when-complete: a repair-armed COMPLETE write publishes and marks the session COMMITTED', () => { const root = mkdtempForTestSync('agent-device-script-writer-complete-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const outPath = path.join(root, 'flows', 'flow.healed.ad'); fs.mkdirSync(path.dirname(outPath), { recursive: true }); const session = makeRepairCompleteSession('default', { @@ -699,7 +717,7 @@ test('C2 commit-when-complete: a repair-armed COMPLETE write publishes and marks test('C2 idempotent post-commit: a second write on a COMMITTED session no-ops (no re-publish, no error)', () => { const root = mkdtempForTestSync('agent-device-script-writer-idempotent-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const outPath = path.join(root, 'flows', 'flow.healed.ad'); fs.mkdirSync(path.dirname(outPath), { recursive: true }); const session = makeRepairCompleteSession('default', { @@ -723,7 +741,7 @@ test('C2 idempotent post-commit: a second write on a COMMITTED session no-ops (n test('write() still emits an ordinary (non-repair) recording on close without --save-script, unaffected by the commit gate', () => { const root = mkdtempForTestSync('agent-device-script-writer-ordinary-unfinalized-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); // An ordinary `open --save-script` recording, not a repair — the Fix 2 gate // only applies to repair-armed sessions. const session = makeAuthoringSession('default', { @@ -736,7 +754,7 @@ test('write() still emits an ordinary (non-repair) recording on close without -- test('write() never appends the completeness sentinel to an ordinary (non-repair) recording', () => { const root = mkdtempForTestSync('agent-device-script-writer-no-sentinel-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeAuthoringSession('default', { actions: [action({ command: 'click', positionals: ['id="save"'] })], }); @@ -747,7 +765,7 @@ test('write() never appends the completeness sentinel to an ordinary (non-repair test('write() publishes atomically: no stray temp file survives a successful repair write', () => { const root = mkdtempForTestSync('agent-device-script-writer-atomic-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const outPath = path.join(root, 'flows', 'atomic.healed.ad'); fs.mkdirSync(path.dirname(outPath), { recursive: true }); const session = makeRepairCompleteSession('default', { @@ -773,7 +791,7 @@ test('write() publishes atomically: no stray temp file survives a successful rep // promote a trailing junk positional into a duration, which would silently change the hold time. test('a resolved longpress keeps its recorded duration when rewriting the selector', () => { const root = mkdtempForTestSync('agent-device-script-writer-longpress-duration-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeAuthoringSession('default', { actions: [ action({ @@ -792,7 +810,7 @@ test('a resolved longpress keeps its recorded duration when rewriting the select test('a resolved longpress without a duration positional writes the selector alone', () => { const root = mkdtempForTestSync('agent-device-script-writer-longpress-noduration-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeAuthoringSession('default', { actions: [ action({ @@ -810,7 +828,7 @@ test('a resolved longpress without a duration positional writes the selector alo test('a resolved longpress drops a blank or non-numeric trailing positional instead of writing it as a duration', () => { for (const junk of [' ', 'abc']) { const root = mkdtempForTestSync('agent-device-script-writer-longpress-junk-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeAuthoringSession('default', { actions: [ action({ @@ -829,7 +847,7 @@ test('a resolved longpress drops a blank or non-numeric trailing positional inst test('#1533: write() publishes nothing for an aborted authoring lifecycle that is still recording', () => { const root = mkdtempForTestSync('agent-device-script-writer-authoring-aborted-'); const sessionsDir = path.join(root, 'sessions'); - const writer = new SessionScriptWriter(sessionsDir); + const writer = new SessionScriptWriter(sessionsDir, hostCwd); // The production shape after `open --save-script` -> `open --save-script`: // the second open aborted the lifecycle AND cleared recording, then its own // recorded action's `--save-script` ingress set the boolean back to true. @@ -844,7 +862,7 @@ test('#1533: write() publishes nothing for an aborted authoring lifecycle that i test('#1533: the abort is terminal per-target too — an explicit --save-script path is not written either', () => { const root = mkdtempForTestSync('agent-device-script-writer-authoring-aborted-explicit-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const outPath = path.join(root, 'flows', 'aborted.ad'); fs.mkdirSync(path.dirname(outPath), { recursive: true }); const session = makeAuthoringSession('default', { @@ -858,7 +876,7 @@ test('#1533: the abort is terminal per-target too — an explicit --save-script test('#1533: an ARMED authoring recording is untouched by the abort gate', () => { const root = mkdtempForTestSync('agent-device-script-writer-authoring-armed-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeAuthoringSession('default', { scriptPublication: authoringPublication('armed'), actions: [action({ command: 'click', positionals: ['id="save"'] })], @@ -876,19 +894,23 @@ test.each([ 'ordinary authoring retains only authored snapshots with record=$record and replayPlanStep=$replayPlanStep', ({ record, replayPlanStep }) => { const root = mkdtempForTestSync('agent-device-script-writer-authoring-snapshot-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter(path.join(root, 'sessions'), hostCwd); const session = makeAuthoringSession('default', { actions: [action({ command: 'open', positionals: ['Demo'] })], }); - recordActionEntry(session, { - command: 'snapshot', - positionals: [], - flags: { - record, - snapshotInteractiveOnly: true, + recordActionEntry( + session, + { + command: 'snapshot', + positionals: [], + flags: { + record, + snapshotInteractiveOnly: true, + }, + interactiveObservation: !replayPlanStep, }, - interactiveObservation: !replayPlanStep, - }); + hostCwd, + ); const { parsed } = writeAndParse(writer, session); expect(parsed.actions.map((entry) => entry.command)).toEqual( diff --git a/packages/daemon/src/core/__tests__/session-store.test.ts b/packages/daemon/src/core/__tests__/session-store.test.ts index ce1a590222..894963aed3 100644 --- a/packages/daemon/src/core/__tests__/session-store.test.ts +++ b/packages/daemon/src/core/__tests__/session-store.test.ts @@ -11,7 +11,7 @@ import { parseReplayScriptDetailed } from '@agent-device/ad-script'; import type { TargetAnnotationV1 } from '@agent-device/contracts/replay'; import { makeMacOsSession, repairPublication } from '../../__tests__/support/session-factories.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; type RecordActionEntry = Parameters[1]; @@ -54,7 +54,10 @@ function makeFixture(prefix: string, sessionsDir?: string): SessionStoreFixture const root = mkdtempForTestSync(prefix); return { root, - store: new SessionStore(sessionsDir ? path.join(root, sessionsDir) : root), + store: new SessionStore( + sessionsDir ? path.join(root, sessionsDir) : root, + createLiveTestHostEnvironment(), + ), session: makeSession('default'), }; } @@ -97,6 +100,7 @@ function assertScriptMatches(script: string, patterns: RegExp[]): void { test('defaultTracePath sanitizes session name', () => { const store = new SessionStore( path.join(mkdtempForTestSync('agent-device-tests'), 'agent-device-tests'), + createLiveTestHostEnvironment(), ); const session = makeSession('session with spaces'); const tracePath = store.defaultTracePath(session); @@ -747,7 +751,7 @@ test('writeSessionLog never fabricates a target-v1 annotation for actions record test('writeRepairTombstone/readRepairTombstone round-trips owner + source path', () => { const root = mkdtempForTestSync('agent-device-tombstone-'); - const store = new SessionStore(path.join(root, 'sessions')); + const store = new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); const session = makeSession('default'); session.scriptPublication = repairPublication('armed', { boundary: 0, @@ -764,7 +768,7 @@ test('writeRepairTombstone/readRepairTombstone round-trips owner + source path', test('readRepairTombstone returns undefined once the tombstone has expired', () => { const root = mkdtempForTestSync('agent-device-tombstone-expiry-'); - const store = new SessionStore(path.join(root, 'sessions')); + const store = new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); const session = makeSession('default'); // TTL 0 => expiresAt <= now => already stale. store.writeRepairTombstone(storeSessionForTest(store, session), 0); @@ -773,7 +777,7 @@ test('readRepairTombstone returns undefined once the tombstone has expired', () test('clearRepairTombstone removes a tombstone (a fresh replay --save-script clears the key)', () => { const root = mkdtempForTestSync('agent-device-tombstone-clear-'); - const store = new SessionStore(path.join(root, 'sessions')); + const store = new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); const session = makeSession('default'); store.writeRepairTombstone(storeSessionForTest(store, session)); assert.ok(store.readRepairTombstone('default')); @@ -789,7 +793,7 @@ test('clearRepairTombstone removes a tombstone (a fresh replay --save-script cle test('BLOCKER 2: finalizeRepairTeardown of a COMPLETE transaction whose commit FAILS preserves the failure in a distinct tombstone, not a generic expiry', () => { const root = mkdtempForTestSync('agent-device-teardown-commit-fail-'); - const store = new SessionStore(path.join(root, 'sessions')); + const store = new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); const healedPath = path.join(root, 'flow.healed.ad'); // A prior COMPLETE (sentinel-marked) healed artifact already sits at the // default sibling path — teardown's auto-commit attempt must refuse to @@ -837,7 +841,7 @@ test('BLOCKER 2: finalizeRepairTeardown of a COMPLETE transaction whose commit F test('BLOCKER 3: finalizeRepairTeardown auto-commit records a terminal close, producing a self-contained, fresh-replayable healed .ad', () => { const root = mkdtempForTestSync('agent-device-teardown-autocommit-close-'); - const store = new SessionStore(path.join(root, 'sessions')); + const store = new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); const healedPath = path.join(root, 'flow.healed.ad'); const session = makeSession('default'); diff --git a/packages/daemon/src/core/session-action-recorder-admission.test.ts b/packages/daemon/src/core/session-action-recorder-admission.test.ts index 54c1549f76..a1056ec764 100644 --- a/packages/daemon/src/core/session-action-recorder-admission.test.ts +++ b/packages/daemon/src/core/session-action-recorder-admission.test.ts @@ -4,15 +4,20 @@ import { getFlagDefinitionsForKey } from '@agent-device/command-registry/flag-re import type { FlagDefinition } from '@agent-device/command-registry/flag-types'; import { makeIosSession } from '../__tests__/support/session-factories.ts'; import { recordActionEntry } from './session-action-recorder.ts'; +import { createLiveTestHostEnvironment } from '@agent-device/test-kit'; test('recorder sanitization follows the declaration: an undeclared key is dropped, a planted declaration is recorded', async () => { assert.equal(declarationFor('overlayRefs').recorded, false); - const shipped = recordActionEntry(makeIosSession('default'), { - command: 'screenshot', - positionals: [], - flags: { overlayRefs: true, fps: 5 }, - result: {}, - }); + const shipped = recordActionEntry( + makeIosSession('default'), + { + command: 'screenshot', + positionals: [], + flags: { overlayRefs: true, fps: 5 }, + result: {}, + }, + createLiveTestHostEnvironment().cwd, + ); assert.equal(shipped?.flags.overlayRefs, undefined, 'overlayRefs is undeclared for recording'); assert.equal(shipped?.flags.fps, 5, 'fps is declared for recording'); @@ -21,12 +26,16 @@ test('recorder sanitization follows the declaration: an undeclared key is droppe const planted = declarationForIn(registry, 'overlayRefs'); Object.assign(planted, { recorded: true }); const { recordActionEntry: recordPlanted } = await import('./session-action-recorder.ts'); - const recorded = recordPlanted(makeIosSession('default'), { - command: 'screenshot', - positionals: [], - flags: { overlayRefs: true }, - result: {}, - }); + const recorded = recordPlanted( + makeIosSession('default'), + { + command: 'screenshot', + positionals: [], + flags: { overlayRefs: true }, + result: {}, + }, + createLiveTestHostEnvironment().cwd, + ); assert.equal(recorded?.flags.overlayRefs, true, 'a planted declaration is copied'); }); diff --git a/packages/daemon/src/core/session-action-recorder.ts b/packages/daemon/src/core/session-action-recorder.ts index 7f832ad7c3..78b922e09a 100644 --- a/packages/daemon/src/core/session-action-recorder.ts +++ b/packages/daemon/src/core/session-action-recorder.ts @@ -58,12 +58,13 @@ export type RecordActionEntry = { export function recordActionEntry( session: SessionState, entry: RecordActionEntry, + hostCwd: string, ): SessionAction | undefined { if (entry.flags?.noRecord) return undefined; // Sensitive fill text stays out of session actions unless `--record-as` parameterizes it. if (isSensitiveFillText(entry.flags) && entry.flags.recordAs === undefined) return undefined; if (isExcludedRepairSegmentObservation(session, entry)) return undefined; - if (entry.flags) applyRecordedSaveScriptFlags(session, entry.flags); + if (entry.flags) applyRecordedSaveScriptFlags(session, entry.flags, hostCwd); const fillLiteral = readRecordedFillLiteral(entry); // #1398: register the literal only AFTER protecting this SAME entry, so a // fill's own recording is never passed through the session-wide substring diff --git a/packages/daemon/src/core/session-replay-transaction.ts b/packages/daemon/src/core/session-replay-transaction.ts index cec5e29c7a..223297b0e0 100644 --- a/packages/daemon/src/core/session-replay-transaction.ts +++ b/packages/daemon/src/core/session-replay-transaction.ts @@ -58,11 +58,12 @@ export function armRepairStep( healedSiblingPath: string; firstArm: boolean; }, + hostCwd: string, ): void { const state = publicationState(session); const explicitPath = typeof params.saveScript === 'string' - ? expandSessionPath(params.saveScript, process.cwd()) + ? expandSessionPath(params.saveScript, hostCwd) : state.kind === 'repair' ? undefined : params.healedSiblingPath; diff --git a/packages/daemon/src/core/session-script-publication-capability.ts b/packages/daemon/src/core/session-script-publication-capability.ts index 0781bfa9ef..c78012816c 100644 --- a/packages/daemon/src/core/session-script-publication-capability.ts +++ b/packages/daemon/src/core/session-script-publication-capability.ts @@ -41,10 +41,10 @@ export function isAuthoringArmedSession(session: SessionState | undefined): bool export function armAuthoringOnOpen( session: SessionState, requested: { path?: string; force?: boolean }, + hostCwd: string, ): void { session.scriptPublication = armAuthoring({ - path: - requested.path !== undefined ? expandSessionPath(requested.path, process.cwd()) : undefined, + path: requested.path !== undefined ? expandSessionPath(requested.path, hostCwd) : undefined, force: requested.force === true, }); } @@ -88,13 +88,17 @@ export function isSessionRecording(session: SessionState | undefined): boolean { * - `authoring{aborted}` -> nothing. The lifecycle is terminal (#1533), so the flag neither * re-arms recording nor retargets: recording-time evidence stopped at the abort. */ -export function applyRecordedSaveScriptFlags(session: SessionState, flags: CommandFlags): void { +export function applyRecordedSaveScriptFlags( + session: SessionState, + flags: CommandFlags, + hostCwd: string, +): void { if (!flags.saveScript) return; if (isAuthoringAborted(publicationState(session))) return; const requested = { path: typeof flags.saveScript === 'string' - ? expandSessionPath(flags.saveScript, process.cwd()) + ? expandSessionPath(flags.saveScript, hostCwd) : undefined, force: flags.force === true, }; @@ -124,6 +128,7 @@ export function applyRecordedSaveScriptFlags(session: SessionState, flags: Comma export function retargetActivePublication( session: SessionState, params: { explicitPath?: string; liveForce: boolean | undefined }, + hostCwd: string, ): void { const state = publicationState(session); if (state.kind !== 'authoring') return; @@ -132,7 +137,7 @@ export function retargetActivePublication( target: resolveScriptTarget(state.target, { path: params.explicitPath !== undefined - ? expandSessionPath(params.explicitPath, process.cwd()) + ? expandSessionPath(params.explicitPath, hostCwd) : undefined, force: params.liveForce === true, }), diff --git a/packages/daemon/src/core/session-script-writer.ts b/packages/daemon/src/core/session-script-writer.ts index 51fd5a253c..55aecf798d 100644 --- a/packages/daemon/src/core/session-script-writer.ts +++ b/packages/daemon/src/core/session-script-writer.ts @@ -112,9 +112,12 @@ function isPublicationWriteBlocked(session: SessionState): boolean { export class SessionScriptWriter { private readonly sessionsDir: string; + /** The runtime host's working directory, the base every recorded script target resolves against. */ + private readonly hostCwd: string; - constructor(sessionsDir: string) { + constructor(sessionsDir: string, hostCwd: string) { this.sessionsDir = sessionsDir; + this.hostCwd = hostCwd; } write(session: SessionState, options?: SessionScriptWriteOptions): SessionScriptWriteResult { @@ -160,7 +163,7 @@ export class SessionScriptWriter { private resolveScriptPath(session: SessionState): string { const targetPath = scriptTargetPath(session.scriptPublication ?? NO_SCRIPT_PUBLICATION); if (targetPath) { - return expandSessionPath(targetPath, process.cwd()); + return expandSessionPath(targetPath, this.hostCwd); } const safeName = safeSessionName(session.name); const timestamp = new Date(session.createdAt).toISOString().replaceAll(/[:.]/g, '-'); diff --git a/packages/daemon/src/core/session-store.ts b/packages/daemon/src/core/session-store.ts index ccd50d1a37..3fb85d71dd 100644 --- a/packages/daemon/src/core/session-store.ts +++ b/packages/daemon/src/core/session-store.ts @@ -4,6 +4,7 @@ import { resolveSessionAppLogPidPath, } from '@agent-device/daemon-contracts/session-artifact-paths'; import { DAEMON_SHUTTING_DOWN_REASON } from '@agent-device/daemon-contracts/daemon-resolution'; +import type { HostEnvironment } from '@agent-device/kernel/source-value'; import path from 'node:path'; import fs from 'node:fs'; import { AppError } from '@agent-device/kernel/errors'; @@ -55,9 +56,17 @@ export class SessionStore { private readonly sessionsDir: string; private readonly scriptWriter: SessionScriptWriter; - constructor(sessionsDir: string) { + /** + * The runtime host this store's sessions live on. Script targets are recorded relative to its + * working directory, and the event-log cap is read from its environment; neither is ever taken + * from the process this module happens to run inside (#3753). + */ + readonly host: HostEnvironment; + + constructor(sessionsDir: string, host: HostEnvironment) { this.sessionsDir = sessionsDir; - this.scriptWriter = new SessionScriptWriter(sessionsDir); + this.host = host; + this.scriptWriter = new SessionScriptWriter(sessionsDir, host.cwd); } /** @@ -187,14 +196,14 @@ export class SessionStore { } recordAction(ref: SessionRef, entry: RecordActionEntry): void { - const action = recordActionEntry(this.requireCurrent(ref), entry); + const action = recordActionEntry(this.requireCurrent(ref), entry, this.host.cwd); if (action) { - appendActionEvent(this.resolveEventLogPath(ref.address), ref.address, action); + appendActionEvent(this.resolveEventLogPath(ref.address), ref.address, action, this.host.env); } } recordEvent(sessionName: string, event: SessionEventLogInput): void { - appendSessionEvent(this.resolveEventLogPath(sessionName), sessionName, event); + appendSessionEvent(this.resolveEventLogPath(sessionName), sessionName, event, this.host.env); } readEvents( diff --git a/packages/daemon/src/execution/__tests__/android-owner-seam.test.ts b/packages/daemon/src/execution/__tests__/android-owner-seam.test.ts index 4c53806429..2432274328 100644 --- a/packages/daemon/src/execution/__tests__/android-owner-seam.test.ts +++ b/packages/daemon/src/execution/__tests__/android-owner-seam.test.ts @@ -9,6 +9,7 @@ import { import { detectAndroidEscapeSurface } from '../android-foreground-surface.ts'; import { resolveDirectTouchReferenceFrameSafely } from '../interaction/internal/interaction-touch-reference-frame.ts'; import { SessionStore } from '../../core/session-store.ts'; +import { createLiveTestHostEnvironment } from '@agent-device/test-kit'; test('provider-owned Android sessions bypass local observation and recovery', async () => { const session = makeAndroidSession('provider-owned-android', { @@ -62,7 +63,10 @@ test('provider-owned Android sessions bypass local observation and recovery', as }, ], }; - const sessionStore = new SessionStore('/tmp/provider-owned-android'); + const sessionStore = new SessionStore( + '/tmp/provider-owned-android', + createLiveTestHostEnvironment(), + ); const ref = sessionStore.publish(session.name, session); await expect( resolveDirectTouchReferenceFrameSafely({ diff --git a/packages/daemon/src/execution/__tests__/fold-runtime.test.ts b/packages/daemon/src/execution/__tests__/fold-runtime.test.ts index eae9f118ea..78c4799d67 100644 --- a/packages/daemon/src/execution/__tests__/fold-runtime.test.ts +++ b/packages/daemon/src/execution/__tests__/fold-runtime.test.ts @@ -26,6 +26,7 @@ import type { import type { GenericPlatformExecutionParams } from '../../core/generic-leaf-execution.ts'; import { readRequestedFoldPose, resolveBoundFoldRuntime } from '../fold-runtime.ts'; import { expectRefusesUnavailableExactOwnerFact } from '../../__tests__/support/runtime-binding-conformance.ts'; +import { createLiveTestHostEnvironment } from '@agent-device/test-kit'; // File-scoped id: this owner binding's `local-family` kind reaches the real on-disk device-claim // admission, so a shared id risks a cross-file claim collision under parallel execution. @@ -232,11 +233,15 @@ test('recording preserves the timeline for replay', () => { { atMs: 0, angle: 0 }, { atMs: 5000, angle: 180 }, ]); - const action = recordActionEntry(makeSession('fold-recording'), { - command: 'fold', - positionals: [], - flags: { keyframes }, - result: { pose: 'open', hingeAngleDegrees: 180 }, - }); + const action = recordActionEntry( + makeSession('fold-recording'), + { + command: 'fold', + positionals: [], + flags: { keyframes }, + result: { pose: 'open', hingeAngleDegrees: 180 }, + }, + createLiveTestHostEnvironment().cwd, + ); expect(action?.flags?.keyframes).toBe(keyframes); }); diff --git a/packages/daemon/src/execution/__tests__/internal-observation.test.ts b/packages/daemon/src/execution/__tests__/internal-observation.test.ts index d1033bc854..5d33725d83 100644 --- a/packages/daemon/src/execution/__tests__/internal-observation.test.ts +++ b/packages/daemon/src/execution/__tests__/internal-observation.test.ts @@ -12,7 +12,7 @@ import { } from '../../core/ref-frame.ts'; import { markSessionPartialRefsIssued, setSessionSnapshot } from '../session-snapshot.ts'; import { SessionStore } from '../../core/session-store.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; function snapshot(ref: string, label = ref): SnapshotState { return { @@ -36,7 +36,10 @@ function scenario() { mkdtempForTestSync('agent-device-internal-observation'), `agent-device-internal-observation-${crypto.randomUUID()}`, ); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const session = makeIosSession(sessionName, { appBundleId: 'com.example.app' }); const prior = snapshot('e1', 'Previously published'); diff --git a/packages/daemon/src/execution/app-event-runtime.ts b/packages/daemon/src/execution/app-event-runtime.ts index e4d270e41b..5a2261c83f 100644 --- a/packages/daemon/src/execution/app-event-runtime.ts +++ b/packages/daemon/src/execution/app-event-runtime.ts @@ -2,6 +2,7 @@ import type { AppEventInput } from '@agent-device/contracts/app-event-runtime'; import { appEventRuntimeUse } from '@agent-device/contracts/platform-runtime-operations'; import type { BoundDeviceRuntime } from '@agent-device/contracts/platform-runtime'; import type { DeviceInfo } from '@agent-device/kernel/device'; +import type { HostEnvironment } from '@agent-device/kernel/source-value'; import { parseTriggerAppEventArgs, resolveAppEventUrl } from './app-events.ts'; import { successText } from '@agent-device/kernel/success-text'; import type { DaemonCommandContext } from '../core/context.ts'; @@ -32,9 +33,10 @@ async function executeAppEvent( device: DeviceInfo, context: DaemonCommandContext, positionals: readonly string[], + env: NodeJS.ProcessEnv, ): Promise> { const { eventName, payload } = parseTriggerAppEventArgs([...positionals]); - const eventUrl = resolveAppEventUrl(device, eventName, payload); + const eventUrl = resolveAppEventUrl(device, eventName, payload, env); const input: AppEventInput = { eventUrl, ...(context.appBundleId === undefined ? {} : { options: { appBundleId: context.appBundleId } }), @@ -56,13 +58,15 @@ async function executeAppEvent( */ export async function resolveBoundAppEventRuntime( params: Readonly<{ + host: HostEnvironment; device: DeviceInfo; positionals: readonly string[]; readiness?: boolean; }> & RuntimeAdmissionBindings, ): Promise { - const { device, positionals, inspectFacts, bindDevice, platformServices, readiness } = params; + const { host, device, positionals, inspectFacts, bindDevice, platformServices, readiness } = + params; const admission = await admitRuntimeUse({ command: 'trigger-app-event', device, @@ -76,6 +80,6 @@ export async function resolveBoundAppEventRuntime( const runtime = admission.runtime; return { ok: true, - execute: (context) => executeAppEvent(runtime, device, context, positionals), + execute: (context) => executeAppEvent(runtime, device, context, positionals, host.env), }; } diff --git a/packages/daemon/src/execution/app-events.test.ts b/packages/daemon/src/execution/app-events.test.ts index 24b07e2465..2123b4570b 100644 --- a/packages/daemon/src/execution/app-events.test.ts +++ b/packages/daemon/src/execution/app-events.test.ts @@ -4,23 +4,9 @@ import { ANDROID_EMULATOR, IOS_DEVICE, MACOS_DEVICE } from '@agent-device/test-k import { parseTriggerAppEventArgs, resolveAppEventUrl } from './app-events.ts'; import { AppError } from '@agent-device/kernel/errors'; -type EnvironmentPatch = Readonly>; - -async function withEnvironment(patch: EnvironmentPatch, run: () => Promise | T): Promise { - const previous = new Map(Object.keys(patch).map((key) => [key, process.env[key]])); - for (const [key, value] of Object.entries(patch)) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; - } - try { - return await run(); - } finally { - for (const [key, value] of previous) { - if (value === undefined) delete process.env[key]; - else process.env[key] = value; - } - } -} +// resolveAppEventUrl takes the runtime host's env, so the tests name the env they mean rather +// than mutating the process environment. +const NO_TEMPLATE: NodeJS.ProcessEnv = {}; test('parseTriggerAppEventArgs validates event name format', () => { assert.throws( @@ -32,21 +18,13 @@ test('parseTriggerAppEventArgs validates event name format', () => { ); }); -test('trigger-app-event reports missing URL template as UNSUPPORTED_OPERATION', async () => { - await withEnvironment( - { - AGENT_DEVICE_APP_EVENT_URL_TEMPLATE: undefined, - AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE: undefined, - }, - () => { - assert.throws( - () => resolveAppEventUrl(ANDROID_EMULATOR, 'screenshot_taken'), - (error) => - error instanceof AppError && - error.code === 'UNSUPPORTED_OPERATION' && - /No app event URL template configured/i.test(error.message), - ); - }, +test('trigger-app-event reports missing URL template as UNSUPPORTED_OPERATION', () => { + assert.throws( + () => resolveAppEventUrl(ANDROID_EMULATOR, 'screenshot_taken', undefined, NO_TEMPLATE), + (error) => + error instanceof AppError && + error.code === 'UNSUPPORTED_OPERATION' && + /No app event URL template configured/i.test(error.message), ); }); @@ -60,59 +38,41 @@ test('trigger-app-event validates payload JSON', () => { ); }); -test('trigger-app-event prefers a platform-specific URL template over the global template', async () => { - await withEnvironment( - { +test('trigger-app-event prefers a platform-specific URL template over the global template', () => { + const { eventName, payload } = parseTriggerAppEventArgs(['screenshot_taken']); + assert.equal( + resolveAppEventUrl(ANDROID_EMULATOR, eventName, payload, { AGENT_DEVICE_APP_EVENT_URL_TEMPLATE: 'myapp://global?name={event}', AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE: 'myapp://android?name={event}', - }, - () => { - const { eventName, payload } = parseTriggerAppEventArgs(['screenshot_taken']); - assert.equal( - resolveAppEventUrl(ANDROID_EMULATOR, eventName, payload), - 'myapp://android?name=screenshot_taken', - ); - }, + }), + 'myapp://android?name=screenshot_taken', ); }); -test('trigger-app-event supports iOS and prefers its platform-specific URL template', async () => { - await withEnvironment( - { +test('trigger-app-event supports iOS and prefers its platform-specific URL template', () => { + const { eventName, payload } = parseTriggerAppEventArgs(['screenshot_taken', '{"source":"ios"}']); + assert.equal( + resolveAppEventUrl(IOS_DEVICE, eventName, payload, { AGENT_DEVICE_APP_EVENT_URL_TEMPLATE: 'myapp://global?name={event}', AGENT_DEVICE_IOS_APP_EVENT_URL_TEMPLATE: 'myapp://ios?name={event}&payload={payload}&platform={platform}', - }, - () => { - const { eventName, payload } = parseTriggerAppEventArgs([ - 'screenshot_taken', - '{"source":"ios"}', - ]); - assert.equal( - resolveAppEventUrl(IOS_DEVICE, eventName, payload), - 'myapp://ios?name=screenshot_taken&payload=%7B%22source%22%3A%22ios%22%7D&platform=ios', - ); - }, + }), + 'myapp://ios?name=screenshot_taken&payload=%7B%22source%22%3A%22ios%22%7D&platform=ios', ); }); -test('trigger-app-event supports macOS and prefers macOS template', async () => { - await withEnvironment( - { +test('trigger-app-event supports macOS and prefers macOS template', () => { + const { eventName, payload } = parseTriggerAppEventArgs([ + 'screenshot_taken', + '{"source":"desktop"}', + ]); + assert.equal( + resolveAppEventUrl(MACOS_DEVICE, eventName, payload, { AGENT_DEVICE_APP_EVENT_URL_TEMPLATE: 'myapp://global?name={event}', AGENT_DEVICE_MACOS_APP_EVENT_URL_TEMPLATE: 'myapp://macos?name={event}&payload={payload}&platform={platform}', - }, - () => { - const { eventName, payload } = parseTriggerAppEventArgs([ - 'screenshot_taken', - '{"source":"desktop"}', - ]); - assert.equal( - resolveAppEventUrl(MACOS_DEVICE, eventName, payload), - 'myapp://macos?name=screenshot_taken&payload=%7B%22source%22%3A%22desktop%22%7D&platform=macos', - ); - }, + }), + 'myapp://macos?name=screenshot_taken&payload=%7B%22source%22%3A%22desktop%22%7D&platform=macos', ); }); @@ -127,17 +87,15 @@ test('trigger-app-event rejects payloads that exceed size limits', () => { ); }); -test('trigger-app-event rejects event URLs that exceed length limits', async () => { - await withEnvironment( - { AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE: `myapp://${'a'.repeat(5000)}?name={event}` }, - () => { - assert.throws( - () => resolveAppEventUrl(ANDROID_EMULATOR, 'screenshot_taken'), - (error) => - error instanceof AppError && - error.code === 'INVALID_ARGS' && - /URL exceeds maximum supported length/i.test(error.message), - ); - }, +test('trigger-app-event rejects event URLs that exceed length limits', () => { + assert.throws( + () => + resolveAppEventUrl(ANDROID_EMULATOR, 'screenshot_taken', undefined, { + AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE: `myapp://${'a'.repeat(5000)}?name={event}`, + }), + (error) => + error instanceof AppError && + error.code === 'INVALID_ARGS' && + /URL exceeds maximum supported length/i.test(error.message), ); }); diff --git a/packages/daemon/src/execution/app-events.ts b/packages/daemon/src/execution/app-events.ts index 2844975bd9..fff5d26b10 100644 --- a/packages/daemon/src/execution/app-events.ts +++ b/packages/daemon/src/execution/app-events.ts @@ -42,10 +42,11 @@ export function parseTriggerAppEventArgs(positionals: string[]): { export function resolveAppEventUrl( device: AppEventDevice, eventName: string, - payload?: AppEventPayload, + payload: AppEventPayload | undefined, + env: NodeJS.ProcessEnv, ): string { const platform = publicPlatformString(device); - const template = readAppEventUrlTemplate(device); + const template = readAppEventUrlTemplate(device, env); if (!template) { throw new AppError( 'UNSUPPORTED_OPERATION', @@ -98,13 +99,16 @@ function parseTriggerEventPayload( } } -function readAppEventUrlTemplate(device: AppEventDevice): string | undefined { +function readAppEventUrlTemplate( + device: AppEventDevice, + env: NodeJS.ProcessEnv, +): string | undefined { const platformSpecific = isIosFamily(device) - ? process.env.AGENT_DEVICE_IOS_APP_EVENT_URL_TEMPLATE + ? env.AGENT_DEVICE_IOS_APP_EVENT_URL_TEMPLATE : isMacOs(device) - ? process.env.AGENT_DEVICE_MACOS_APP_EVENT_URL_TEMPLATE - : process.env.AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE; - const candidate = platformSpecific ?? process.env.AGENT_DEVICE_APP_EVENT_URL_TEMPLATE; + ? env.AGENT_DEVICE_MACOS_APP_EVENT_URL_TEMPLATE + : env.AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE; + const candidate = platformSpecific ?? env.AGENT_DEVICE_APP_EVENT_URL_TEMPLATE; const trimmed = candidate?.trim(); return trimmed ? trimmed : undefined; } diff --git a/packages/daemon/src/execution/generic-runtime-execution.ts b/packages/daemon/src/execution/generic-runtime-execution.ts index 0ff52d6d71..28ca400a6d 100644 --- a/packages/daemon/src/execution/generic-runtime-execution.ts +++ b/packages/daemon/src/execution/generic-runtime-execution.ts @@ -31,6 +31,8 @@ export async function resolveGenericRuntimeExecution( session: SessionState; context: DaemonCommandContext; createCommandSurface: CreateDaemonCommandSurface; + /** The runtime host's working directory: the fallback base for a client-typed output path. */ + hostCwd: string; }> & ScreenshotRuntimeBindings, ): Promise { diff --git a/packages/daemon/src/execution/handlers/__tests__/react-native.test.ts b/packages/daemon/src/execution/handlers/__tests__/react-native.test.ts index b3ee6ad5c3..429037400f 100644 --- a/packages/daemon/src/execution/handlers/__tests__/react-native.test.ts +++ b/packages/daemon/src/execution/handlers/__tests__/react-native.test.ts @@ -4,7 +4,7 @@ import { handleReactNativeCommands } from '../react-native.ts'; import { captureSnapshot } from '../../../core/snapshot-capture.ts'; import { SessionStore } from '../../../core/session-store.ts'; import type { SessionState } from '../../../core/session-state.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { getRuntimeBindings, mockTapPoint, @@ -861,7 +861,7 @@ test('react-native dismiss-overlay ignores app copy that only mentions RN overla function makeSessionStore(): SessionStore { const root = mkdtempForTestSync('agent-device-rn-handler-'); - return new SessionStore(path.join(root, 'sessions')); + return new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); } function makeSession(name: string, platform: 'ios' | 'android' = 'ios'): SessionState { diff --git a/packages/daemon/src/execution/handlers/__tests__/snapshot-alert.test.ts b/packages/daemon/src/execution/handlers/__tests__/snapshot-alert.test.ts index 8cef94a0fb..e7453d10a5 100644 --- a/packages/daemon/src/execution/handlers/__tests__/snapshot-alert.test.ts +++ b/packages/daemon/src/execution/handlers/__tests__/snapshot-alert.test.ts @@ -6,7 +6,7 @@ import { handleSnapshotCommands as handleProductionSnapshotCommands } from '../. import { snapshotRuntimeFixture } from '../../../__tests__/support/snapshot-runtime-fixture.ts'; import { withAppleRunnerProvider } from '@agent-device/platform-apple/runner'; import { runAppleRunnerCommand } from '@agent-device/platform-apple/runner/operations'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import path from 'node:path'; import { fakeCreateCommandSurface } from '../../../__tests__/support/command-surface-fake.ts'; @@ -27,7 +27,7 @@ const mockRunnerCommand = vi.mocked(runAppleRunnerCommand); function makeSessionStore(): SessionStore { const root = mkdtempForTestSync('agent-device-snapshot-alert-'); - return new SessionStore(path.join(root, 'sessions')); + return new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); } function makeSession(name: string): SessionState { diff --git a/packages/daemon/src/execution/handlers/session-selector-dispatch.ts b/packages/daemon/src/execution/handlers/session-selector-dispatch.ts index 963dbaa97d..ec9872c927 100644 --- a/packages/daemon/src/execution/handlers/session-selector-dispatch.ts +++ b/packages/daemon/src/execution/handlers/session-selector-dispatch.ts @@ -1,4 +1,5 @@ import type { DaemonResponse } from '@agent-device/kernel/contracts'; +import type { HostEnvironment } from '@agent-device/kernel/source-value'; import type { DaemonPlatformServices } from '../../core/platform-services.ts'; import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; import type { DeviceInfo } from '@agent-device/kernel/device'; @@ -123,6 +124,7 @@ function requireForegroundIosKeyboardSession( /** The params every migrated session-route handler takes; identical across the leaves. */ type SessionRouteHandlerParams = Readonly<{ + host: HostEnvironment; req: DaemonRequest; sessionName: string; logPath: string; @@ -134,6 +136,7 @@ type SessionRouteHandlerParams = Readonly<{ type SessionRouteRuntimeResolver = ( params: Readonly<{ + host: HostEnvironment; device: DeviceInfo; positionals: string[]; readiness?: boolean; @@ -182,6 +185,7 @@ async function runBoundSessionRoute( ...(params.updateSession ? { updateSession: params.updateSession } : {}), prepare: async (device, ref) => { const bound = await params.resolveRuntime({ + host: params.host, device, positionals, readiness: true, @@ -215,7 +219,6 @@ export async function handleKeyboardCommand( resolveRuntime: (runtimeParams) => resolveBoundKeyboardRuntime(runtimeParams), }); } - export async function handleAppEventCommand( params: SessionRouteHandlerParams, ): Promise { diff --git a/packages/daemon/src/execution/interaction/internal/interaction-ios-tap-outcome.test.ts b/packages/daemon/src/execution/interaction/internal/interaction-ios-tap-outcome.test.ts index e670207bef..cc0ba7aebd 100644 --- a/packages/daemon/src/execution/interaction/internal/interaction-ios-tap-outcome.test.ts +++ b/packages/daemon/src/execution/interaction/internal/interaction-ios-tap-outcome.test.ts @@ -30,7 +30,11 @@ import { appCaptureComparisonKey, systemSurfaceCaptureComparisonKey, } from '../../../core/__tests__/ios-comparison-key-fixture.ts'; -import { IOS_SIMULATOR, mkdtempForTestSync } from '@agent-device/test-kit'; +import { + IOS_SIMULATOR, + createLiveTestHostEnvironment, + mkdtempForTestSync, +} from '@agent-device/test-kit'; import { getRuntimeBindings, mockTapPoint, @@ -798,9 +802,10 @@ test('corroborated runtime taps retain target evidence through save and replay', ]); expect(recordedAction?.targetEvidence).toBeDefined(); - const written = new SessionScriptWriter(path.join(root, 'sessions')).write( - sessionStore.get(sessionName)!, - ); + const written = new SessionScriptWriter( + path.join(root, 'sessions'), + createLiveTestHostEnvironment().cwd, + ).write(sessionStore.get(sessionName)!); expect(written.written).toBe(true); if (!written.written) return; const savedScript = fs.readFileSync(written.path, 'utf8'); diff --git a/packages/daemon/src/execution/interaction/internal/interaction-target-evidence.test.ts b/packages/daemon/src/execution/interaction/internal/interaction-target-evidence.test.ts index 1d3048cfdc..cc5c06a315 100644 --- a/packages/daemon/src/execution/interaction/internal/interaction-target-evidence.test.ts +++ b/packages/daemon/src/execution/interaction/internal/interaction-target-evidence.test.ts @@ -9,7 +9,7 @@ import { makeSessionStore } from '../../../__tests__/support/store-factory.ts'; import { makeAuthoringSession } from '../../../__tests__/support/session-factories.ts'; import { SessionScriptWriter } from '../../../core/session-script-writer.ts'; import type { SessionState } from '../../../core/session-state.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { installLegacyCaptureInteractorResolution } from '../../../__tests__/support/legacy-capture-interactor-resolution.ts'; import { captureAndStoreSnapshot } from '../../../__tests__/support/command-surface-override-fake.ts'; import { @@ -203,7 +203,10 @@ function expectDescendantBasedRecording(session: SessionState): void { function writeSessionScript(session: SessionState): string { const root = mkdtempForTestSync('agent-device-press-retarget-'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter( + path.join(root, 'sessions'), + createLiveTestHostEnvironment().cwd, + ); const written = writer.write(session); if (!written.written) throw new Error('expected the script to be written'); return fs.readFileSync(written.path, 'utf8'); diff --git a/packages/daemon/src/execution/screenshot-runtime-execution.test.ts b/packages/daemon/src/execution/screenshot-runtime-execution.test.ts index 50690f1455..9a7960a0a5 100644 --- a/packages/daemon/src/execution/screenshot-runtime-execution.test.ts +++ b/packages/daemon/src/execution/screenshot-runtime-execution.test.ts @@ -72,6 +72,7 @@ test('admits one capture plan, binds once, and hands the runtime the resolved de const req = screenshotRequest({ positionals: [outPath] }); const resolved = await resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req, @@ -99,6 +100,7 @@ test('an iOS simulator session capture skips the redundant boot probe', async () const req = screenshotRequest({ positionals: [outPath] }); const resolved = await resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req, @@ -119,6 +121,7 @@ test('refuses an unavailable exact-owner capture fact before binding', async () const session = makeSession('default', { device: ANDROID_EMULATOR }); const resolved = await resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req: screenshotRequest({ positionals: ['/tmp/unsupported.png'] }), @@ -150,6 +153,7 @@ test('--overlay-refs is refused up front when the target cannot capture a tree', const session = makeSession('default', { device: ANDROID_EMULATOR }); const resolved = await resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req: screenshotRequest({ positionals: ['/tmp/overlay.png'], flags: { overlayRefs: true } }), @@ -184,6 +188,7 @@ test('the default destination is a reserved temp file, removed when the capture const req = screenshotRequest(); const resolved = await resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req, @@ -205,6 +210,7 @@ test('rejects the retired max-size field before inspecting owner facts', async ( await expect( resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req: screenshotRequest({ @@ -225,6 +231,7 @@ test('rejects --pixel-density outside iOS-family simulators before inspecting ow await expect( resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req: screenshotRequest({ @@ -277,6 +284,7 @@ test('the crop runs against the platform screenshot dimensions', async () => { }); const resolved = await resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req, @@ -324,6 +332,7 @@ test('a partial crop surfaces its warning once in the result record and annotate }); const resolved = await resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req, @@ -348,6 +357,7 @@ test('the crop plan is refused up front when the target cannot capture a tree', const session = makeSession('default', { device: ANDROID_EMULATOR }); const resolved = await resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req: screenshotRequest({ @@ -385,6 +395,7 @@ test('the result record carries the display rotation the capture reported', asyn const req = screenshotRequest({ positionals: [outPath] }); const resolved = await resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req, @@ -406,6 +417,7 @@ test('the result record omits the display rotation when the capture reported non const req = screenshotRequest({ positionals: [outPath] }); const resolved = await resolveScreenshotGenericExecution({ + hostCwd: process.cwd(), createCommandSurface: testCreateCommandSurface, platformServices: daemonPlatformServicesFixture(), req, diff --git a/packages/daemon/src/execution/screenshot-runtime.ts b/packages/daemon/src/execution/screenshot-runtime.ts index 6a11847c28..e7d2ba054e 100644 --- a/packages/daemon/src/execution/screenshot-runtime.ts +++ b/packages/daemon/src/execution/screenshot-runtime.ts @@ -52,6 +52,8 @@ export async function resolveScreenshotGenericExecution( req: DaemonRequest; session: SessionState; createCommandSurface: CreateDaemonCommandSurface; + /** The runtime host's working directory: the fallback base for a client-typed output path. */ + hostCwd: string; }> & ScreenshotRuntimeBindings, ): Promise { @@ -60,7 +62,7 @@ export async function resolveScreenshotGenericExecution( if (retiredMaxSize) throw new AppError('INVALID_ARGS', retiredMaxSize); assertSupportedScreenshotPixelDensity(session.device, req.flags?.screenshotPixelDensity); - const request = readScreenshotRequest(req); + const request = readScreenshotRequest(req, params.hostCwd); const cropOn = typeof req.flags?.screenshotCropOn === 'string' && req.flags.screenshotCropOn.length > 0 ? req.flags.screenshotCropOn @@ -319,11 +321,12 @@ async function annotateScreenshotWithSessionRefs( */ function readScreenshotRequest( req: DaemonRequest, + hostCwd: string, ): Readonly<{ outPath: string | undefined; recorded: RecordedGenericRequest }> { const positionals = req.positionals ?? []; const flags = req.flags ?? {}; const expand = (value: string | undefined) => - value === undefined ? undefined : expandSessionPath(value, req.meta?.cwd ?? process.cwd()); + value === undefined ? undefined : expandSessionPath(value, req.meta?.cwd ?? hostCwd); const positionalPath = expand(positionals[0]); const outFlag = expand(flags.out); return { diff --git a/packages/daemon/src/execution/sparse-fallback-screenshot.test.ts b/packages/daemon/src/execution/sparse-fallback-screenshot.test.ts index 8c318ffe44..f26d5a82eb 100644 --- a/packages/daemon/src/execution/sparse-fallback-screenshot.test.ts +++ b/packages/daemon/src/execution/sparse-fallback-screenshot.test.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { expect, test } from 'vitest'; import type { SnapshotQualityVerdict } from '@agent-device/kernel/snapshot'; import { makeIosSession } from '../__tests__/support/session-factories.ts'; -import { mkdtempForTest } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTest } from '@agent-device/test-kit'; import { SessionStore } from '../core/session-store.ts'; import { dispatchSnapshotViaRuntime } from '../sessions/snapshot-runtime.ts'; import type { DownloadableArtifactRegistration } from '../resources/artifact-tracking.ts'; @@ -25,7 +25,10 @@ const SPARSE: SnapshotQualityVerdict = { async function scenario() { const root = await mkdtempForTest('agent-device-sparse-fallback-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, diff --git a/packages/daemon/src/resources/__tests__/filesystem-boundary-faults.test.ts b/packages/daemon/src/resources/__tests__/filesystem-boundary-faults.test.ts index 30b38ebbbe..629e7e2368 100644 --- a/packages/daemon/src/resources/__tests__/filesystem-boundary-faults.test.ts +++ b/packages/daemon/src/resources/__tests__/filesystem-boundary-faults.test.ts @@ -24,10 +24,11 @@ import { makeRepairCompleteSession, } from '../../__tests__/support/session-factories.ts'; import { + createLiveTestHostEnvironment, + pinOwnProcessStartTime, + registerFilesystemBoundaryMatrix, type FilesystemBoundaryFixture, type FilesystemBoundaryOwner, - registerFilesystemBoundaryMatrix, - pinOwnProcessStartTime, } from '@agent-device/test-kit'; pinOwnProcessStartTime(hostProcess); @@ -118,7 +119,10 @@ function createDeviceClaimFixture(root: string): FilesystemBoundaryFixture { function createSessionScriptFixture(root: string): FilesystemBoundaryFixture { const targetPath = path.join(root, 'published.ad'); - const writer = new SessionScriptWriter(path.join(root, 'sessions')); + const writer = new SessionScriptWriter( + path.join(root, 'sessions'), + createLiveTestHostEnvironment().cwd, + ); const session = makeRepairCompleteSession('filesystem-fault', { scriptPublication: repairPublication('complete', { path: targetPath, force: true }), actions: [{ ts: 1, command: 'open', positionals: ['Demo'], flags: {} }], @@ -156,7 +160,7 @@ function createShutdownReportFixture(root: string): FilesystemBoundaryFixture { function createSessionStoreFixture(root: string): FilesystemBoundaryFixture { const targetPath = path.join(root, 'published-by-teardown.ad'); - const store = new SessionStore(path.join(root, 'sessions')); + const store = new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); const session = makeRepairCompleteSession('filesystem-fault', { scriptPublication: repairPublication('complete', { path: targetPath, force: true }), }); diff --git a/packages/daemon/src/resources/artifact-tracking.ts b/packages/daemon/src/resources/artifact-tracking.ts index a73a041a1e..70791de7c1 100644 --- a/packages/daemon/src/resources/artifact-tracking.ts +++ b/packages/daemon/src/resources/artifact-tracking.ts @@ -112,10 +112,11 @@ export function trackDownloadableArtifact( export async function prepareDownloadableArtifact( artifactId: string, - tenantId?: string, + tenantId: string | undefined, + hostEnv: NodeJS.ProcessEnv, ): Promise { const { entry, stat } = readDownloadableArtifactEntry(artifactId, tenantId); - const payload = await prepareArtifactPayload(artifactId, entry, stat); + const payload = await prepareArtifactPayload(artifactId, entry, stat, hostEnv); if (!payload) { throw new AppError( 'COMMAND_FAILED', @@ -126,7 +127,8 @@ export async function prepareDownloadableArtifact( } export async function listDownloadableArtifacts( - tenantId?: string, + tenantId: string | undefined, + hostEnv: NodeJS.ProcessEnv, ): Promise { const artifacts: DaemonArtifactInventoryEntry[] = []; for (const [id, entry] of pendingArtifacts()) { @@ -138,7 +140,7 @@ export async function listDownloadableArtifacts( cleanupDownloadableArtifact(id); continue; } - const payload = await prepareArtifactPayloadForInventory(id, entry, stat); + const payload = await prepareArtifactPayloadForInventory(id, entry, stat, hostEnv); if (!payload) continue; artifacts.push({ id, @@ -158,9 +160,10 @@ async function prepareArtifactPayloadForInventory( artifactId: string, entry: ArtifactEntry, stat: fs.Stats, + hostEnv: NodeJS.ProcessEnv, ): Promise { try { - return await prepareArtifactPayload(artifactId, entry, stat); + return await prepareArtifactPayload(artifactId, entry, stat, hostEnv); } catch { // Inventory is best-effort; direct downloads still surface artifact-specific errors. return null; @@ -191,6 +194,7 @@ async function prepareArtifactPayload( artifactId: string, entry: ArtifactEntry, stat: fs.Stats, + hostEnv: NodeJS.ProcessEnv, ): Promise { if (stat.isFile()) { return { @@ -202,7 +206,7 @@ async function prepareArtifactPayload( } if (!stat.isDirectory()) return null; - const directoryArchive = await ensureDirectoryArchive(artifactId, entry); + const directoryArchive = await ensureDirectoryArchive(artifactId, entry, hostEnv); return { artifactPath: directoryArchive.archivePath, fileName: directoryArchive.fileName, @@ -214,6 +218,7 @@ async function prepareArtifactPayload( async function ensureDirectoryArchive( artifactId: string, entry: ArtifactEntry, + hostEnv: NodeJS.ProcessEnv, ): Promise { if (entry.directoryArchive && fs.existsSync(entry.directoryArchive.archivePath)) { return entry.directoryArchive; @@ -222,7 +227,7 @@ async function ensureDirectoryArchive( return await entry.directoryArchivePromise; } - const archivePromise = createDirectoryArchive(artifactId, entry); + const archivePromise = createDirectoryArchive(artifactId, entry, hostEnv); entry.directoryArchivePromise = archivePromise; try { return await archivePromise; @@ -236,6 +241,7 @@ async function ensureDirectoryArchive( async function createDirectoryArchive( artifactId: string, entry: ArtifactEntry, + hostEnv: NodeJS.ProcessEnv, ): Promise { cleanupDirectoryArchive(entry); @@ -261,7 +267,7 @@ async function createDirectoryArchive( ], { env: { - ...process.env, + ...hostEnv, COPYFILE_DISABLE: '1', }, }, diff --git a/packages/daemon/src/resources/device/__tests__/device-claim-admission.test.ts b/packages/daemon/src/resources/device/__tests__/device-claim-admission.test.ts index 7a6a3df3f0..53cf66ba94 100644 --- a/packages/daemon/src/resources/device/__tests__/device-claim-admission.test.ts +++ b/packages/daemon/src/resources/device/__tests__/device-claim-admission.test.ts @@ -1,7 +1,8 @@ import { ANDROID_EMULATOR, - deviceClaimFilePath, + createLiveTestHostEnvironment, createTestHostEnvironment, + deviceClaimFilePath, mkdtempForTestSync, } from '@agent-device/test-kit'; import { expect, test, vi } from 'vitest'; @@ -635,7 +636,10 @@ async function bindInRequestSession(params: { flags: {}, meta: params.cwd ? { cwd: params.cwd } : undefined, }, - sessionStore: new SessionStore(path.join(params.stateDir, 'sessions')), + sessionStore: new SessionStore( + path.join(params.stateDir, 'sessions'), + createLiveTestHostEnvironment(), + ), leaseRegistry: new LeaseRegistry(), deviceRuntimeGateway: unavailableDeviceRuntimeGateway, platformRequestScope: { @@ -732,7 +736,10 @@ test('the request scope covers the implicit workspace session that routed to the const elsewhere = path.join(root, 'other-worktree'); fs.mkdirSync(workspace, { recursive: true }); fs.mkdirSync(elsewhere, { recursive: true }); - const sessionStore = new SessionStore(path.join(stateDir, 'sessions')); + const sessionStore = new SessionStore( + path.join(stateDir, 'sessions'), + createLiveTestHostEnvironment(), + ); const routedAddress = resolveEffectiveSessionName( { token: 't', diff --git a/packages/daemon/src/server/__tests__/host-lease-http.test.ts b/packages/daemon/src/server/__tests__/host-lease-http.test.ts index ce00adc40c..8e8a98ad66 100644 --- a/packages/daemon/src/server/__tests__/host-lease-http.test.ts +++ b/packages/daemon/src/server/__tests__/host-lease-http.test.ts @@ -32,6 +32,8 @@ async function withServer( leaseRegistry: registry, endSessionsForLease, handleRequest: async () => ({ ok: true, data: {} }), + env: process.env, + cwd: process.cwd(), }); try { const port = await listenOnLoopback(server); diff --git a/packages/daemon/src/server/__tests__/http-server-artifacts.test.ts b/packages/daemon/src/server/__tests__/http-server-artifacts.test.ts index bd6393420a..506a79f243 100644 --- a/packages/daemon/src/server/__tests__/http-server-artifacts.test.ts +++ b/packages/daemon/src/server/__tests__/http-server-artifacts.test.ts @@ -60,15 +60,21 @@ test(ANDROID_ARTIFACTS_CONTRACT_EVIDENCE.testName, async () => { try { assert.deepEqual( - (await listDownloadableArtifacts()).map((artifact) => artifact.filename), + (await listDownloadableArtifacts(undefined, process.env)).map( + (artifact) => artifact.filename, + ), ['public.txt'], ); assert.deepEqual( - (await listDownloadableArtifacts('tenant-a')).map((artifact) => artifact.filename), + (await listDownloadableArtifacts('tenant-a', process.env)).map( + (artifact) => artifact.filename, + ), ['public.txt', 'tenant-a.txt'], ); assert.deepEqual( - (await listDownloadableArtifacts('tenant-b')).map((artifact) => artifact.filename), + (await listDownloadableArtifacts('tenant-b', process.env)).map( + (artifact) => artifact.filename, + ), ['public.txt', 'tenant-b.txt'], ); } finally { @@ -105,7 +111,7 @@ test('downloadable artifact inventory skips directory artifacts that fail to arc if (cmd !== 'tar') return undefined; throw new Error('tar unavailable'); }, - async () => await listDownloadableArtifacts(), + async () => await listDownloadableArtifacts(undefined, process.env), ); assert.deepEqual( artifacts.map((artifact) => artifact.filename), @@ -134,6 +140,8 @@ test('daemon artifact inventory exposes directory artifacts as tar.gz downloads' const server = await createDaemonHttpServer({ token: 'daemon-secret', handleRequest: async (): Promise => ({ ok: true, data: {} }), + env: process.env, + cwd: process.cwd(), }); try { @@ -197,6 +205,8 @@ test('daemon artifact inventory lists artifacts and downloads consume them', asy const server = await createDaemonHttpServer({ token: 'daemon-secret', handleRequest: async (): Promise => ({ ok: true, data: {} }), + env: process.env, + cwd: process.cwd(), }); try { @@ -254,6 +264,8 @@ test('daemon artifact downloads can keep the source file while consuming the inv const server = await createDaemonHttpServer({ token: 'daemon-secret', handleRequest: async (): Promise => ({ ok: true, data: {} }), + env: process.env, + cwd: process.cwd(), }); try { @@ -303,6 +315,8 @@ test('daemon artifact downloads can be forced retained by server option', async token: 'daemon-secret', retainArtifacts: true, handleRequest: async (): Promise => ({ ok: true, data: {} }), + env: process.env, + cwd: process.cwd(), }); try { @@ -361,7 +375,7 @@ test('downloadable artifact inventory reports a screenshot display rotation only }); try { - const inventory = await listDownloadableArtifacts(); + const inventory = await listDownloadableArtifacts(undefined, process.env); const rotated = inventory.find((entry) => entry.id === rotatedId); const plain = inventory.find((entry) => entry.id === plainId); assert.equal(rotated?.displayRotation, 'landscape-right'); diff --git a/packages/daemon/src/server/__tests__/http-server-rpc-validation.test.ts b/packages/daemon/src/server/__tests__/http-server-rpc-validation.test.ts index 0a95d97b20..da7a752722 100644 --- a/packages/daemon/src/server/__tests__/http-server-rpc-validation.test.ts +++ b/packages/daemon/src/server/__tests__/http-server-rpc-validation.test.ts @@ -37,7 +37,11 @@ async function withCommandRpcServer( handlerCalls += 1; return { ok: true, data: { ok: true } }; }; - const server = await createDaemonHttpServer({ handleRequest }); + const server = await createDaemonHttpServer({ + handleRequest, + env: process.env, + cwd: process.cwd(), + }); try { const port = await listenOnLoopback(server); @@ -102,7 +106,11 @@ test('the rpc boundary never accepts internal request fields from the wire', asy received.push(req); return { ok: true, data: { ok: true } }; }; - const server = await createDaemonHttpServer({ handleRequest }); + const server = await createDaemonHttpServer({ + handleRequest, + env: process.env, + cwd: process.cwd(), + }); try { const port = await listenOnLoopback(server); @@ -155,7 +163,11 @@ async function withInstallFromSourceRpcServer( dispatched.push(req); return { ok: true, data: { ok: true } }; }; - const server = await createDaemonHttpServer({ handleRequest, env }); + const server = await createDaemonHttpServer({ + handleRequest, + env: env ?? process.env, + cwd: process.cwd(), + }); try { const port = await listenOnLoopback(server); @@ -233,6 +245,7 @@ test('remote HTTP rejects host path install sources in the command RPC used by t handlerCalls += 1; return { ok: true, data: {} }; }, + cwd: process.cwd(), }); try { @@ -278,6 +291,7 @@ test('remote HTTP accepts an uploaded path artifact without resolving the client } return { ok: true, data: {} }; }, + cwd: process.cwd(), }); try { @@ -309,6 +323,7 @@ test('local command RPC keeps host paths unrestricted', async (t) => { received.push(request); return { ok: true, data: {} }; }, + cwd: process.cwd(), }); try { @@ -338,6 +353,8 @@ test('lease.allocate forwards retainOnClose to the handler as lease meta', async received.push(request); return { ok: true, data: {} }; }, + env: process.env, + cwd: process.cwd(), }); try { const port = await listenOnLoopback(server); @@ -379,6 +396,7 @@ test('only local command RPC keeps the client developer dir', async (t) => { received.push(request); return { ok: true, data: {} }; }, + cwd: process.cwd(), }); try { const response = await postCommandRpc(await listenOnLoopback(server), params); @@ -414,6 +432,7 @@ test('only local lease allocation RPC keeps the provider credential fingerprint' received.push(request); return { ok: true, data: {} }; }, + cwd: process.cwd(), }); try { const response = await fetch(`http://127.0.0.1:${await listenOnLoopback(server)}/rpc`, { diff --git a/packages/daemon/src/server/__tests__/http-server-rpc.fixtures.ts b/packages/daemon/src/server/__tests__/http-server-rpc.fixtures.ts index b97a43e9a3..2df9432efb 100644 --- a/packages/daemon/src/server/__tests__/http-server-rpc.fixtures.ts +++ b/packages/daemon/src/server/__tests__/http-server-rpc.fixtures.ts @@ -72,8 +72,17 @@ export async function postRpcRaw( export async function startRpcServer( token: string, handleRequest: (request: DaemonRequest) => Promise, - options: Omit[0], 'handleRequest' | 'token'> = {}, + options: Omit< + Parameters[0], + 'handleRequest' | 'token' | 'env' | 'cwd' + > = {}, ): Promise<{ port: number; server: Server }> { - const server = await createDaemonHttpServer({ token, handleRequest, ...options }); + const server = await createDaemonHttpServer({ + token, + handleRequest, + env: process.env, + cwd: process.cwd(), + ...options, + }); return { port: (await listenOnLoopback(server)) as number, server }; } diff --git a/packages/daemon/src/server/__tests__/http-server-tenant-trust.test.ts b/packages/daemon/src/server/__tests__/http-server-tenant-trust.test.ts index 966430a645..d8aab1339d 100644 --- a/packages/daemon/src/server/__tests__/http-server-tenant-trust.test.ts +++ b/packages/daemon/src/server/__tests__/http-server-tenant-trust.test.ts @@ -51,6 +51,8 @@ async function withRpcServer( observedRequests.push(req); return { ok: true, data: { meta: req.meta } }; }, + env: process.env, + cwd: process.cwd(), }); try { const port = await listenOnLoopback(server); @@ -94,6 +96,8 @@ async function withDiagnosticsHookServer( path.join(sessionsDir, safeSessionName(ref.session)), ref.requestId, ), + env: process.env, + cwd: process.cwd(), }); try { const port = await listenOnLoopback(server); @@ -483,6 +487,8 @@ test('aux route (upload): a hook configured but silent on tenant refuses a clien const server = await createDaemonHttpServer({ token: DAEMON_TOKEN, handleRequest: async (): Promise => ({ ok: true, data: {} }), + env: process.env, + cwd: process.cwd(), }); try { const port = await listenOnLoopback(server); diff --git a/packages/daemon/src/server/__tests__/human-control-http.test.ts b/packages/daemon/src/server/__tests__/human-control-http.test.ts index 833de8970f..d2f194007f 100644 --- a/packages/daemon/src/server/__tests__/human-control-http.test.ts +++ b/packages/daemon/src/server/__tests__/human-control-http.test.ts @@ -66,6 +66,8 @@ for (const transport of ['tenant RPC', 'host PUT'] as const) { token: 'test-token', leaseRegistry: registry, handleRequest, + env: process.env, + cwd: process.cwd(), }); server.on('request', (_req, res) => { res.once('close', () => { @@ -128,6 +130,8 @@ test('host administration and tenant RPC use the same lease registry with distin token: 'test-token', leaseRegistry: registry, handleRequest, + env: process.env, + cwd: process.cwd(), }); try { const port = await listenOnLoopback(server); diff --git a/packages/daemon/src/server/__tests__/request-diagnostics-http.test.ts b/packages/daemon/src/server/__tests__/request-diagnostics-http.test.ts index 7cd709a32a..d8a9352e83 100644 --- a/packages/daemon/src/server/__tests__/request-diagnostics-http.test.ts +++ b/packages/daemon/src/server/__tests__/request-diagnostics-http.test.ts @@ -42,6 +42,8 @@ async function withDiagnosticsServer( path.join(sessionsDir, safeSessionName(ref.session)), ref.requestId, ), + env: process.env, + cwd: process.cwd(), }); try { const port = await listenOnLoopback(server); diff --git a/packages/daemon/src/server/__tests__/request-save-script-transports.test.ts b/packages/daemon/src/server/__tests__/request-save-script-transports.test.ts index 78258b43d9..931555d037 100644 --- a/packages/daemon/src/server/__tests__/request-save-script-transports.test.ts +++ b/packages/daemon/src/server/__tests__/request-save-script-transports.test.ts @@ -33,10 +33,11 @@ import type { SessionState } from '../../core/session-state.ts'; import { makeIosSession } from '../../__tests__/support/session-factories.ts'; import { closeLoopbackServer, + createLiveTestHostEnvironment, + createTestHostEnvironment, listenOnLoopback, mkdtempForTestSync, skipWhenLoopbackUnavailable, - createTestHostEnvironment, } from '@agent-device/test-kit'; import { flushSessionEventLogWrites } from '@agent-device/session-journal/session-event-log'; import { fakeCreateCommandSurface } from '../../__tests__/support/command-surface-fake.ts'; @@ -72,7 +73,10 @@ afterEach(async () => { function setup(): Harness { const root = mkdtempForTestSync('agent-device-save-script-transport-'); roots.push(root); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const session = makeIosSession(SESSION); sessionStore.publish(SESSION, session); const handleRequest = createRequestHandler({ @@ -125,7 +129,12 @@ async function sendOverHttp( handleRequest: DaemonInvokeFn, request: WireRequest, ): Promise { - const server = await createDaemonHttpServer({ handleRequest, token: TOKEN }); + const server = await createDaemonHttpServer({ + handleRequest, + token: TOKEN, + env: process.env, + cwd: process.cwd(), + }); try { const port = await listenOnLoopback(server); const response = await fetch(`http://127.0.0.1:${port}/rpc`, { diff --git a/packages/daemon/src/server/__tests__/session-idle-expiry-harness.ts b/packages/daemon/src/server/__tests__/session-idle-expiry-harness.ts index 501276e40d..63d89de99a 100644 --- a/packages/daemon/src/server/__tests__/session-idle-expiry-harness.ts +++ b/packages/daemon/src/server/__tests__/session-idle-expiry-harness.ts @@ -1,4 +1,9 @@ -import { deviceClaimFilePath, IOS_SIMULATOR, mkdtempForTestSync } from '@agent-device/test-kit'; +import { + IOS_SIMULATOR, + createLiveTestHostEnvironment, + deviceClaimFilePath, + mkdtempForTestSync, +} from '@agent-device/test-kit'; import { assert } from 'vitest'; import fs from 'node:fs'; import path from 'node:path'; @@ -100,7 +105,13 @@ export function createIdleExpiryHarness(): Readonly<{ return { makeFixture: (prefix) => { const root = mkdtempForTestSync(prefix); - return { sessionStore: new SessionStore(path.join(root, 'sessions')), claims: claimStores() }; + return { + sessionStore: new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ), + claims: claimStores(), + }; }, idleClaimedSession: (store, name = 'default') => { const session = makeIosSession(name, { diff --git a/packages/daemon/src/server/downloadable-artifact-http.ts b/packages/daemon/src/server/downloadable-artifact-http.ts index 532418f800..854afe1a5a 100644 --- a/packages/daemon/src/server/downloadable-artifact-http.ts +++ b/packages/daemon/src/server/downloadable-artifact-http.ts @@ -25,12 +25,17 @@ export function tryHandleDownloadableArtifactHttpRoute(params: { res: http.ServerResponse; authorize: DownloadableArtifactHttpAuthorizer; retainArtifacts?: boolean; + /** The runtime host's environment, handed to the directory-archive spawn. */ + hostEnv: NodeJS.ProcessEnv; }): boolean { - const { req, res, authorize, retainArtifacts } = params; + const { req, res, authorize, retainArtifacts, hostEnv } = params; const route = resolveDownloadableArtifactHttpRoute(req); if (route === null) return false; - void handleDownloadableArtifactHttpRoute(route, req, res, authorize, { retainArtifacts }); + void handleDownloadableArtifactHttpRoute(route, req, res, authorize, { + retainArtifacts, + hostEnv, + }); return true; } @@ -39,11 +44,11 @@ async function handleDownloadableArtifactHttpRoute( req: http.IncomingMessage, res: http.ServerResponse, authorize: DownloadableArtifactHttpAuthorizer, - options: { retainArtifacts?: boolean }, + options: { retainArtifacts?: boolean; hostEnv: NodeJS.ProcessEnv }, ): Promise { switch (route.kind) { case 'inventory': - await handleArtifactInventory(req, res, authorize); + await handleArtifactInventory(req, res, authorize, options); return; case 'download': await handleArtifactDownload(route.artifactId, req, res, authorize, options); @@ -68,7 +73,7 @@ async function handleArtifactDownload( req: http.IncomingMessage, res: http.ServerResponse, authorize: DownloadableArtifactHttpAuthorizer, - options: { retainArtifacts?: boolean }, + options: { retainArtifacts?: boolean; hostEnv: NodeJS.ProcessEnv }, ): Promise { if (!artifactId) { res.statusCode = 400; @@ -87,7 +92,7 @@ async function handleArtifactDownload( }); if (!auth) return; - const artifact = await prepareDownloadableArtifact(artifactId, auth.tenantId); + const artifact = await prepareDownloadableArtifact(artifactId, auth.tenantId, options.hostEnv); const retainArtifactAfterDownload = options.retainArtifacts === true; const stream = fs.createReadStream(artifact.artifactPath); res.statusCode = 200; @@ -133,6 +138,7 @@ async function handleArtifactInventory( req: http.IncomingMessage, res: http.ServerResponse, authorize: DownloadableArtifactHttpAuthorizer, + options: { hostEnv: NodeJS.ProcessEnv }, ): Promise { try { const auth = await authorize({ @@ -147,7 +153,11 @@ async function handleArtifactInventory( res.statusCode = 200; res.setHeader('content-type', 'application/json'); - res.end(JSON.stringify({ artifacts: await listDownloadableArtifacts(auth.tenantId) })); + res.end( + JSON.stringify({ + artifacts: await listDownloadableArtifacts(auth.tenantId, options.hostEnv), + }), + ); } catch (error) { sendRestJsonError(res, normalizeError(error)); } diff --git a/packages/daemon/src/server/http/daemon-runtime-device-claims.test.ts b/packages/daemon/src/server/http/daemon-runtime-device-claims.test.ts index e56771a9c8..20f35b2e85 100644 --- a/packages/daemon/src/server/http/daemon-runtime-device-claims.test.ts +++ b/packages/daemon/src/server/http/daemon-runtime-device-claims.test.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import { teardownDaemonSessionForShutdown } from './server-shutdown.ts'; import path from 'node:path'; import { afterEach, expect, test, vi } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { platformResourceCleanupFixture } from '../../__tests__/support/platform-resource-cleanup-fixture.ts'; vi.mock('../../sessions/session-teardown.ts', async (importOriginal) => { @@ -36,7 +36,10 @@ function setup(): { session: SessionState; sessionStore: SessionStore; stateDir: createdAt: Date.now(), actions: [], }; - const sessionStore = new SessionStore(path.join(stateDir, 'sessions')); + const sessionStore = new SessionStore( + path.join(stateDir, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish(session.name, session); return { session, sessionStore, stateDir }; } diff --git a/packages/daemon/src/server/http/daemon-runtime-recording-teardown.test.ts b/packages/daemon/src/server/http/daemon-runtime-recording-teardown.test.ts index 1e790f0321..fd0de74ec4 100644 --- a/packages/daemon/src/server/http/daemon-runtime-recording-teardown.test.ts +++ b/packages/daemon/src/server/http/daemon-runtime-recording-teardown.test.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { teardownDaemonSessionForShutdown } from './server-shutdown.ts'; import { afterEach, expect, test, vi } from 'vitest'; import type { ScreenRecordingCompletion } from '@agent-device/contracts/screen-recording-runtime'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { platformResourceCleanupFixture } from '../../__tests__/support/platform-resource-cleanup-fixture.ts'; import { SessionStore } from '../../core/session-store.ts'; import { makeRecordingSession } from '../../__tests__/support/session-teardown.fixtures.ts'; @@ -15,7 +15,10 @@ afterEach(() => { test('daemon shutdown awaits durable recording finalization inside its extended budget', async () => { vi.useFakeTimers(); const root = mkdtempForTestSync('agent-device-shutdown-recording-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const completion: ScreenRecordingCompletion = { backend: 'simctl recordVideo', outPath: path.join(root, 'recording.mp4'), @@ -51,7 +54,10 @@ test('daemon shutdown awaits durable recording finalization inside its extended test('daemon shutdown resolves durable recording resources through the effective session key', async () => { const root = mkdtempForTestSync('agent-device-shutdown-effective-session-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const effectiveSessionName = 'cwd:0123456789abcdef:default'; const session = makeRecordingSession({ name: effectiveSessionName, diff --git a/packages/daemon/src/server/http/daemon-runtime-web-close-teardown.test.ts b/packages/daemon/src/server/http/daemon-runtime-web-close-teardown.test.ts index 12376f23d9..a9f12e1294 100644 --- a/packages/daemon/src/server/http/daemon-runtime-web-close-teardown.test.ts +++ b/packages/daemon/src/server/http/daemon-runtime-web-close-teardown.test.ts @@ -1,7 +1,11 @@ import path from 'node:path'; import { teardownDaemonSessionForShutdown } from './server-shutdown.ts'; import { afterEach, expect, test, vi } from 'vitest'; -import { mkdtempForTestSync, WEB_DESKTOP_DEVICE } from '@agent-device/test-kit'; +import { + WEB_DESKTOP_DEVICE, + createLiveTestHostEnvironment, + mkdtempForTestSync, +} from '@agent-device/test-kit'; import { platformResourceCleanupFixture } from '../../__tests__/support/platform-resource-cleanup-fixture.ts'; import { makeWebSession } from '../../__tests__/support/session-teardown.fixtures.ts'; import { SessionStore } from '../../core/session-store.ts'; @@ -12,7 +16,10 @@ afterEach(() => { }); function openStore(prefix: string): SessionStore { - return new SessionStore(path.join(mkdtempForTestSync(prefix), 'sessions')); + return new SessionStore( + path.join(mkdtempForTestSync(prefix), 'sessions'), + createLiveTestHostEnvironment(), + ); } // The agent-browser CLI call a web close makes has its own 30s internal timeout, well past the 5s diff --git a/packages/daemon/src/server/http/daemon-runtime.ts b/packages/daemon/src/server/http/daemon-runtime.ts index acb59b7ab5..cf1cd49ed5 100644 --- a/packages/daemon/src/server/http/daemon-runtime.ts +++ b/packages/daemon/src/server/http/daemon-runtime.ts @@ -308,7 +308,7 @@ async function startDaemonRuntimeOnHost( ? () => assertDaemonPolicyAllowsCapability(daemonPolicy, 'device-shutdown') : undefined; - const sessionStore = new SessionStore(sessionsDir); + const sessionStore = new SessionStore(sessionsDir, host); const deps = await options.composeDeps({ host, baseDir, diff --git a/packages/daemon/src/server/http/http-authorization.test.ts b/packages/daemon/src/server/http/http-authorization.test.ts index 6fab53b1c3..5e927b667d 100644 --- a/packages/daemon/src/server/http/http-authorization.test.ts +++ b/packages/daemon/src/server/http/http-authorization.test.ts @@ -36,6 +36,8 @@ async function withAuxRouteAndHook( token: DAEMON_TOKEN, handleRequest: async (): Promise => ({ ok: true, data: {} }), resolveRequestDiagnosticsPath: () => path.join(root, 'unused-request-log.json'), + env: process.env, + cwd: process.cwd(), }); try { const port = await listenOnLoopback(server); diff --git a/packages/daemon/src/server/http/http-instance-precondition.test.ts b/packages/daemon/src/server/http/http-instance-precondition.test.ts index 958742ff44..515b5ba799 100644 --- a/packages/daemon/src/server/http/http-instance-precondition.test.ts +++ b/packages/daemon/src/server/http/http-instance-precondition.test.ts @@ -23,6 +23,8 @@ test('a stale RPC instance is refused after authentication and before command di handlerCalls += 1; return { ok: true, data: {} }; }, + env: process.env, + cwd: process.cwd(), }); try { const port = await listenOnLoopback(server); diff --git a/packages/daemon/src/server/http/http-server-boundary-cancellation.test.ts b/packages/daemon/src/server/http/http-server-boundary-cancellation.test.ts index 1d4c26b7c4..59fba98380 100644 --- a/packages/daemon/src/server/http/http-server-boundary-cancellation.test.ts +++ b/packages/daemon/src/server/http/http-server-boundary-cancellation.test.ts @@ -44,6 +44,8 @@ for (const [commandClass, row] of Object.entries(CANCELLATION_ROWS)) { markAbortObserved(); return { ok: true, data: { canceled: signal.aborted } }; }, + env: process.env, + cwd: process.cwd(), }); try { diff --git a/packages/daemon/src/server/http/http-server-disconnect.test.ts b/packages/daemon/src/server/http/http-server-disconnect.test.ts index 862a79d735..3331d502a6 100644 --- a/packages/daemon/src/server/http/http-server-disconnect.test.ts +++ b/packages/daemon/src/server/http/http-server-disconnect.test.ts @@ -39,6 +39,8 @@ test('HTTP request disconnected before response headers cancels the request', as markAbortObserved(); return { ok: true, data: { canceled: signal.aborted } }; }, + env: process.env, + cwd: process.cwd(), }); try { @@ -82,6 +84,8 @@ test('concurrent HTTP requests reject a duplicate request ID without replacing i await firstReleased; return { ok: true, data: { survived: !firstSignal.aborted } }; }, + env: process.env, + cwd: process.cwd(), }); try { @@ -153,6 +157,8 @@ test('disconnecting one HTTP request leaves another request uncanceled', async ( await survivorReleased; return { ok: true, data: { survived: true } }; }, + env: process.env, + cwd: process.cwd(), }); try { diff --git a/packages/daemon/src/server/http/http-server-lease-allocate.test.ts b/packages/daemon/src/server/http/http-server-lease-allocate.test.ts index 85a9e35fae..a82ff22c7b 100644 --- a/packages/daemon/src/server/http/http-server-lease-allocate.test.ts +++ b/packages/daemon/src/server/http/http-server-lease-allocate.test.ts @@ -23,6 +23,8 @@ test('Provider-backed integration daemon HTTP lease allocate forwards provider a observedRequests.push(req); return { ok: true, data: { command: req.command } }; }, + env: process.env, + cwd: process.cwd(), }); try { diff --git a/packages/daemon/src/server/http/http-server-limrun-uploaded-apps.test.ts b/packages/daemon/src/server/http/http-server-limrun-uploaded-apps.test.ts index 544e2542fd..3fb5004428 100644 --- a/packages/daemon/src/server/http/http-server-limrun-uploaded-apps.test.ts +++ b/packages/daemon/src/server/http/http-server-limrun-uploaded-apps.test.ts @@ -26,7 +26,13 @@ test('public HTTP marks command and lease requests before dispatch', async (t) = requests.push(request); return { ok: true, data: {} }; }); - const server = await createDaemonHttpServer({ token, leaseRegistry, handleRequest }); + const server = await createDaemonHttpServer({ + token, + leaseRegistry, + handleRequest, + env: process.env, + cwd: process.cwd(), + }); try { const port = await listenOnLoopback(server); diff --git a/packages/daemon/src/server/http/http-server-provider-scenarios.test.ts b/packages/daemon/src/server/http/http-server-provider-scenarios.test.ts index 83a9d4cfd1..a46e81c069 100644 --- a/packages/daemon/src/server/http/http-server-provider-scenarios.test.ts +++ b/packages/daemon/src/server/http/http-server-provider-scenarios.test.ts @@ -22,11 +22,23 @@ import { import { closeLoopbackServer, + createLiveTestHostEnvironment, listenOnLoopback, mkdtempForTestSync, skipWhenLoopbackUnavailable, } from '@agent-device/test-kit'; +/** The host facts every scenario server runs with: the live process env and cwd (#3753). */ +const scenarioHost = createLiveTestHostEnvironment(); + +async function startScenarioServer() { + return await createDaemonHttpServer({ + token: 'provider-scenario-token', + handleRequest: async (): Promise => ({ ok: true, data: {} }), + ...scenarioHost, + }); +} + type RpcResponse = { status: number; body: { @@ -85,6 +97,7 @@ test('Provider-backed integration daemon HTTP server maps RPC methods, auth, and }, }; }, + ...createLiveTestHostEnvironment(), }); try { @@ -276,6 +289,7 @@ test('Provider-backed integration daemon HTTP server cancels progress streams wh resolveHandlerDone(); } }, + ...createLiveTestHostEnvironment(), }); try { @@ -317,6 +331,7 @@ test('Provider-backed integration daemon HTTP server destroys oversized RPC requ handled = true; return { ok: true, data: {} }; }, + ...createLiveTestHostEnvironment(), }); try { @@ -360,6 +375,7 @@ test('Provider-backed integration daemon HTTP server validates malformed JSON-RP handled = true; return { ok: true, data: {} }; }, + ...createLiveTestHostEnvironment(), }); try { @@ -434,6 +450,7 @@ test('Provider-backed integration daemon HTTP server writes error envelopes afte }); throw new AppError('COMMAND_FAILED', 'stream failed after headers'); }, + ...createLiveTestHostEnvironment(), }); try { @@ -483,10 +500,7 @@ test('Provider-backed integration daemon HTTP server accepts uploads and streams artifactType: 'screenshot', fileName: 'screen.png', }); - const server = await createDaemonHttpServer({ - token: 'provider-scenario-token', - handleRequest: async (): Promise => ({ ok: true, data: {} }), - }); + const server = await startScenarioServer(); try { const port = await listenOnLoopback(server); @@ -534,10 +548,7 @@ test('Provider-backed integration daemon HTTP server resumes direct uploads befo } const content = Buffer.from('fake-apk-resumable-content'); - const server = await createDaemonHttpServer({ - token: 'provider-scenario-token', - handleRequest: async (): Promise => ({ ok: true, data: {} }), - }); + const server = await startScenarioServer(); let trackedUploadId = ''; try { @@ -620,10 +631,7 @@ test('Provider-backed integration daemon HTTP server isolates same-artifact uplo } const content = Buffer.from('same-apk-content'); - const server = await createDaemonHttpServer({ - token: 'provider-scenario-token', - handleRequest: async (): Promise => ({ ok: true, data: {} }), - }); + const server = await startScenarioServer(); const trackedUploadIds: string[] = []; try { @@ -708,10 +716,7 @@ test('Provider-backed integration daemon HTTP server reports unknown upload tick return; } - const server = await createDaemonHttpServer({ - token: 'provider-scenario-token', - handleRequest: async (): Promise => ({ ok: true, data: {} }), - }); + const server = await startScenarioServer(); try { const port = await listenOnLoopback(server); @@ -778,6 +783,8 @@ test('Provider-backed integration daemon HTTP auth hook can scope tenants and re observedRequest = req; return { ok: true, data: { meta: req.meta } }; }, + env: process.env, + cwd: process.cwd(), }); const port = await listenOnLoopback(server); const accepted = await callRpc( diff --git a/packages/daemon/src/server/http/http-server-proxy.test.ts b/packages/daemon/src/server/http/http-server-proxy.test.ts index ca93e9c3a4..9f12c62043 100644 --- a/packages/daemon/src/server/http/http-server-proxy.test.ts +++ b/packages/daemon/src/server/http/http-server-proxy.test.ts @@ -194,6 +194,7 @@ test('proxy enforces public-only Maestro HTTP policy on a local daemon', async ( ), }; }, + cwd: process.cwd(), }); const targetPort = await listenOnLoopback(loopbackTarget); const daemonPort = await listenOnLoopback(daemon); @@ -267,6 +268,7 @@ test('daemon proxy cancels the upstream daemon request when its client disconnec await canceled; return { ok: false, error: { code: 'COMMAND_FAILED', message: 'request canceled' } }; }, + cwd: process.cwd(), }); const proxy = createDaemonProxyServer({ upstreamBaseUrl: `http://127.0.0.1:${await listenOnLoopback(daemon)}`, diff --git a/packages/daemon/src/server/http/http-server-resumable-upload.test.ts b/packages/daemon/src/server/http/http-server-resumable-upload.test.ts index 3190c6e991..27c752e92e 100644 --- a/packages/daemon/src/server/http/http-server-resumable-upload.test.ts +++ b/packages/daemon/src/server/http/http-server-resumable-upload.test.ts @@ -21,6 +21,8 @@ test('chunked ranged overflow rolls back before an exact retry and finalize', as const server = await createDaemonHttpServer({ token: 'resumable-token', handleRequest: async (): Promise => ({ ok: true, data: {} }), + env: process.env, + cwd: process.cwd(), }); let trackedUploadId = ''; try { diff --git a/packages/daemon/src/server/http/http-server.ts b/packages/daemon/src/server/http/http-server.ts index b689d17f5b..ac2d9d26a5 100644 --- a/packages/daemon/src/server/http/http-server.ts +++ b/packages/daemon/src/server/http/http-server.ts @@ -75,8 +75,10 @@ export async function createDaemonHttpServer(options: { endSessionsForLease?: (leaseId: string) => Promise; token?: string; retainArtifacts?: boolean; - env?: NodeJS.ProcessEnv; - cwd?: string; + /** The runtime host's environment: read for the auth hook and handed to the download archive. */ + env: NodeJS.ProcessEnv; + /** The runtime host's working directory, the base for resolving the auth hook module. */ + cwd: string; /** * Resolves a request diagnostics record path for the `/sessions/.../requests/...` * route (#1801). Omitted by embedded servers with no session store; the route @@ -97,8 +99,8 @@ export async function createDaemonHttpServer(options: { const leaseBackends = LEASE_BACKENDS.filter( (backend) => backend !== 'macos-app' || process.platform === 'darwin', ); - const environment = options.env ?? process.env; - const authHook = await loadHttpAuthHook(environment, options.cwd ?? process.cwd()); + const environment = options.env; + const authHook = await loadHttpAuthHook(environment, options.cwd); const { handleRequest, token, @@ -159,6 +161,7 @@ export async function createDaemonHttpServer(options: { req, res, retainArtifacts, + hostEnv: environment, authorize: async (request) => await authorizeAuxiliaryHttpRequest({ req: request.req, diff --git a/packages/daemon/src/server/request-router.ts b/packages/daemon/src/server/request-router.ts index 23f8df44a8..e3cbd0051d 100644 --- a/packages/daemon/src/server/request-router.ts +++ b/packages/daemon/src/server/request-router.ts @@ -610,6 +610,7 @@ async function dispatchGenericForLockedScope(params: { const runtimeExecution = await resolveGenericRuntimeExecution({ req: lockedScope.req, session, + hostCwd: sessionStore.host.cwd, // `scroll` parses its distance/timing flags during admission, so the resolved context is // needed before the dispatcher builds its own. context: lockedScope.contextFromFlags( diff --git a/packages/daemon/src/server/session-idle-expiry.ts b/packages/daemon/src/server/session-idle-expiry.ts index 903472e1da..d20fcc4885 100644 --- a/packages/daemon/src/server/session-idle-expiry.ts +++ b/packages/daemon/src/server/session-idle-expiry.ts @@ -44,7 +44,7 @@ const IDLE_EXPIRY_TOMBSTONE_TTL_MS = 60 * 60_000; * shortest window the unit supports, not for the feature being disabled, and silently disabling a * feature someone just opted into is the worst possible reading of a typo. */ -export function resolveSessionIdleExpiryMs(env: NodeJS.ProcessEnv = process.env): number { +export function resolveSessionIdleExpiryMs(env: NodeJS.ProcessEnv): number { const raw = env[SESSION_IDLE_EXPIRY_ENV]?.trim(); if (!raw) return 0; const parsed = Number(raw); diff --git a/packages/daemon/src/sessions/__tests__/open-device-contention-wait.test.ts b/packages/daemon/src/sessions/__tests__/open-device-contention-wait.test.ts index ef065afa89..7defc81400 100644 --- a/packages/daemon/src/sessions/__tests__/open-device-contention-wait.test.ts +++ b/packages/daemon/src/sessions/__tests__/open-device-contention-wait.test.ts @@ -2,7 +2,7 @@ import { test, expect, vi, afterEach } from 'vitest'; import { isRequestCanceledError } from '@agent-device/kernel/errors'; import type { CommandFlags } from '@agent-device/contracts/command'; import { clearRequestCanceled, markRequestCanceled } from '@agent-device/host-kit/request'; -import { IOS_SIMULATOR } from '@agent-device/test-kit'; +import { IOS_SIMULATOR, createLiveTestHostEnvironment } from '@agent-device/test-kit'; import { getFlagDefinitionsForKey } from '@agent-device/command-registry/flag-registry'; import { makeSessionStore } from '../../__tests__/support/store-factory.ts'; import type { DaemonRequest } from '../../core/daemon-request.ts'; @@ -132,7 +132,7 @@ test('a refusal offers the flag only to a caller that did not arrive carrying it }); test('only a fresh open with a budget and a resolved device gets a wait', () => { - const sessionStore = new SessionStore('/tmp/ad-wait-none'); + const sessionStore = new SessionStore('/tmp/ad-wait-none', createLiveTestHostEnvironment()); const budget = openRequest({ waitMs: 5_000 }); expect( diff --git a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts index 1437d0ebc4..27150be500 100644 --- a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-android-occlusion.test.ts @@ -6,7 +6,7 @@ import { ANDROID_QS_SHADE_CAPTURE_RAW_NODES, walkNonRawAndroidFixture, } from '@agent-device/platform-android/ui-hierarchy-fixtures'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { SessionStore } from '../../../core/session-store.ts'; import { replayCoordinatorForTest, replaySessionForTest } from './replay-session-fixture.ts'; import { @@ -50,7 +50,10 @@ test.each([ }, ])('buildReplayFailureDivergence: a full-cover quick-settings shade $name', async ({ prepare }) => { const root = mkdtempForTestSync('agent-device-replay-divergence-qsshade-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, diff --git a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-capture-policy.test.ts b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-capture-policy.test.ts index e62394d153..9df8d1e6ef 100644 --- a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-capture-policy.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-capture-policy.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { beforeEach, expect, test, vi } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { makeAndroidSession } from '../../../__tests__/support/session-factories.ts'; import { SessionStore } from '../../../core/session-store.ts'; import { buildReplayFailureDivergence } from '@agent-device/replay-port/session-replay-divergence'; @@ -49,7 +49,10 @@ beforeEach(() => resetLegacySnapshotCapture(vi.mocked(captureSnapshotWithInterac // this file, so the retry BRANCH runs without a real wall-clock wait. test('buildReplayFailureDivergence: routes through the freshness-retry wrapper and uses the retried fresh tree, not the first stale dump (#1264 capture parity)', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-fresh-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const appBundleId = 'com.callstack.agentdevicelab'; const session = makeAndroidSession(sessionName, { appBundleId }); @@ -146,7 +149,10 @@ test('buildReplayFailureDivergence: routes through the freshness-retry wrapper a // dropped while the interactive-only policy is still applied. test('buildReplayFailureDivergence: divergence capture drops the action snapshotRaw/scope/depth flags (#1264 clean flags policy)', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-flags-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const appBundleId = 'com.callstack.agentdevicelab'; sessionStore.publish(sessionName, makeAndroidSession(sessionName, { appBundleId })); diff --git a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts index d917a5f6b1..5df12c3ece 100644 --- a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-chrome-filter.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { beforeEach, expect, test, vi } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { makeAndroidSession, makeIosSession, @@ -99,7 +99,10 @@ function keyboardSwampedNodes(actionableLabel: string) { test('buildReplayFailureDivergence excludes keyboard chrome from screen.refs and surfaces the actionable target within the cap', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-keyboard-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -210,7 +213,10 @@ function structuralNoiseNodes() { test('buildReplayFailureDivergence drops unlabeled non-interactive structural nodes so actionable controls surface within the cap', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-structural-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -325,7 +331,10 @@ function keyboardWithAccessoryNodes() { test('buildReplayFailureDivergence keeps an app inputAccessoryView control in screen.refs while filtering keyboard keys', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-accessory-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -380,7 +389,10 @@ test('buildReplayFailureDivergence keeps an app inputAccessoryView control in sc // on-device transport that produces the pre-walk raw tree is stubbed. test('buildReplayFailureDivergence excludes Android status-bar/IME chrome from screen.refs on a real (walked) non-raw capture', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-android-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, diff --git a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-observation.test.ts b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-observation.test.ts index ef0cdeee7f..b22144f225 100644 --- a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-observation.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-observation.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { beforeEach, expect, test, vi } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { AppError } from '@agent-device/kernel/errors'; import { sleep } from '@agent-device/host-kit/retry'; import { makeIosSession } from '../../../__tests__/support/session-factories.ts'; @@ -48,7 +48,10 @@ test('captureDivergenceObservation retryLaunchRace: the 12s deadline bounds retr vi.useFakeTimers(); try { const root = mkdtempForTestSync('agent-device-replay-divergence-deadline-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -94,7 +97,7 @@ test.each(['rebuild', 'retire'] as const)( 'divergence capture binds observation authority before awaiting the native capture: %s', async (change) => { const root = mkdtempForTestSync('agent-device-divergence-lifetime-'); - const store = new SessionStore(path.join(root, 'sessions')); + const store = new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); const session = makeIosSession('default', { appBundleId: 'com.example.app' }); const ref = store.publish('cwd:worktree:default', session); const replay = replayDivergenceForTest(store, ref.address); @@ -142,7 +145,7 @@ test.each(['rebuild', 'retire'] as const)( 'capture retries retain their original lifetime through backoff: %s', async (change) => { const root = mkdtempForTestSync('agent-device-divergence-retry-lifetime-'); - const store = new SessionStore(path.join(root, 'sessions')); + const store = new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); const session = makeIosSession('default', { appBundleId: 'com.example.app' }); const ref = store.publish('cwd:worktree:default', session); const replay = replayDivergenceForTest(store, ref.address); diff --git a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts index 9d24591e07..674530bbd2 100644 --- a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-overlay.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { beforeEach, expect, test, vi } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { makeAndroidSession } from '../../../__tests__/support/session-factories.ts'; import { ANDROID_IME_CAPTURE_RAW_NODES, @@ -47,7 +47,10 @@ beforeEach(() => resetLegacySnapshotCapture(vi.mocked(captureSnapshotWithInterac // dedicated realistic-tree test below. test('buildReplayFailureDivergence: a system-overlay window survives into screen.refs while status/nav chrome is filtered (#1264)', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-overlay-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -160,7 +163,10 @@ function appSwampWithTrailingOverlay(appBundleId: string) { test('buildReplayFailureDivergence: a fully-captured overlay dismiss-target enumerating LAST still lands within the screen.refs cap (#1264 cap burial)', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-burial-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const appBundleId = 'com.callstack.agentdevicelab'; sessionStore.publish(sessionName, makeAndroidSession(sessionName, { appBundleId })); @@ -256,7 +262,10 @@ function overlayMassCoveringApp(appBundleId: string) { test('buildReplayFailureDivergence: when a system overlay mass-covers the app, the overlay dismiss-target surfaces in screen.refs and refs is non-empty (#1264 occlusion)', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-occlusion-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const appBundleId = 'com.callstack.agentdevicelab'; sessionStore.publish(sessionName, makeAndroidSession(sessionName, { appBundleId })); @@ -331,7 +340,10 @@ function bareScrimMassCoveringApp(appBundleId: string) { test('buildReplayFailureDivergence: a mass-covered app with no actionable overlay node still returns non-empty screen.refs (#1264 occlusion fallback)', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-scrim-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const appBundleId = 'com.callstack.agentdevicelab'; sessionStore.publish(sessionName, makeAndroidSession(sessionName, { appBundleId })); @@ -381,7 +393,10 @@ test('buildReplayFailureDivergence: a mass-covered app with no actionable overla // the frame scope equals the emitted screen ref set (covered refs included). test('buildReplayFailureDivergence: the partial ref frame authorizes exactly the emitted screen.refs, including mass-covered fallback refs (#1257 + #1264)', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-frame-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const appBundleId = 'com.callstack.agentdevicelab'; sessionStore.publish(sessionName, makeAndroidSession(sessionName, { appBundleId })); diff --git a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-publication.test.ts b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-publication.test.ts index 03ce256165..8107313155 100644 --- a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-publication.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-publication.test.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { beforeEach, expect, test, vi } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; vi.mock('../../../core/snapshot-interactor-capture.ts', () => ({ captureSnapshotWithInteractor: vi.fn(), @@ -42,7 +42,10 @@ beforeEach(() => { function scenario(refCount = 20) { const root = mkdtempForTestSync('agent-device-divergence-publication-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const session = makeIosSession(sessionName, { appBundleId: 'com.example.app' }); const prior: SnapshotState = { diff --git a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-suggestions.test.ts b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-suggestions.test.ts index e36a15f3e7..1b426cf2b4 100644 --- a/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-suggestions.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-divergence/session-replay-divergence-suggestions.test.ts @@ -1,6 +1,6 @@ import path from 'node:path'; import { beforeEach, expect, test, vi } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { makeIosSession } from '../../../__tests__/support/session-factories.ts'; import { SessionStore } from '../../../core/session-store.ts'; import { buildReplayFailureDivergence } from '@agent-device/replay-port/session-replay-divergence'; @@ -25,7 +25,10 @@ beforeEach(() => resetLegacySnapshotCapture(vi.mocked(captureSnapshotWithInterac test('buildReplayFailureDivergence dedupes suggestions using the strongest basis', async () => { const root = mkdtempForTestSync('agent-device-replay-suggest-dedupe-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -78,7 +81,10 @@ test.each(['region', 'absent', 'none', 'unverifiable'] as const)( 'suggestions rank identity, recorded region, then document order (evidence: %s)', async (evidence) => { const root = mkdtempForTestSync('agent-device-replay-suggest-region-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish('default', makeIosSession('default', { appBundleId: 'com.example.app' })); const button = (index: number, parentIndex: number, label: string) => ({ index, diff --git a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts index 35269909f5..4f760efd70 100644 --- a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-error-projection.test.ts @@ -7,7 +7,7 @@ import { SessionStore } from '../../../core/session-store.ts'; import { replayCommandForTest } from '../replay-runtime/replay-command-fixture.ts'; import { makeIosSession } from '../../../__tests__/support/session-factories.ts'; import { maestroScriptSourceBundleFor } from '../../../__tests__/support/replay-script-source-fixture.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import type { DaemonRequest } from '../../../core/daemon-request.ts'; // A replay error raised before any step runs carries no `failure`, so the route @@ -19,7 +19,10 @@ test('a typed Maestro replay error keeps its recovery hint', async () => { const root = mkdtempForTestSync('agent-device-maestro-hint-'); const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, `${stringify({ appId: 'com.example.app' })}---\n${stringify([])}`); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish('default', makeIosSession('default')); const req = { diff --git a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-failure.test.ts b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-failure.test.ts index 7812c140a4..c63411cea8 100644 --- a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-failure.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-failure.test.ts @@ -1,5 +1,5 @@ import { beforeEach, expect, test, vi } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { const actual = @@ -47,7 +47,10 @@ async function buildFailureScenario( ) { const root = mkdtempForTestSync('agent-device-maestro-suggestions-'); const sessionName = 'default'; - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, maestroFlowForCommand(command)); @@ -94,7 +97,10 @@ function maestroFlowForCommand(command: MaestroRuntimeCommand): string { test('typed Maestro failure diagnostics render expanded selector values without extra flags', async () => { const root = mkdtempForTestSync('agent-device-maestro-expanded-selector-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); @@ -157,7 +163,10 @@ test('typed Maestro failure diagnostics render expanded selector values without test('typed Maestro nested scopes retain resolved target values after unwind', async () => { const root = mkdtempForTestSync('agent-device-maestro-nested-redaction-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); @@ -254,7 +263,10 @@ test('typed Maestro nested scopes retain resolved target values after unwind', a test('typed Maestro renders flow-local values when static include resolution fails', async () => { const root = mkdtempForTestSync('agent-device-maestro-include-redaction-'); const sessionName = 'default'; - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); const flowName = 'checkout-details'; diff --git a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts index 5a1309cc2c..3a2507465c 100644 --- a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-remote-evalscript.test.ts @@ -8,7 +8,11 @@ import { platformResourceCleanupFixture } from '../../../__tests__/support/platf import { SessionStore } from '../../../core/session-store.ts'; import { makeIosSession } from '../../../__tests__/support/session-factories.ts'; import { maestroScriptSourceBundleFor } from '../../../__tests__/support/replay-script-source-fixture.ts'; -import { mkdtempForTestSync, createTestHostEnvironment } from '@agent-device/test-kit'; +import { + createLiveTestHostEnvironment, + createTestHostEnvironment, + mkdtempForTestSync, +} from '@agent-device/test-kit'; import type { DaemonRequest } from '../../../core/daemon-request.ts'; import { unavailableBindDevice, @@ -48,7 +52,10 @@ function engineTrustFlags(): boolean[] { async function runWithNetworkFlag(publicNetworkOnly: boolean | undefined) { const root = mkdtempForTestSync('agent-device-maestro-remote-wire-'); const flowPath = writeFlow(root, 'flow.yaml'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish('default', makeIosSession('default')); const req = { @@ -97,7 +104,10 @@ async function runWithTestNetworkFlag(publicNetworkOnly: boolean | undefined) { const root = mkdtempForTestSync('agent-device-maestro-remote-test-'); const firstPath = writeFlow(root, '01-flow.yaml'); const secondPath = writeFlow(root, '02-flow.yaml'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const req = { token: 'test-token', diff --git a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts index c8535c10f3..6427473048 100644 --- a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-maestro-session-address.test.ts @@ -7,7 +7,7 @@ import { SessionStore } from '../../../core/session-store.ts'; import { replayCommandForTest } from '../replay-runtime/replay-command-fixture.ts'; import { makeIosSession } from '../../../__tests__/support/session-factories.ts'; import { maestroScriptSourceBundleFor } from '../../../__tests__/support/replay-script-source-fixture.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import type { DaemonRequest } from '../../../core/daemon-request.ts'; // The Maestro replay route resolves the store key upstream and then validates the session's @@ -19,7 +19,10 @@ test('a typed Maestro selector conflict names the session store key, not "defaul const root = mkdtempForTestSync('agent-device-maestro-address-'); const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, `${stringify({ appId: 'com.example.app' })}---\n${stringify([])}`); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const session = makeIosSession('default'); session.sessionScope = { kind: 'cwd', id: '8bea844ab16aa9b3' }; sessionStore.publish(SCOPED_ADDRESS, session); diff --git a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts index fdeebcdab2..a7dad5e715 100644 --- a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro-dispatch.test.ts @@ -5,8 +5,9 @@ import fs from 'node:fs'; import path from 'node:path'; import { test } from 'vitest'; import { - assertDispatchDisclosureDriversMatchRows, DISPATCH_DISCLOSURE_TABLE_PATH, + assertDispatchDisclosureDriversMatchRows, + createLiveTestHostEnvironment, dispatchDisclosureRowsOwnedBy, mkdtempForTestSync, } from '@agent-device/test-kit'; @@ -206,7 +207,7 @@ test('runReplayCommand reports snapshot diagnostics from per-action session samp scriptPath, ['snapshot', 'snapshot', 'snapshot', 'snapshot', 'snapshot', ''].join('\n'), ); - const sessionStore = new SessionStore(path.join(root, 'state')); + const sessionStore = new SessionStore(path.join(root, 'state'), createLiveTestHostEnvironment()); sessionStore.publish( 's', makeIosSession('s', { @@ -270,7 +271,7 @@ test('runReplayCommand reports snapshot diagnostics on replay failure', async () scriptPath, ['snapshot', 'snapshot', 'snapshot', 'snapshot', 'click "Missing"', ''].join('\n'), ); - const sessionStore = new SessionStore(path.join(root, 'state')); + const sessionStore = new SessionStore(path.join(root, 'state'), createLiveTestHostEnvironment()); sessionStore.publish( 's', makeIosSession('s', { diff --git a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts index 78c34fa7d0..adc7949fc1 100644 --- a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro-shell-artifacts.test.ts @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import { test } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { SessionStore } from '../../../core/session-store.ts'; import { replayScriptSourceBundleFor } from '../../../__tests__/support/replay-script-source-fixture.ts'; @@ -45,7 +45,7 @@ test('runReplayCommand writes per-action timing events to active trace', async ( fs.writeFileSync(scriptPath, 'context platform=ios\nclick id="submit"\nwait "Done" 5000\n'); fs.writeFileSync(tracePath, ''); - const sessionStore = new SessionStore(path.join(root, 'state')); + const sessionStore = new SessionStore(path.join(root, 'state'), createLiveTestHostEnvironment()); sessionStore.publish('s', { name: 's', device: { diff --git a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts index 8ba176c0a8..30b3c9329d 100644 --- a/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts +++ b/packages/daemon/src/sessions/__tests__/replay-maestro/session-replay-runtime-maestro.fixtures.ts @@ -45,7 +45,7 @@ import fs from 'node:fs'; import path from 'node:path'; import type { CommandFlags } from '@agent-device/contracts/command'; import type { DaemonInvokeFn, DaemonRequest } from '../../../core/daemon-request.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { SessionStore } from '../../../core/session-store.ts'; import { runReplayForTest } from '../replay-runtime/replay-command-fixture.ts'; import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay'; @@ -88,7 +88,7 @@ async function runReplayFixture(params: { fs.writeFileSync(scriptPath, params.script); const calls: CapturedInvocation[] = []; const invoke = createFixtureInvoke({ calls, delegate: params.invoke, isMaestro }); - const sessionStore = new SessionStore(path.join(root, 'state')); + const sessionStore = new SessionStore(path.join(root, 'state'), createLiveTestHostEnvironment()); seedReplayFixtureSession(sessionStore, params.sessionPlatform); const scriptSource = isMaestro ? await maestroScriptSourceBundleFor(scriptPath) diff --git a/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-acceptance.test.ts b/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-acceptance.test.ts index d2a5dc21f2..6ab7c92184 100644 --- a/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-acceptance.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-acceptance.test.ts @@ -8,7 +8,7 @@ import { storeSessionForTest } from '../../../__tests__/support/store-factory.ts */ import { test, expect, vi, beforeEach } from 'vitest'; import { markRepairTransactionComplete } from '../../../core/session-replay-transaction.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { const actual = @@ -75,7 +75,10 @@ const SAVE_ANNOTATION = test('a healed script survives repair + fresh-session replay: self-contained open, every selector step annotated, no bare @ref', async () => { const root = mkdtempForTestSync('agent-device-replay-repair-accept-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -155,7 +158,10 @@ test('a healed script survives repair + fresh-session replay: self-contained ope // (separate SessionStore, separate state dir — never reuses the repair // session). --- const freshRoot = mkdtempForTestSync('agent-device-replay-repair-fresh-'); - const freshSessionStore = new SessionStore(path.join(freshRoot, 'sessions')); + const freshSessionStore = new SessionStore( + path.join(freshRoot, 'sessions'), + createLiveTestHostEnvironment(), + ); const freshSessionName = 'fresh'; const invokedFresh: DaemonRequest[] = []; const freshReplay = await runReplayForTest({ diff --git a/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-empty-tail.test.ts b/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-empty-tail.test.ts index 89e56adbf4..f99d59d97a 100644 --- a/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-empty-tail.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-empty-tail.test.ts @@ -27,6 +27,7 @@ import { test, expect, vi, beforeEach } from 'vitest'; import { bottomTabsRealCaptureFixture, buildNodes, + createLiveTestHostEnvironment, mkdtempForTestSync, recordArticleEvidence, } from '@agent-device/test-kit'; @@ -85,7 +86,10 @@ beforeEach(() => { test('a record-and-heal divergence on the LAST step resumes with an empty tail and commits — but only after the corrective press is actually recorded', async () => { const root = mkdtempForTestSync('agent-device-replay-empty-tail-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -215,7 +219,10 @@ test('a record-and-heal divergence on the LAST step resumes with an empty tail a test('a manual divergence (unannotated action-failure) on the LAST step resumes with an empty tail and commits — but only after the corrective press is actually recorded', async () => { const root = mkdtempForTestSync('agent-device-replay-empty-tail-manual-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -332,7 +339,10 @@ test('a manual divergence (unannotated action-failure) on the LAST step resumes test('a caution (identity-mismatch) divergence on the LAST step resumes with an empty tail and commits — but only after the corrective press is actually recorded', async () => { const root = mkdtempForTestSync('agent-device-replay-empty-tail-caution-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -459,7 +469,10 @@ test('a caution (identity-mismatch) divergence on the LAST step resumes with an test('--from N stays legal for a caution divergence even after the N + 1 empty-tail watermark is stamped', async () => { const root = mkdtempForTestSync('agent-device-replay-empty-tail-caution-from-n-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -556,7 +569,10 @@ test('--from N stays legal for a caution divergence even after the N + 1 empty-t test('an unauthorized --from one past the plan end is rejected on an ARMED session whose last-step divergence hint is state-repair, not record-and-heal', async () => { const root = mkdtempForTestSync('agent-device-replay-empty-tail-state-repair-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -642,7 +658,10 @@ test('an unauthorized --from one past the plan end is rejected on an ARMED sessi test('a stale --plan-digest on an empty-tail resume is rejected WITHOUT consuming the watermark, so a subsequent correct retry still succeeds', async () => { const root = mkdtempForTestSync('agent-device-replay-empty-tail-digest-retry-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, diff --git a/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-loop.test.ts b/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-loop.test.ts index 19f976a4d5..d89ecb6796 100644 --- a/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-loop.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-loop.test.ts @@ -12,7 +12,7 @@ import { isSessionRecording } from '../../../core/session-script-publication-cap * `session.actions` accumulates for real instead of staying empty. */ import { test, expect, vi, beforeEach } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { const actual = @@ -82,7 +82,10 @@ const SAVE_ANNOTATION = function setup(prefix: string, sessionOverrides = {}) { const root = mkdtempForTestSync(prefix); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName, sessionOverrides)); return { root, sessionStore, sessionName, logPath: path.join(root, 'daemon.log') }; @@ -401,7 +404,10 @@ test('a --no-record state-fix action never enters session.actions', () => { test('R1 bootstrap: a session created by step 1 (open) arms in time for step 2 to get fresh evidence', async () => { const root = mkdtempForTestSync('agent-device-replay-repair-bootstrap-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; // No pre-existing session — `open` (step 1) creates it. const filePath = writeReplayFile(root, ['open "Demo"', 'click id="save"']); @@ -428,7 +434,10 @@ test('R1 bootstrap: a session created by step 1 (open) arms in time for step 2 t test('BLOCKER 4: a minimal [open, terminal close] cold-start script arms the transaction and skips the close', async () => { const root = mkdtempForTestSync('agent-device-replay-repair-open-close-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; // No pre-existing session — step 1 (`open`) CREATES it, so pre-open arming is // a no-op and the transaction can only arm once the session exists. diff --git a/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-record-exclusion.test.ts b/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-record-exclusion.test.ts index 14be8040ad..384badf705 100644 --- a/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-record-exclusion.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-record-exclusion.test.ts @@ -21,9 +21,10 @@ import { storeSessionForTest } from '../../../__tests__/support/store-factory.ts */ import { test, expect, vi, beforeEach } from 'vitest'; import { - mkdtempForTestSync, bindTestCommandHost, + createLiveTestHostEnvironment, createTestHostEnvironment, + mkdtempForTestSync, } from '@agent-device/test-kit'; vi.mock('@agent-device/platform-apple/runner/operations', () => ({ @@ -139,7 +140,10 @@ const SAVE_ANNOTATION = function setup(prefix: string) { const root = mkdtempForTestSync(prefix); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, diff --git a/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-transaction.fixtures.ts b/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-transaction.fixtures.ts index 30795c302c..dfffc12a86 100644 --- a/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-transaction.fixtures.ts +++ b/packages/daemon/src/sessions/__tests__/replay-repair/session-replay-repair-transaction.fixtures.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { SessionStore } from '../../../core/session-store.ts'; import { LeaseRegistry } from '../../../resources/lease-registry.ts'; import { @@ -30,7 +30,10 @@ export const SAVE_ANNOTATION = export function setup(prefix: string) { const root = mkdtempForTestSync(prefix); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, diff --git a/packages/daemon/src/sessions/__tests__/replay-runtime/application.test.ts b/packages/daemon/src/sessions/__tests__/replay-runtime/application.test.ts index 356231cd54..7e449ed460 100644 --- a/packages/daemon/src/sessions/__tests__/replay-runtime/application.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-runtime/application.test.ts @@ -9,7 +9,7 @@ import { registerRequestAbort, withRequestProgressSink, } from '@agent-device/host-kit/request'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { makeIosSession } from '../../../__tests__/support/session-factories.ts'; import { withClientReplayScriptSources } from '../../../__tests__/support/replay-script-source-fixture.ts'; import type { DaemonRequest } from '../../../core/daemon-request.ts'; @@ -102,7 +102,10 @@ test('replay routes native and Maestro sources through one application seam and for (const replayCase of cases) { const root = mkdtempForTestSync(`agent-device-replay-application-${replayCase.kind}-`); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish('default', makeIosSession('default')); const req = await replayCase.buildRequest(root); const invoked: DaemonRequest[] = []; @@ -130,7 +133,10 @@ test('replay routes native and Maestro sources through one application seam and test('replay admits an annotated target before dispatching its mutation', async () => { const root = mkdtempForTestSync('agent-device-replay-application-admission-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish('default', makeIosSession('default', { appBundleId: 'com.example.app' })); const filePath = writeReplayFile(root, [ '# agent-device:target-v1 {"id":"save","role":"button","label":"Save","ancestry":[],"sibling":0,"viewportOrder":0,"verification":"verified"}', @@ -168,7 +174,10 @@ test('replay admits an annotated target before dispatching its mutation', async test('replay projects the exact failure cause and retains artifacts from the failed step', async () => { const root = mkdtempForTestSync('agent-device-replay-application-failure-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish('default', makeIosSession('default')); const artifactPath = path.join(root, 'failure.png'); fs.writeFileSync(artifactPath, 'artifact'); @@ -216,7 +225,10 @@ test('replay projects the exact failure cause and retains artifacts from the fai test('test hosts native and Maestro sources format-neutrally and reports replay progress', async () => { const root = mkdtempForTestSync('agent-device-replay-application-suite-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const nativePath = path.join(root, '01-native.ad'); const maestroPath = path.join(root, '02-maestro.yaml'); fs.writeFileSync(nativePath, 'context platform=ios\nopen "Native"\n'); @@ -260,7 +272,10 @@ test('test hosts native and Maestro sources format-neutrally and reports replay test('test stops after an active cancellation and does not start the next replay', async () => { const root = mkdtempForTestSync('agent-device-replay-application-cancel-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const firstPath = writeReplayFile(root, ['open "First"']); const secondPath = path.join(root, 'second.ad'); fs.writeFileSync(secondPath, 'open "Second"\n'); diff --git a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-action-runtime.test.ts b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-action-runtime.test.ts index cd99c6cf67..a32f78eb89 100644 --- a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-action-runtime.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-action-runtime.test.ts @@ -6,6 +6,7 @@ import type { DaemonRequest } from '../../../core/daemon-request.ts'; import { invokeReplayAction } from '@agent-device/replay-port/session-replay-action-runtime'; import { replayDaemonDependencies } from '../../handlers/session-replay-command.ts'; import { resolveReplayAction } from '@agent-device/ad-script'; +import { createLiveTestHostEnvironment } from '@agent-device/test-kit'; const REPLAY_REQUEST: DaemonRequest = { token: 'token', @@ -42,12 +43,16 @@ test.each(['', ' '])( resolvedSessionScope: undefined, dependencies: replayDaemonDependencies, invoke: async (request) => { - recordActionEntry(session, { - command: request.command, - positionals: request.positionals ?? [], - flags: request.flags ?? {}, - result: { text: request.positionals?.at(-1) }, - }); + recordActionEntry( + session, + { + command: request.command, + positionals: request.positionals ?? [], + flags: request.flags ?? {}, + result: { text: request.positionals?.at(-1) }, + }, + createLiveTestHostEnvironment().cwd, + ); return { ok: true, data: {} }; }, }); diff --git a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-artifact-ledger.test.ts b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-artifact-ledger.test.ts index 77997e1ac5..5deba8dfdf 100644 --- a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-artifact-ledger.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-artifact-ledger.test.ts @@ -9,7 +9,7 @@ import { baseReplayRequest as baseReq, writeReplayFile, } from './session-replay-runtime.fixtures.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; /** * #1478 P5 follow-up (one daemon-owned artifact ledger): artifact-path @@ -29,7 +29,10 @@ import { mkdtempForTestSync } from '@agent-device/test-kit'; */ test('a mid-loop throw reports exactly the artifacts of the steps that ran before it', async () => { const root = mkdtempForTestSync('agent-device-replay-artifact-ledger-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -88,7 +91,10 @@ test('a mid-loop throw reports exactly the artifacts of the steps that ran befor */ test('a completed run reports every step’s artifacts through the threaded ledger value', async () => { const root = mkdtempForTestSync('agent-device-replay-artifact-ledger-ok-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, diff --git a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-binding.test.ts b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-binding.test.ts index 0540b12bf0..dee551d55c 100644 --- a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-binding.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-binding.test.ts @@ -8,7 +8,7 @@ import { SessionStore } from '../../../core/session-store.ts'; import { runReplayForTest } from './replay-command-fixture.ts'; import { captureSnapshotWithInteractor } from '../../../core/snapshot-interactor-capture.ts'; import { baseReplayRequest as baseReq } from './session-replay-runtime.fixtures.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { const actual = @@ -32,7 +32,10 @@ beforeEach(() => { test('typed Maestro does not resolve a device before an explicit-platform flow needs one', async () => { const root = mkdtempForTestSync('agent-device-maestro-explicit-platform-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, 'appId: com.example.app\n---\n- launchApp\n'); const invoked: string[] = []; @@ -58,7 +61,10 @@ test('typed Maestro does not resolve a device before an explicit-platform flow n test('typed Maestro keeps a port-only runtime digest stable after launch binds the device', async () => { const root = mkdtempForTestSync('agent-device-maestro-runtime-device-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, 'appId: com.example.app\n---\n- launchApp\n- back\n'); diff --git a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-failure-response.test.ts b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-failure-response.test.ts index fd4b5a155a..98eda5f099 100644 --- a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-failure-response.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-failure-response.test.ts @@ -1,5 +1,5 @@ import { test, expect, vi, beforeEach } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { const actual = @@ -35,7 +35,10 @@ beforeEach(() => { test('divergence cause and action strings pass through the central redactor at construction', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-redact-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['click "Save"']); @@ -69,7 +72,10 @@ test('a fill divergence never serializes the typed text at any response level', const sentinel = 'SuperSecretPassword-do-not-leak-12345'; for (const level of ['digest', 'default', 'full'] as const) { const root = mkdtempForTestSync('agent-device-replay-fill-leak-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -126,7 +132,10 @@ test('a fill divergence never serializes the typed text at any response level', test('a capture-failed screen hint redacts a secret in the capture error', async () => { const root = mkdtempForTestSync('agent-device-replay-screen-redact-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -163,7 +172,10 @@ test('a capture-failed screen hint redacts a secret in the capture error', async test('an expanded ${VAR} value echoed by a selector error never reaches the public divergence', async () => { const sentinel = 'ExpandedVarSecret-98765-do-not-leak'; const root = mkdtempForTestSync('agent-device-replay-var-leak-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -211,7 +223,10 @@ test('an expanded built-in AD_DEVICE_ID never reaches the public divergence', as const deviceId = 'BuiltInDeviceId-486b3d4c-8f92-4dc0-b5c6-unique'; const sessionName = 'static-session-context'; const root = mkdtempForTestSync('agent-device-replay-builtin-var-leak-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish( sessionName, makeIosSession(sessionName, { appBundleId: 'com.example.app' }), diff --git a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-failure.test.ts b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-failure.test.ts index ace9f47e95..1c1bd76a76 100644 --- a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-failure.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-failure.test.ts @@ -1,6 +1,6 @@ import type { DaemonResponse } from '@agent-device/kernel/contracts'; import { test, expect, vi, beforeEach } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { const actual = @@ -38,7 +38,10 @@ beforeEach(() => { }); test('a failing replay step returns REPLAY_DIVERGENCE with cause preserved and correct step provenance', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -121,7 +124,10 @@ test('a failing replay step returns REPLAY_DIVERGENCE with cause preserved and c test('#1262: a LAST-step failure with NO active session carries NO empty-tail alternateFrom and never advertises --from length+1 in text', async () => { const root = mkdtempForTestSync('agent-device-replay-no-session-tail-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; // NO session is pre-seeded — a single-step plan whose only (LAST) step fails // before any session exists (mirrors a one-step `open` failure, or a session @@ -167,7 +173,10 @@ test('#1262: a LAST-step failure with NO active session carries NO empty-tail al test('a normalized nested failure preserves typed recovery signals on REPLAY_DIVERGENCE', async () => { const root = mkdtempForTestSync('agent-device-replay-recovery-signals-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -205,7 +214,10 @@ test('a normalized nested failure preserves typed recovery signals on REPLAY_DIV test('a capture the runner declared sparse reaches the divergence details as a verdict', async () => { const root = mkdtempForTestSync('agent-device-replay-sparse-verdict-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -250,7 +262,10 @@ test('a capture the runner declared sparse reaches the divergence details as a v test('a failing replay step captures an available screen digest with blessed refs', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-screen-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -300,7 +315,10 @@ test('a failing replay step captures an available screen digest with blessed ref test('a failing replay step ranks a re-resolved suggestion when the recorded selector still matches', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-suggest-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -354,7 +372,10 @@ test('a failing replay step ranks a re-resolved suggestion when the recorded sel test('divergence screen never masks the original cause when the session already closed', async () => { const root = mkdtempForTestSync('agent-device-replay-divergence-no-session-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; // Intentionally no session stored: simulates the session closing mid-replay. const filePath = writeReplayFile(root, ['click "Save"']); @@ -400,7 +421,10 @@ function writeMaestroInclude(root: string): string { test('a failure inside a retry-wrapped runFlow include reports the include file and line', async () => { const root = mkdtempForTestSync('agent-device-replay-retry-provenance-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const childPath = writeMaestroInclude(root); @@ -457,7 +481,10 @@ test('a failure inside a retry-wrapped runFlow include reports the include file test('a failure inside a runtime runFlow.when-wrapped include reports the include file and line', async () => { const root = mkdtempForTestSync('agent-device-replay-when-provenance-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const childPath = writeMaestroInclude(root); @@ -513,7 +540,10 @@ test('a failure inside a runtime runFlow.when-wrapped include reports the includ test('typed Maestro failures rank suggestions with Maestro regex selector semantics', async () => { const root = mkdtempForTestSync('agent-device-maestro-suggestion-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); @@ -569,7 +599,10 @@ test('typed Maestro failures rank suggestions with Maestro regex selector semant test('typed Maestro failures never serialize input text', async () => { const root = mkdtempForTestSync('agent-device-maestro-text-redaction-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); diff --git a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-keep-session.test.ts b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-keep-session.test.ts index ff5a1a2d5d..6d0bde6da8 100644 --- a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-keep-session.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-keep-session.test.ts @@ -1,5 +1,5 @@ import { test, expect, vi } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; /** * #1555 structural-quality review ("topology fix... its subject now lives @@ -51,7 +51,10 @@ import { test('--keep-session suppresses a close that is terminal among executable actions', async () => { const root = mkdtempForTestSync('agent-device-replay-keep-marker-tail-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'close', 'replay "./nested-flow.ad"']); @@ -77,7 +80,10 @@ test('--keep-session suppresses a close that is terminal among executable action test('--keep-session fails explicitly when the completed replay has no live session', async () => { const root = mkdtempForTestSync('agent-device-replay-keep-postcondition-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Log out"']); @@ -104,7 +110,10 @@ test('--keep-session fails explicitly when the completed replay has no live sess test('--keep-session suppresses only the authored terminal close and reports the surviving session', async () => { const root = mkdtempForTestSync('agent-device-replay-keep-session-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"', 'close']); @@ -131,7 +140,10 @@ test('--keep-session suppresses only the authored terminal close and reports the test('--keep-session preserves an interior close instead of broad command filtering', async () => { const root = mkdtempForTestSync('agent-device-replay-keep-interior-close-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'close', 'open "Next"']); @@ -154,7 +166,10 @@ test('--keep-session preserves an interior close instead of broad command filter test('--keep-session is a no-op for an already close-less script', async () => { const root = mkdtempForTestSync('agent-device-replay-keep-close-less-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); @@ -176,7 +191,10 @@ test('--keep-session is a no-op for an already close-less script', async () => { test('--keep-session rejects Maestro YAML before engine dispatch', async () => { const root = mkdtempForTestSync('agent-device-replay-keep-maestro-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = path.join(root, 'flow.yaml'); diff --git a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-plan.test.ts b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-plan.test.ts index 69c4625ecb..9f5bb95001 100644 --- a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-plan.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime-plan.test.ts @@ -1,5 +1,5 @@ import { test, expect, vi, beforeEach } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { const actual = @@ -45,7 +45,10 @@ beforeEach(() => { }); test('resume skips steps 1..from-1 without invoking them and executes only from the reported step', async () => { const root = mkdtempForTestSync('agent-device-replay-resume-skip-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, [ @@ -104,7 +107,10 @@ test('resume skips steps 1..from-1 without invoking them and executes only from test('resume requires both --from and --plan-digest together', async () => { const root = mkdtempForTestSync('agent-device-replay-resume-pair-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); @@ -136,7 +142,10 @@ test('resume requires both --from and --plan-digest together', async () => { test('resume rejects an out-of-range --from before any action', async () => { const root = mkdtempForTestSync('agent-device-replay-resume-range-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); @@ -171,7 +180,10 @@ test('resume rejects an out-of-range --from before any action', async () => { */ test("a rejected --from/--plan-digest resume never reaches prepareReplaySession's coordinator-mutating writes", async () => { const root = mkdtempForTestSync('agent-device-replay-resume-no-mutate-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Continue"', 'click "Save"']); @@ -220,7 +232,10 @@ test("a rejected --from/--plan-digest resume never reaches prepareReplaySession' test('resume rejects a stale --plan-digest after the script changed', async () => { const root = mkdtempForTestSync('agent-device-replay-resume-stale-digest-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); @@ -267,7 +282,10 @@ test('resume rejects a stale --plan-digest after the script changed', async () = test('resume rejects a digest from a different effective platform or target before any action', async () => { const root = mkdtempForTestSync('agent-device-replay-resume-effective-target-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, [ @@ -323,7 +341,10 @@ test('resume rejects a digest from a different effective platform or target befo }); test('resume rejects resuming past a retry-wrapped step in the skipped range', async () => { const root = mkdtempForTestSync('agent-device-replay-resume-control-flow-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const mainPath = path.join(root, 'main.yaml'); @@ -392,7 +413,10 @@ test('resume rejects resuming past a retry-wrapped step in the skipped range', a test('typed Maestro resume digest binds an inferred session target', async () => { const root = mkdtempForTestSync('agent-device-maestro-session-target-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const session = makeIosSession(sessionName); sessionStore.publish(sessionName, session); @@ -443,7 +467,10 @@ test('typed Maestro resume digest binds an inferred session target', async () => test('typed Maestro rejects selectors that conflict with an active session', async () => { const root = mkdtempForTestSync('agent-device-maestro-session-conflict-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const flowPath = path.join(root, 'flow.yaml'); @@ -470,7 +497,10 @@ test('typed Maestro rejects selectors that conflict with an active session', asy test('fresh typed Maestro replay resolves its configured app before runtime defaults', async () => { const root = mkdtempForTestSync('agent-device-maestro-app-selection-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, 'appId: com.example.demo\n---\n- launchApp\n'); const genericDevice = { ...makeIosSession('generic').device, id: 'SIM-GENERIC' }; @@ -507,7 +537,10 @@ test('fresh typed Maestro replay resolves its configured app before runtime defa test('native replay applies an authored Android platform to its static app open', async () => { const root = mkdtempForTestSync('agent-device-native-android-selection-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const replayPath = path.join(root, 'flow.ad'); fs.writeFileSync( replayPath, @@ -535,7 +568,10 @@ test('native replay applies an authored Android platform to its static app open' test('platform-less typed Maestro replay preserves a resolved Android device', async () => { const root = mkdtempForTestSync('agent-device-maestro-android-selection-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, 'appId: com.example.demo\n---\n- launchApp\n'); const androidDevice = { ...makeAndroidSession('android').device, id: 'ANDROID-EMULATOR' }; @@ -569,7 +605,10 @@ test('platform-less typed Maestro replay preserves a resolved Android device', a test('typed Maestro resume digest binds effective stored runtime hints', async () => { const root = mkdtempForTestSync('agent-device-maestro-session-runtime-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); sessionStore.setRuntimeHints(sessionName, { diff --git a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime.test.ts b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime.test.ts index 069d023075..dd927b80cf 100644 --- a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-runtime.test.ts @@ -1,6 +1,6 @@ import { isSessionRecording } from '../../../core/session-script-publication-capability.ts'; import { test, expect, vi, beforeEach } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { const actual = @@ -41,7 +41,10 @@ beforeEach(() => { }); test('a successful replay prints one line with the step count and wall time', async () => { const root = mkdtempForTestSync('agent-device-replay-success-message-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); @@ -70,7 +73,10 @@ test('a successful replay prints one line with the step count and wall time', as test('a close-less replay reports sessionActive: true (real producer, session still in the store)', async () => { const root = mkdtempForTestSync('agent-device-replay-session-active-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); @@ -91,7 +97,10 @@ test('a close-less replay reports sessionActive: true (real producer, session st test('a replay whose terminal close removes the session reports sessionActive: false', async () => { const root = mkdtempForTestSync('agent-device-replay-session-closed-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'close']); @@ -118,7 +127,10 @@ test('a replay whose terminal close removes the session reports sessionActive: f test('Maestro YAML uses the typed engine while .ad remains generic', async () => { const root = mkdtempForTestSync('agent-device-typed-maestro-route-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); @@ -167,7 +179,10 @@ test('Maestro YAML uses the typed engine while .ad remains generic', async () => test('bare Maestro YAML requires explicit --maestro routing', async () => { const root = mkdtempForTestSync('agent-device-maestro-explicit-route-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); @@ -192,7 +207,10 @@ test('bare Maestro YAML requires explicit --maestro routing', async () => { test('ADR 0016 / #1384: a Maestro replay reports sessionActive: true (real producer, no fake HTTP)', async () => { const root = mkdtempForTestSync('agent-device-maestro-session-active-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); @@ -217,7 +235,10 @@ test('ADR 0016 / #1384: a Maestro replay reports sessionActive: true (real produ test('typed Maestro nested commands receive the runtime hints bound into the plan', async () => { const root = mkdtempForTestSync('agent-device-maestro-runtime-envelope-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const session = makeIosSession(sessionName); sessionStore.publish(sessionName, { @@ -267,7 +288,10 @@ test('typed Maestro nested commands receive the runtime hints bound into the pla test('typed Maestro writes source-aware redacted step timing traces', async () => { const root = mkdtempForTestSync('agent-device-maestro-step-trace-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); @@ -327,7 +351,10 @@ test('typed Maestro writes source-aware redacted step timing traces', async () = test('replay trace failures do not change action semantics', async () => { const root = mkdtempForTestSync('agent-device-replay-trace-failure-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); @@ -350,7 +377,10 @@ test('replay trace failures do not change action semantics', async () => { test('generic replay traces redact typed text', async () => { const root = mkdtempForTestSync('agent-device-replay-trace-redaction-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const secret = 'highly-sensitive-value'; @@ -376,7 +406,10 @@ test('generic replay traces redact typed text', async () => { test('Maestro YAML rejects .ad repair recording before executing any command', async () => { const root = mkdtempForTestSync('agent-device-typed-maestro-save-script-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const yamlPath = path.join(root, 'flow.yaml'); @@ -404,7 +437,10 @@ test('Maestro YAML rejects .ad repair recording before executing any command', a test('Maestro YAML cannot append commands to an active .ad repair session', async () => { const root = mkdtempForTestSync('agent-device-typed-maestro-active-repair-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const session = makeRepairArmedSession(sessionName); sessionStore.publish(sessionName, session); @@ -431,7 +467,10 @@ test('Maestro YAML cannot append commands to an active .ad repair session', asyn }); test('replay rejects legacy JSON payload files', async () => { const root = mkdtempForTestSync('agent-device-replay-json-rejected-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = path.join(root, 'replay.json'); @@ -464,7 +503,10 @@ test('replay rejects legacy JSON payload files', async () => { // before `inspectAdReplay` runs. test('replay rejects an unknown --replay-backend value before any step dispatch (#1555 P1)', async () => { const root = mkdtempForTestSync('agent-device-replay-unknown-backend-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); @@ -493,7 +535,10 @@ test('replay rejects an unknown --replay-backend value before any step dispatch // the restored check does not overreject the accepted value. test('replay still dispatches a plain .ad script with replayBackend: "maestro"', async () => { const root = mkdtempForTestSync('agent-device-replay-ad-maestro-backend-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); @@ -515,7 +560,10 @@ test('replay still dispatches a plain .ad script with replayBackend: "maestro"', test('replay rejects malformed .ad lines with unclosed quotes', async () => { const root = mkdtempForTestSync('agent-device-replay-invalid-ad-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, [String.raw`click "id=\"broken\"`]); @@ -538,7 +586,10 @@ test('replay rejects malformed .ad lines with unclosed quotes', async () => { test('--update never rewrites the .ad file, even when a re-resolvable suggestion exists', async () => { const root = mkdtempForTestSync('agent-device-replay-update-no-write-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, @@ -591,7 +642,10 @@ test('--update never rewrites the .ad file, even when a re-resolvable suggestion test('a successful --update replay reports healed: 0 (heal is retired, not just quiet)', async () => { const root = mkdtempForTestSync('agent-device-replay-update-healed-zero-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['open "Demo"', 'click "Save"']); @@ -612,7 +666,10 @@ test('a successful --update replay reports healed: 0 (heal is retired, not just test('--update no longer refuses env directives (the guard existed only for rewrite safety)', async () => { const root = mkdtempForTestSync('agent-device-replay-update-env-ok-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['env NAME=World', 'open "Demo"']); @@ -630,7 +687,10 @@ test('--update no longer refuses env directives (the guard existed only for rewr test('--update no longer refuses ${VAR} interpolation (the guard existed only for rewrite safety)', async () => { const root = mkdtempForTestSync('agent-device-replay-update-interp-ok-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, ['click label="${NAME}"']); diff --git a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-script-source.test.ts b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-script-source.test.ts index 5d72303d19..57103223af 100644 --- a/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-script-source.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-runtime/session-replay-script-source.test.ts @@ -6,7 +6,7 @@ * path is refused with a message that says so. */ import { beforeEach, expect, test, vi } from 'vitest'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; vi.mock('@agent-device/device-selection/dispatch-resolve', async (importOriginal) => { const actual = @@ -44,7 +44,10 @@ beforeEach(() => { test('a bundled replay runs after the script file is gone from the daemon host', async () => { const root = mkdtempForTestSync('agent-device-replay-source-bundle-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish('default', makeIosSession('default')); const scriptPath = path.join(root, 'flow.ad'); fs.writeFileSync(scriptPath, 'open "Demo"\nclick "Save"\n'); @@ -74,7 +77,10 @@ test('a bundled replay runs after the script file is gone from the daemon host', test('a bundled Maestro replay resolves runFlow includes from the bundle, not the daemon disk', async () => { const root = mkdtempForTestSync('agent-device-replay-maestro-bundle-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish('default', makeIosSession('default')); const flowPath = path.join(root, 'flow.yaml'); fs.writeFileSync(flowPath, 'appId: com.example.app\n---\n- runFlow: included.yaml\n'); @@ -106,7 +112,10 @@ test('a bundled Maestro replay resolves runFlow includes from the bundle, not th test('a Maestro include missing from the bundle fails naming the include path', async () => { const root = mkdtempForTestSync('agent-device-replay-maestro-missing-include-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish('default', makeIosSession('default')); const flowPath = path.join(root, 'flow.yaml'); const flow = 'appId: com.example.app\n---\n- runFlow: absent.yaml\n'; @@ -140,7 +149,10 @@ test('a Maestro include missing from the bundle fails naming the include path', // being served an ENOENT for a file that exists on the machine it ran on. test('a request carrying only a path is refused with the client-must-send-sources message', async () => { const root = mkdtempForTestSync('agent-device-replay-path-only-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish('default', makeIosSession('default')); const scriptPath = path.join(root, 'flow.ad'); fs.writeFileSync(scriptPath, 'open "Demo"\n'); @@ -170,7 +182,10 @@ test('a request carrying only a path is refused with the client-must-send-source // produces still points at the script the operator ran — path and line alike. test('a failing step reports the caller-resolved script path and line', async () => { const root = mkdtempForTestSync('agent-device-replay-source-display-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish('default', makeIosSession('default')); const scriptPath = path.join(root, 'flow.ad'); fs.writeFileSync(scriptPath, 'open "Demo"\nclick "Nope"\n'); diff --git a/packages/daemon/src/sessions/__tests__/replay-suite/session-test-command-trust.test.ts b/packages/daemon/src/sessions/__tests__/replay-suite/session-test-command-trust.test.ts index f085dbb5ee..0c514be787 100644 --- a/packages/daemon/src/sessions/__tests__/replay-suite/session-test-command-trust.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-suite/session-test-command-trust.test.ts @@ -13,7 +13,7 @@ import { import { runReplayTestCommand } from '@agent-device/replay-port/test-command'; import type { ReplayCommand, ReplayTestCommand } from '@agent-device/replay-port/command-types'; import { replayScriptSourceBundleFor } from '../../../__tests__/support/replay-script-source-fixture.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import type { DaemonInvokeFn, DaemonRequest } from '../../../core/daemon-request.ts'; const capturedCommands: ReplayCommand[] = []; @@ -33,7 +33,10 @@ async function forwardedCommands(publicNetworkOnly?: true): Promise { }); function setupSession(root: string): { sessionStore: SessionStore; sessionName: string } { - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish( sessionName, diff --git a/packages/daemon/src/sessions/__tests__/replay-target/session-replay-scenario.fixtures.ts b/packages/daemon/src/sessions/__tests__/replay-target/session-replay-scenario.fixtures.ts index f8a3626575..84fed0b199 100644 --- a/packages/daemon/src/sessions/__tests__/replay-target/session-replay-scenario.fixtures.ts +++ b/packages/daemon/src/sessions/__tests__/replay-target/session-replay-scenario.fixtures.ts @@ -1,6 +1,6 @@ import type { DaemonResponse } from '@agent-device/kernel/contracts'; import path from 'node:path'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { makeIosAppSession } from '../../../__tests__/support/session-factories.ts'; import { SessionStore } from '../../../core/session-store.ts'; import type { DaemonInvokeFn, DaemonRequest } from '../../../core/daemon-request.ts'; @@ -20,7 +20,10 @@ type ReplaySessionScene = Readonly<{ function iosReplayScene(prefix: string): ReplaySessionScene { const root = mkdtempForTestSync(prefix); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; sessionStore.publish(sessionName, makeIosAppSession(sessionName)); return { root, sessionStore, sessionName, logPath: path.join(root, 'daemon.log') }; diff --git a/packages/daemon/src/sessions/__tests__/replay-target/session-replay-selector-routes.test.ts b/packages/daemon/src/sessions/__tests__/replay-target/session-replay-selector-routes.test.ts index 5e38bd2dca..e62a262404 100644 --- a/packages/daemon/src/sessions/__tests__/replay-target/session-replay-selector-routes.test.ts +++ b/packages/daemon/src/sessions/__tests__/replay-target/session-replay-selector-routes.test.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { beforeEach, expect, test, vi } from 'vitest'; import { makeIosSession } from '../../../__tests__/support/session-factories.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { SessionStore } from '../../../core/session-store.ts'; import { runReplayForTest } from '../replay-runtime/replay-command-fixture.ts'; import { @@ -38,7 +38,10 @@ beforeEach(() => { test('replay executes selector reads before reporting a covered-target divergence', async () => { const root = mkdtempForTestSync('agent-device-replay-selector-routes-'); const sessionName = 'default'; - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish(sessionName, makeIosSession(sessionName)); const filePath = writeReplayFile(root, [ 'open "Demo"', diff --git a/packages/daemon/src/sessions/__tests__/session-routing.test.ts b/packages/daemon/src/sessions/__tests__/session-routing.test.ts index ea894f5e53..6f7ea4443b 100644 --- a/packages/daemon/src/sessions/__tests__/session-routing.test.ts +++ b/packages/daemon/src/sessions/__tests__/session-routing.test.ts @@ -9,7 +9,7 @@ import { resolveEffectiveSessionName, resolveSessionScope } from '../session-rou import type { DaemonRequest } from '../../core/daemon-request.ts'; import type { SessionState } from '../../core/session-state.ts'; import type { SessionScope } from '@agent-device/contracts/session'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; function makeSession(name: string): SessionState { return { @@ -31,7 +31,7 @@ function makeStore(t: TestContext): SessionStore { t.onTestFinished(() => { fs.rmSync(root, { recursive: true, force: true }); }); - return new SessionStore(path.join(root, 'sessions')); + return new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); } test('does not reuse lone active session for implicit default session from another scope', (t) => { diff --git a/packages/daemon/src/sessions/__tests__/session-teardown-budget.test.ts b/packages/daemon/src/sessions/__tests__/session-teardown-budget.test.ts index 071f543d98..6d17d8c686 100644 --- a/packages/daemon/src/sessions/__tests__/session-teardown-budget.test.ts +++ b/packages/daemon/src/sessions/__tests__/session-teardown-budget.test.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import { expect, test } from 'vitest'; import { AGENT_BROWSER_TIMEOUT_MS } from '@agent-device/platform-web'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { makeAndroidSession } from '../../__tests__/support/session-factories.ts'; import { SessionStore } from '../../core/session-store.ts'; import { @@ -16,7 +16,10 @@ import { test('daemon session teardown budget extends for an active durable recording', () => { const root = mkdtempForTestSync('agent-device-shutdown-recording-budget-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const session = makeRecordingSession({ name: 'budget-session', sessionStore, diff --git a/packages/daemon/src/sessions/handlers/__tests__/install-source.test.ts b/packages/daemon/src/sessions/handlers/__tests__/install-source.test.ts index 186e6f6547..baf31e5b4b 100644 --- a/packages/daemon/src/sessions/handlers/__tests__/install-source.test.ts +++ b/packages/daemon/src/sessions/handlers/__tests__/install-source.test.ts @@ -27,6 +27,7 @@ import { touchRuntimeOperationFacts } from '@agent-device/contracts/touch-runtim import type { DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { + createLiveTestHostEnvironment, mkdtempForTestSync, unavailableApplicationLifecycleOperationFacts, } from '@agent-device/test-kit'; @@ -48,6 +49,7 @@ type SourceRuntimeOptions = Readonly<{ function makeStore(): SessionStore { return new SessionStore( path.join(mkdtempForTestSync('agent-device-install-source-'), 'sessions'), + createLiveTestHostEnvironment(), ); } diff --git a/packages/daemon/src/sessions/handlers/__tests__/session-boot-shutdown-device-claims.test.ts b/packages/daemon/src/sessions/handlers/__tests__/session-boot-shutdown-device-claims.test.ts index a488664e70..3fe7fa23d1 100644 --- a/packages/daemon/src/sessions/handlers/__tests__/session-boot-shutdown-device-claims.test.ts +++ b/packages/daemon/src/sessions/handlers/__tests__/session-boot-shutdown-device-claims.test.ts @@ -15,8 +15,9 @@ import { import { normalizeError } from '@agent-device/kernel/errors'; import { ANDROID_EMULATOR, - createTestHostEnvironment, bindTestCommandHost, + createLiveTestHostEnvironment, + createTestHostEnvironment, } from '@agent-device/test-kit'; import { isolatedDeviceClaimStores, @@ -46,7 +47,7 @@ const setup = isolatedDeviceClaimStores('agent-device-boot-shutdown-claim-'); const OWNER_STATE_DIR_NAME = 'daemon-a'; function daemonB(stateDir: string): SessionStore { - return new SessionStore(path.join(stateDir, 'sessions')); + return new SessionStore(path.join(stateDir, 'sessions'), createLiveTestHostEnvironment()); } async function claimForeignDaemon(root: string): Promise { diff --git a/packages/daemon/src/sessions/handlers/__tests__/session-device-claims.test.ts b/packages/daemon/src/sessions/handlers/__tests__/session-device-claims.test.ts index e08bd1ef5b..5bdbfbbadf 100644 --- a/packages/daemon/src/sessions/handlers/__tests__/session-device-claims.test.ts +++ b/packages/daemon/src/sessions/handlers/__tests__/session-device-claims.test.ts @@ -6,10 +6,11 @@ import { afterEach, test, vi } from 'vitest'; import * as hostProcess from '@agent-device/host-kit/process'; import { MACOS_DEVICE, - mkdtempForTestSync, - pinOwnProcessStartTime, bindTestCommandHost, + createLiveTestHostEnvironment, createTestHostEnvironment, + mkdtempForTestSync, + pinOwnProcessStartTime, } from '@agent-device/test-kit'; import { fakeCreateCommandSurface } from '../../../__tests__/support/command-surface-fake.ts'; @@ -123,7 +124,10 @@ function setup(): { store: SessionStore; stateDir: string } { const stateDir = mkdtempForTestSync('agent-device-session-device-claim-'); const claimsDir = path.join(stateDir, 'claims'); process.env.AGENT_DEVICE_CLAIMS_DIR = claimsDir; - return { store: new SessionStore(path.join(stateDir, 'sessions')), stateDir }; + return { + store: new SessionStore(path.join(stateDir, 'sessions'), createLiveTestHostEnvironment()), + stateDir, + }; } const android: DeviceInfo = { diff --git a/packages/daemon/src/sessions/handlers/__tests__/session-doctor-device.test.ts b/packages/daemon/src/sessions/handlers/__tests__/session-doctor-device.test.ts index d268913a14..ce85f76b16 100644 --- a/packages/daemon/src/sessions/handlers/__tests__/session-doctor-device.test.ts +++ b/packages/daemon/src/sessions/handlers/__tests__/session-doctor-device.test.ts @@ -48,7 +48,7 @@ test('doctor android inventory timeout surfaces the wedged-adb-server hint', asy return []; }, async () => { - await appendDeviceInventoryCheck(checks, req, undefined); + await appendDeviceInventoryCheck(checks, req, undefined, false); }, ); @@ -82,7 +82,7 @@ test('doctor inventory names as host devices only the platforms this host discov }, local: async () => [androidEmulator], }, - async () => await appendDeviceInventoryCheck([], req, undefined), + async () => await appendDeviceInventoryCheck([], req, undefined, false), ); assert.deepEqual( diff --git a/packages/daemon/src/sessions/handlers/__tests__/session-reinstall.test.ts b/packages/daemon/src/sessions/handlers/__tests__/session-reinstall.test.ts index cad9c2019e..4ab1468549 100644 --- a/packages/daemon/src/sessions/handlers/__tests__/session-reinstall.test.ts +++ b/packages/daemon/src/sessions/handlers/__tests__/session-reinstall.test.ts @@ -4,9 +4,10 @@ import fs from 'node:fs'; import path from 'node:path'; import { beforeEach, expect, test, vi } from 'vitest'; import { - mkdtempForTestSync, bindTestCommandHost, + createLiveTestHostEnvironment, createTestHostEnvironment, + mkdtempForTestSync, } from '@agent-device/test-kit'; import { trackUploadedArtifact } from '../../../resources/artifact-tracking.ts'; import type { @@ -37,6 +38,7 @@ bindTestCommandHost(); function makeStore(): SessionStore { return new SessionStore( path.join(mkdtempForTestSync('agent-device-session-app-deploy-'), 'sessions'), + createLiveTestHostEnvironment(), ); } diff --git a/packages/daemon/src/sessions/handlers/__tests__/session-script-publication.test.ts b/packages/daemon/src/sessions/handlers/__tests__/session-script-publication.test.ts index 4615c9f057..c21f8f87b8 100644 --- a/packages/daemon/src/sessions/handlers/__tests__/session-script-publication.test.ts +++ b/packages/daemon/src/sessions/handlers/__tests__/session-script-publication.test.ts @@ -19,7 +19,7 @@ import { scriptTargetPath, scriptTargetForce, } from '../../../core/session-script-publication-state.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; const TARGET_EVIDENCE: TargetAnnotationV1 = { id: 'continue', @@ -47,7 +47,7 @@ let store: SessionStore; beforeEach(() => { root = mkdtempForTestSync('agent-device-active-publication-'); - store = new SessionStore(path.join(root, 'sessions')); + store = new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); }); function armedSession(overrides: Partial = {}): SessionState { diff --git a/packages/daemon/src/sessions/handlers/__tests__/trace-runtime.test.ts b/packages/daemon/src/sessions/handlers/__tests__/trace-runtime.test.ts index fcc87825cf..c2bdb8fe8f 100644 --- a/packages/daemon/src/sessions/handlers/__tests__/trace-runtime.test.ts +++ b/packages/daemon/src/sessions/handlers/__tests__/trace-runtime.test.ts @@ -2,9 +2,10 @@ import fs from 'node:fs'; import path from 'node:path'; import { expect, test } from 'vitest'; import { - mkdtempForTestSync, bindTestCommandHost, + createLiveTestHostEnvironment, createTestHostEnvironment, + mkdtempForTestSync, } from '@agent-device/test-kit'; import { SessionStore } from '../../../core/session-store.ts'; import type { DaemonRequest } from '../../../core/daemon-request.ts'; @@ -15,7 +16,10 @@ bindTestCommandHost(); function fixture() { const root = mkdtempForTestSync('agent-device-trace-runtime-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const session: SessionState = { name: 'trace-session', device: { diff --git a/packages/daemon/src/sessions/handlers/lease.ts b/packages/daemon/src/sessions/handlers/lease.ts index 672f5b9e69..2b7cc4f8fd 100644 --- a/packages/daemon/src/sessions/handlers/lease.ts +++ b/packages/daemon/src/sessions/handlers/lease.ts @@ -81,6 +81,7 @@ export async function handleLeaseCommands(args: LeaseHandlerArgs): Promise, }; } @@ -404,10 +405,11 @@ async function listArtifactsForRequest( leaseScope: ReturnType, leaseRegistry: LeaseRegistry, cloudArtifactProvider: CloudArtifactProvider | undefined, + hostEnv: NodeJS.ProcessEnv, ): Promise { const providerSessionId = readFlagString(req.flags, 'providerSessionId'); if (shouldListDaemonArtifacts(leaseScope, providerSessionId)) { - return await listDaemonArtifacts(leaseScope.tenantId); + return await listDaemonArtifacts(leaseScope.tenantId, hostEnv); } return await listCloudArtifactsForRequest( @@ -425,8 +427,11 @@ function shouldListDaemonArtifacts( return isProxyLeaseScope(leaseScope) || (!leaseScope.leaseProvider && !providerSessionId); } -async function listDaemonArtifacts(tenantId: string | undefined): Promise { - const artifacts = await listDownloadableArtifacts(tenantId); +async function listDaemonArtifacts( + tenantId: string | undefined, + hostEnv: NodeJS.ProcessEnv, +): Promise { + const artifacts = await listDownloadableArtifacts(tenantId, hostEnv); return { source: 'daemon', status: 'ready', diff --git a/packages/daemon/src/sessions/handlers/record-runtime.ts b/packages/daemon/src/sessions/handlers/record-runtime.ts index 10bceed2bf..63b0fcb16f 100644 --- a/packages/daemon/src/sessions/handlers/record-runtime.ts +++ b/packages/daemon/src/sessions/handlers/record-runtime.ts @@ -292,13 +292,21 @@ function prepareRecordingStart(params: RecordRuntimeHandlerParams, session: Sess resourcePath, device: session.device, }), - outputPaths: resolveRecordingStartPaths(params.req, session.device.platform), + outputPaths: resolveRecordingStartPaths( + params.req, + session.device.platform, + params.sessionStore.host.cwd, + ), }; } type RecordingStartPaths = RecordingOutputPaths & Readonly<{ clientOutputPath?: string }>; -function resolveRecordingStartPaths(req: DaemonRequest, platform: Platform): RecordingStartPaths { +function resolveRecordingStartPaths( + req: DaemonRequest, + platform: Platform, + hostCwd: string, +): RecordingStartPaths { const requestedPath = req.positionals?.[1]; const clientOutputPath = req.meta?.clientArtifactPaths?.outPath; if (requestedPath === undefined && clientOutputPath !== undefined) { @@ -309,7 +317,7 @@ function resolveRecordingStartPaths(req: DaemonRequest, platform: Platform): Rec ...resolveRecordingOutputPaths({ requestedPath: buildRemoteTempArtifactPath('recording', extension), platform, - cwd: process.cwd(), + cwd: hostCwd, }), clientOutputPath: appendRecordingExtensionWhenMissing(clientOutputPath, extension), }; @@ -318,7 +326,7 @@ function resolveRecordingStartPaths(req: DaemonRequest, platform: Platform): Rec ...resolveRecordingOutputPaths({ requestedPath, platform, - cwd: req.meta?.cwd ?? process.cwd(), + cwd: req.meta?.cwd ?? hostCwd, }), ...(clientOutputPath === undefined ? {} : { clientOutputPath }), }; diff --git a/packages/daemon/src/sessions/handlers/session-doctor-app.ts b/packages/daemon/src/sessions/handlers/session-doctor-app.ts index c7b1a6a5f0..feedf6762d 100644 --- a/packages/daemon/src/sessions/handlers/session-doctor-app.ts +++ b/packages/daemon/src/sessions/handlers/session-doctor-app.ts @@ -16,9 +16,10 @@ export async function appendAppChecks( session: SessionState | undefined; targetApp?: string; listInstalledApps?: DoctorAppInventory; + useColor: boolean; }, ): Promise { - const { device, targetApp, session, listInstalledApps } = params; + const { device, targetApp, session, listInstalledApps, useColor } = params; if (!targetApp) { return; } @@ -28,31 +29,43 @@ export async function appendAppChecks( ? resolveUniqueInstalledAppMatch(targetApp, await listInstalledApps('all'))?.id : undefined; if (!resolved) { - appendDoctorCheck(checks, { - id: 'target-app', - status: 'info', - // approach (b): emit the PUBLIC leaf platform (ios/macos), never the internal `apple`. - summary: `Target app installation checks are not supported for ${publicPlatformString(device)}.`, - evidence: { requested: targetApp, platform: publicPlatformString(device) }, - }); + appendDoctorCheck( + checks, + { + id: 'target-app', + status: 'info', + // approach (b): emit the PUBLIC leaf platform (ios/macos), never the internal `apple`. + summary: `Target app installation checks are not supported for ${publicPlatformString(device)}.`, + evidence: { requested: targetApp, platform: publicPlatformString(device) }, + }, + useColor, + ); return; } - appendDoctorCheck(checks, { - id: 'target-app', - status: 'pass', - summary: `Target app is launchable: ${resolved}`, - evidence: { requested: targetApp, resolved, sessionApp: session?.appBundleId }, - }); + appendDoctorCheck( + checks, + { + id: 'target-app', + status: 'pass', + summary: `Target app is launchable: ${resolved}`, + evidence: { requested: targetApp, resolved, sessionApp: session?.appBundleId }, + }, + useColor, + ); } catch (error) { const normalized = normalizeError(error); - appendDoctorCheck(checks, { - id: 'target-app', - status: 'fail', - summary: `Target app check failed: ${normalized.message}`, - hint: normalized.hint ?? 'Install the app or pass an exact package/bundle id or app name.', - command: `agent-device apps --platform ${publicPlatformString(device)} --all`, - evidence: { code: normalized.code, message: normalized.message }, - }); + appendDoctorCheck( + checks, + { + id: 'target-app', + status: 'fail', + summary: `Target app check failed: ${normalized.message}`, + hint: normalized.hint ?? 'Install the app or pass an exact package/bundle id or app name.', + command: `agent-device apps --platform ${publicPlatformString(device)} --all`, + evidence: { code: normalized.code, message: normalized.message }, + }, + useColor, + ); } } diff --git a/packages/daemon/src/sessions/handlers/session-doctor-device.ts b/packages/daemon/src/sessions/handlers/session-doctor-device.ts index 383244406a..d918a1da30 100644 --- a/packages/daemon/src/sessions/handlers/session-doctor-device.ts +++ b/packages/daemon/src/sessions/handlers/session-doctor-device.ts @@ -40,21 +40,26 @@ export async function appendDeviceInventoryCheck( checks: DoctorCheck[], req: DaemonRequest, session: SessionState | undefined, + useColor: boolean, ): Promise { const selector = deviceInventorySelector(req, session); try { const inventory = await readDoctorDeviceInventory(selector); const devices = filterInventoryForSelector(inventory.devices, selector); - appendDoctorCheck(checks, { - id: 'device', - status: devices.length === 0 ? 'fail' : 'pass', - summary: deviceInventorySummary(devices, selector, inventory.failures), - hint: devices.length === 0 ? deviceInventoryFailureHint(inventory.failures) : undefined, - command: devices.length === 0 ? deviceInventoryCommand(selector) : undefined, - evidence: deviceInventoryEvidence(devices, inventory.failures), - }); + appendDoctorCheck( + checks, + { + id: 'device', + status: devices.length === 0 ? 'fail' : 'pass', + summary: deviceInventorySummary(devices, selector, inventory.failures), + hint: devices.length === 0 ? deviceInventoryFailureHint(inventory.failures) : undefined, + command: devices.length === 0 ? deviceInventoryCommand(selector) : undefined, + evidence: deviceInventoryEvidence(devices, inventory.failures), + }, + useColor, + ); if (devices.length > 0) { - appendInventoryFailureChecks(checks, inventory.failures); + appendInventoryFailureChecks(checks, inventory.failures, useColor); } return { devices, @@ -64,14 +69,18 @@ export async function appendDeviceInventoryCheck( }; } catch (error) { const normalized = normalizeError(error); - appendDoctorCheck(checks, { - id: 'device', - status: 'fail', - summary: normalized.message, - hint: normalized.hint, - command: 'agent-device devices', - evidence: { code: normalized.code, details: normalized.details }, - }); + appendDoctorCheck( + checks, + { + id: 'device', + status: 'fail', + summary: normalized.message, + hint: normalized.hint, + command: 'agent-device devices', + evidence: { code: normalized.code, details: normalized.details }, + }, + useColor, + ); return { devices: [], hostDevices: [], platform: selector.platform, target: selector.target }; } } @@ -80,35 +89,40 @@ export function resolveDoctorDeviceForAppCheck( checks: DoctorCheck[], inventory: DoctorDeviceInventory | undefined, targetApp: string | undefined, + useColor: boolean, ): DeviceInfo | undefined { if (!targetApp || !inventory) return undefined; const booted = inventory.devices.filter((device) => device.booted === true); if (booted.length === 1) return booted[0]; - appendDoctorCheck(checks, { - id: 'target-app-device', - status: 'fail', - summary: - booted.length === 0 - ? 'Target app check needs one booted device; none matched.' - : `Target app check needs one booted device; ${booted.length} matched.`, - hint: - booted.length === 0 - ? 'Boot a device, or adjust --platform/--target/--device/--udid/--serial.' - : 'Pass --platform/--target/--device/--udid/--serial so doctor checks the intended device.', - command: inventory.platform - ? `agent-device devices --platform ${inventory.platform}` - : 'agent-device devices', - evidence: { - targetApp, - booted: booted.map((device) => ({ - // approach (b): emit the PUBLIC leaf platform (ios/macos), never the internal `apple`. - platform: publicPlatformString(device), - id: device.id, - name: device.name, - })), + appendDoctorCheck( + checks, + { + id: 'target-app-device', + status: 'fail', + summary: + booted.length === 0 + ? 'Target app check needs one booted device; none matched.' + : `Target app check needs one booted device; ${booted.length} matched.`, + hint: + booted.length === 0 + ? 'Boot a device, or adjust --platform/--target/--device/--udid/--serial.' + : 'Pass --platform/--target/--device/--udid/--serial so doctor checks the intended device.', + command: inventory.platform + ? `agent-device devices --platform ${inventory.platform}` + : 'agent-device devices', + evidence: { + targetApp, + booted: booted.map((device) => ({ + // approach (b): emit the PUBLIC leaf platform (ios/macos), never the internal `apple`. + platform: publicPlatformString(device), + id: device.id, + name: device.name, + })), + }, }, - }); + useColor, + ); return undefined; } @@ -170,9 +184,10 @@ function combineDiscoveries(discoveries: readonly DeviceInventoryDiscovery[]): { function appendInventoryFailureChecks( checks: DoctorCheck[], failures: DoctorInventoryFailure[], + useColor: boolean, ): void { for (const failure of failures) { - appendDoctorCheck(checks, inventoryFailureCheck(failure)); + appendDoctorCheck(checks, inventoryFailureCheck(failure), useColor); } } diff --git a/packages/daemon/src/sessions/handlers/session-doctor-output.ts b/packages/daemon/src/sessions/handlers/session-doctor-output.ts index 7491d9a343..a680dea83b 100644 --- a/packages/daemon/src/sessions/handlers/session-doctor-output.ts +++ b/packages/daemon/src/sessions/handlers/session-doctor-output.ts @@ -3,7 +3,6 @@ import { formatDoctorCheckDetailLines, formatDoctorCheckSummaryLine, } from '@agent-device/host-kit/doctor-lines'; -import { shouldColorStatusMarkers } from '@agent-device/host-kit/status-markers'; import type { DoctorCheck, DoctorStatus } from '@agent-device/contracts/observability'; export function summarizeDoctorStatus(checks: DoctorCheck[]): 'pass' | 'warn' | 'fail' { @@ -23,26 +22,32 @@ export function sortChecks(checks: DoctorCheck[]): DoctorCheck[] { return [...checks].sort((a, b) => order[a.status] - order[b.status]); } -export function appendDoctorChecks(checks: DoctorCheck[], ...items: DoctorCheck[]): void { +export function appendDoctorChecks( + checks: DoctorCheck[], + useColor: boolean, + ...items: DoctorCheck[] +): void { for (const check of items) { - appendDoctorCheck(checks, check); + appendDoctorCheck(checks, check, useColor); } } -export function appendDoctorCheck(checks: DoctorCheck[], check: DoctorCheck): void { +export function appendDoctorCheck( + checks: DoctorCheck[], + check: DoctorCheck, + useColor: boolean, +): void { checks.push(check); emitRequestProgress({ type: 'command', status: 'progress', - message: formatDoctorCheckProgressMessage(check), + message: formatDoctorCheckProgressMessage(check, useColor), }); } -function formatDoctorCheckProgressMessage(check: DoctorCheck): string { +function formatDoctorCheckProgressMessage(check: DoctorCheck, useColor: boolean): string { return [ - formatDoctorCheckSummaryLine(check, { - useColor: shouldColorStatusMarkers(process.env, Boolean(process.stderr.isTTY)), - }), + formatDoctorCheckSummaryLine(check, { useColor }), ...formatDoctorCheckDetailLines(check), ].join('\n'); } diff --git a/packages/daemon/src/sessions/handlers/session-doctor.ts b/packages/daemon/src/sessions/handlers/session-doctor.ts index 90813af791..76c89bb7fb 100644 --- a/packages/daemon/src/sessions/handlers/session-doctor.ts +++ b/packages/daemon/src/sessions/handlers/session-doctor.ts @@ -5,6 +5,7 @@ import { PUBLIC_COMMANDS } from '@agent-device/command-registry/catalog'; import { isIosFamily, publicPlatformString, type DeviceInfo } from '@agent-device/kernel/device'; import { AppError } from '@agent-device/kernel/errors'; import { emitRequestProgress } from '@agent-device/host-kit/request'; +import { shouldColorStatusMarkers } from '@agent-device/host-kit/status-markers'; import { isActiveProviderDevice } from '../../core/provider-device-admission.ts'; import { listLocalDeviceInventory, @@ -67,9 +68,11 @@ export async function handleDoctorCommand(params: { const session = sessionStore.get(sessionName); const options = readDoctorOptions(req, session); const stateDir = resolveDoctorStateDir(sessionStore, sessionName); + const useColor = shouldColorStatusMarkers(params.host.env, Boolean(process.stderr.isTTY)); const checks: DoctorCheck[] = []; appendDoctorChecks( checks, + useColor, { id: 'agent-device', status: 'pass', @@ -85,12 +88,12 @@ export async function handleDoctorCommand(params: { } const context = hostDiagnosticsContext(options, stateDir, androidAdbExecutor); - const inventory = await appendDeviceInventoryCheck(checks, req, session); + const inventory = await appendDeviceInventoryCheck(checks, req, session, useColor); const toolchain = await hostDiagnostics.toolchainCheck( session?.device.platform ?? inventory?.platform, context, ); - if (toolchain) appendDoctorCheck(checks, toolchain); + if (toolchain) appendDoctorCheck(checks, toolchain, useColor); const appCheckDevice = await appendLocalDoctorChecks({ checks, context, @@ -102,11 +105,12 @@ export async function handleDoctorCommand(params: { bindDevice, req, env: params.host.env, + useColor, }); const warmupDevice = session?.device ?? resolveHostWarmupDevice(inventory, appCheckDevice); if (warmupDevice) { const warmup = await hostDiagnostics.warmupCheck(warmupDevice, context); - if (warmup) appendDoctorCheck(checks, warmup); + if (warmup) appendDoctorCheck(checks, warmup, useColor); } return doctorResponse(checks, options, { device: appCheckDevice, includeMetro: true, inventory }); } @@ -175,6 +179,7 @@ async function appendLocalDoctorChecks(params: { bindDevice?: BindDeviceRuntime; req: DaemonRequest; env: HostEnvironment['env']; + useColor: boolean; }): Promise { const { checks, @@ -187,9 +192,11 @@ async function appendLocalDoctorChecks(params: { bindDevice, req, env, + useColor, } = params; const appCheckDevice = - session?.device ?? resolveDoctorDeviceForAppCheck(checks, inventory, options.targetApp); + session?.device ?? + resolveDoctorDeviceForAppCheck(checks, inventory, options.targetApp, useColor); if (appCheckDevice) { await appendDeviceScopedDoctorChecks(checks, { context, @@ -201,12 +208,17 @@ async function appendLocalDoctorChecks(params: { bindDevice, req, env, + useColor, }); } if (options.shouldProbeMetro) { - appendDoctorCheck(checks, await probeMetro(options.metroHost, options.metroPort, options.kind)); + appendDoctorCheck( + checks, + await probeMetro(options.metroHost, options.metroPort, options.kind), + useColor, + ); } - appendDoctorChecks(checks, ...(await hostDiagnostics.ambientChecks(context))); + appendDoctorChecks(checks, useColor, ...(await hostDiagnostics.ambientChecks(context))); return appCheckDevice; } @@ -222,10 +234,21 @@ async function appendDeviceScopedDoctorChecks( bindDevice?: BindDeviceRuntime; req: DaemonRequest; env: HostEnvironment['env']; + useColor: boolean; }, ): Promise { - const { context, hostDiagnostics, device, options, session, inspectFacts, bindDevice, req, env } = - params; + const { + context, + hostDiagnostics, + device, + options, + session, + inspectFacts, + bindDevice, + req, + env, + useColor, + } = params; let listInstalledApps: DoctorAppInventory | undefined; try { listInstalledApps = await resolveDoctorAppInventoryForDoctor({ @@ -248,8 +271,9 @@ async function appendDeviceScopedDoctorChecks( session, targetApp: options.targetApp, listInstalledApps, + useColor, }); - appendDoctorChecks(checks, ...(await hostDiagnostics.deviceChecks(device, context))); + appendDoctorChecks(checks, useColor, ...(await hostDiagnostics.deviceChecks(device, context))); } async function resolveDoctorAppInventoryForDoctor( diff --git a/packages/daemon/src/sessions/handlers/session-progress.test.ts b/packages/daemon/src/sessions/handlers/session-progress.test.ts index d4116f290c..6dcd6c98db 100644 --- a/packages/daemon/src/sessions/handlers/session-progress.test.ts +++ b/packages/daemon/src/sessions/handlers/session-progress.test.ts @@ -33,6 +33,7 @@ const ANDROID_TWO: DeviceInfo = { async function runSuite(params: { root: string; + inputs?: string[]; requestId: string; flags?: DaemonRequest['flags']; devices?: DeviceInfo[]; @@ -57,7 +58,7 @@ async function runSuite(params: { token: 't', session: 'default', command: 'test', - positionals: [params.root], + positionals: params.inputs ?? [params.root], flags: params.flags, meta: { cwd: params.root, requestId: params.requestId }, }, @@ -91,6 +92,7 @@ test('native suite progress carries skip, retry hint, and attempt session values const { events, data } = await runSuite({ root, + inputs: [skippedPath, retryPath], requestId: 'progress', flags: { platform: 'android', retries: 1 }, invoke: async () => { diff --git a/packages/daemon/src/sessions/handlers/session-replay-command.ts b/packages/daemon/src/sessions/handlers/session-replay-command.ts index 2de5f945c0..d95911586a 100644 --- a/packages/daemon/src/sessions/handlers/session-replay-command.ts +++ b/packages/daemon/src/sessions/handlers/session-replay-command.ts @@ -141,6 +141,7 @@ export function createReplaySession( update: updateSession, clearRepairTombstone: () => store.clearRepairTombstone(name), }, + hostCwd: store.host.cwd, }), assertSelectorMatches: (flags) => { const ref = store.lookup(name); diff --git a/packages/daemon/src/sessions/handlers/session-script-publication.ts b/packages/daemon/src/sessions/handlers/session-script-publication.ts index b5d1c99e1f..89889ce656 100644 --- a/packages/daemon/src/sessions/handlers/session-script-publication.ts +++ b/packages/daemon/src/sessions/handlers/session-script-publication.ts @@ -42,7 +42,11 @@ export function handleSessionScriptPublication(params: { if (req.positionals?.[0] !== undefined && !explicitPath) { return failure(new AppError('INVALID_ARGS', 'session save-script path cannot be empty.')); } - retargetActivePublication(session, { explicitPath, liveForce: req.flags?.force }); + retargetActivePublication( + session, + { explicitPath, liveForce: req.flags?.force }, + sessionStore.host.cwd, + ); const result = sessionStore.writeSessionLog(ref, { force: effectiveWriteForce(session, req.flags?.force), diff --git a/packages/daemon/src/sessions/handlers/snapshot-runtime-disclosure.test.ts b/packages/daemon/src/sessions/handlers/snapshot-runtime-disclosure.test.ts index 77c40fb734..bd5af446fd 100644 --- a/packages/daemon/src/sessions/handlers/snapshot-runtime-disclosure.test.ts +++ b/packages/daemon/src/sessions/handlers/snapshot-runtime-disclosure.test.ts @@ -5,7 +5,7 @@ import type { IosTargetActivation, PostGestureOutcome } from '@agent-device/kern import { makeSnapshotState } from '@agent-device/selectors/snapshot-geometry-fixtures'; import { markDeferredInteractionOutcome } from '../../core/deferred-interaction-outcome.ts'; import { makeAndroidSession, makeIosSession } from '../../__tests__/support/session-factories.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { SessionStore } from '../../core/session-store.ts'; import { handleSnapshotCommands } from './snapshot.ts'; import { legacyDispatchCapture } from '../../__tests__/support/legacy-snapshot-capture-fixture.ts'; @@ -147,7 +147,10 @@ test('a snapshot after a scroll that moved nothing warns and stamps the no-effec function scenario(params: { storedRepair?: boolean; android?: boolean }) { const root = mkdtempForTestSync('agent-device-snapshot-target-activation'); const sessionName = 'default'; - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const session = params.android ? makeAndroidSession(sessionName) : makeIosSession(sessionName, { appBundleId: 'com.example.app' }); diff --git a/packages/daemon/src/sessions/handlers/snapshot-runtime-route.test.ts b/packages/daemon/src/sessions/handlers/snapshot-runtime-route.test.ts index 74571362cb..a9dd6e7aaa 100644 --- a/packages/daemon/src/sessions/handlers/snapshot-runtime-route.test.ts +++ b/packages/daemon/src/sessions/handlers/snapshot-runtime-route.test.ts @@ -18,6 +18,7 @@ vi.mock('../snapshot-runtime.ts', () => ({ import { SessionStore } from '../../core/session-store.ts'; import type { DaemonRequest } from '../../core/daemon-request.ts'; import { handleSnapshotCommands } from './snapshot.ts'; +import { createLiveTestHostEnvironment } from '@agent-device/test-kit'; beforeEach(() => { dispatchSnapshotDiffViaRuntime.mockReset(); @@ -29,7 +30,10 @@ beforeEach(() => { test('snapshot route forwards the request runtime facts and binding seams exactly once', async () => { const inspectFacts = vi.fn(); const bindDevice = vi.fn(); - const sessionStore = new SessionStore('/tmp/agent-device-snapshot-runtime-route-test'); + const sessionStore = new SessionStore( + '/tmp/agent-device-snapshot-runtime-route-test', + createLiveTestHostEnvironment(), + ); const req: DaemonRequest = { command: 'snapshot', positionals: [], @@ -66,7 +70,10 @@ test('snapshot route forwards the request runtime facts and binding seams exactl test('diff route forwards the request runtime facts and binding seams exactly once', async () => { const inspectFacts = vi.fn(); const bindDevice = vi.fn(); - const sessionStore = new SessionStore('/tmp/agent-device-snapshot-diff-route-test'); + const sessionStore = new SessionStore( + '/tmp/agent-device-snapshot-diff-route-test', + createLiveTestHostEnvironment(), + ); const req: DaemonRequest = { command: 'diff', positionals: ['snapshot'], diff --git a/packages/daemon/src/sessions/server/daemon-idle-reap-lifecycle.test.ts b/packages/daemon/src/sessions/server/daemon-idle-reap-lifecycle.test.ts index 78f6a5fa0f..9941a0a72e 100644 --- a/packages/daemon/src/sessions/server/daemon-idle-reap-lifecycle.test.ts +++ b/packages/daemon/src/sessions/server/daemon-idle-reap-lifecycle.test.ts @@ -27,7 +27,10 @@ let sessionStore: SessionStore; beforeEach(() => { stateDir = mkdtempForTestSync('agent-device-daemon-lifecycle-admission-'); - sessionStore = new SessionStore(path.join(stateDir, 'sessions')); + sessionStore = new SessionStore(path.join(stateDir, 'sessions'), { + env: process.env, + cwd: process.cwd(), + }); }); afterEach(() => { diff --git a/packages/daemon/src/sessions/server/daemon-idle-reap.test.ts b/packages/daemon/src/sessions/server/daemon-idle-reap.test.ts index 9e51d5e792..e0d5b4786f 100644 --- a/packages/daemon/src/sessions/server/daemon-idle-reap.test.ts +++ b/packages/daemon/src/sessions/server/daemon-idle-reap.test.ts @@ -12,7 +12,7 @@ import { isDaemonIdle, resolveDaemonIdleReapMs, } from './daemon-idle-reap.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { makeTestScreenRecordingResource } from '../../__tests__/support/screen-recording-live-handle.ts'; let stateDir: string; @@ -20,7 +20,7 @@ let sessionStore: SessionStore; beforeEach(() => { stateDir = mkdtempForTestSync('agent-device-daemon-idle-reap-'); - sessionStore = new SessionStore(path.join(stateDir, 'sessions')); + sessionStore = new SessionStore(path.join(stateDir, 'sessions'), createLiveTestHostEnvironment()); }); afterEach(() => { diff --git a/packages/daemon/src/sessions/server/daemon-idle-reap.ts b/packages/daemon/src/sessions/server/daemon-idle-reap.ts index 45df16799b..a47ef7bcc8 100644 --- a/packages/daemon/src/sessions/server/daemon-idle-reap.ts +++ b/packages/daemon/src/sessions/server/daemon-idle-reap.ts @@ -48,7 +48,7 @@ const DAEMON_IDLE_REAP_DEFAULT_MS = 5 * 60_000; // that host, and one whose owner cannot be proven is treated the same way, because nothing here can // show the window's owner is nobody. export function resolveDaemonIdleReapMs( - env: NodeJS.ProcessEnv = process.env, + env: NodeJS.ProcessEnv, ownership: DaemonRuntimeOwnership = 'detached', ): number { if (ownership !== 'detached') return 0; @@ -119,7 +119,7 @@ export function createDaemonIdleReap(params: { */ hasRetainedLeases: () => boolean; onIdleReap: () => void; - env?: NodeJS.ProcessEnv; + env: NodeJS.ProcessEnv; /** * Who owns the process this timer runs in. A host that started the runtime in its own process owns * its lifetime: an idle window tuned for a detached daemon nobody is watching would take the runtime diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-ended-lease.test.ts b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-ended-lease.test.ts index 3964932f3e..5599470992 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-ended-lease.test.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-ended-lease.test.ts @@ -3,9 +3,10 @@ import { beforeEach, expect, test, vi } from 'vitest'; import path from 'node:path'; import { AppError } from '@agent-device/kernel/errors'; import { - mkdtempForTestSync, bindTestCommandHost, + createLiveTestHostEnvironment, createTestHostEnvironment, + mkdtempForTestSync, } from '@agent-device/test-kit'; import { SessionStore } from '../../../../core/session-store.ts'; import type { DaemonResponse } from '@agent-device/kernel/contracts'; @@ -73,7 +74,10 @@ function close( positionals: string[] = [], ): { response: Promise; endedLeases: EndedLeaseLedger } { const root = mkdtempForTestSync('agent-device-session-close-ended-lease-'); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); sessionStore.publish(SESSION_NAME, session); const endedLeases: EndedLeaseLedger = []; const response = handleSessionCommands({ diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-lifecycle-runtime.test.ts b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-lifecycle-runtime.test.ts index 506a5ad03b..27ff1fc970 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-lifecycle-runtime.test.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-lifecycle-runtime.test.ts @@ -25,9 +25,10 @@ import { dispatchApplicationLifecycleEffect, } from '../../../../__tests__/support/application-lifecycle-runtime-fixture.ts'; import { - mkdtempForTestSync, bindTestCommandHost, + createLiveTestHostEnvironment, createTestHostEnvironment, + mkdtempForTestSync, } from '@agent-device/test-kit'; bindTestCommandHost(); @@ -37,7 +38,7 @@ const noopInvoke = async (_req: DaemonRequest): Promise => ({ ok function makeSessionStore(): SessionStore { const root = mkdtempForTestSync('agent-device-session-close-lifecycle-runtime-'); - return new SessionStore(path.join(root, 'sessions')); + return new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); } function makeSession(name: string, device: SessionState['device']): SessionState { diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-script.test.ts b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-script.test.ts index 340335fe63..05e8d17ea1 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-script.test.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-script.test.ts @@ -14,7 +14,7 @@ import { commitRepairScriptBeforeClose, finalizeOrdinaryCloseScript, } from '../session-close-script.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { flushSessionEventLogWrites } from '@agent-device/session-journal/session-event-log'; const roots: string[] = []; @@ -33,7 +33,7 @@ function setup(name: string, session = makeIosSession(name, { appBundleId: 'com. const root = mkdtempForTestSync('agent-device-session-close-script-'); roots.push(root); const sessionsDir = path.join(root, 'sessions'); - const sessionStore = new SessionStore(sessionsDir); + const sessionStore = new SessionStore(sessionsDir, createLiveTestHostEnvironment()); const ref = sessionStore.publish(name, session); const req: DaemonRequest = { token: 'token', diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-shutdown.fixtures.ts b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-shutdown.fixtures.ts index b62d1438a0..b9fd597268 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-shutdown.fixtures.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-shutdown.fixtures.ts @@ -65,7 +65,11 @@ import { stopIosRunnerSession, } from '@agent-device/platform-apple/runner/operations'; import { cleanupAppleXctracePerfCapture } from '@agent-device/platform-apple/perf'; -import { mkdtempForTestSync, WEB_DESKTOP_DEVICE } from '@agent-device/test-kit'; +import { + WEB_DESKTOP_DEVICE, + createLiveTestHostEnvironment, + mkdtempForTestSync, +} from '@agent-device/test-kit'; import { acquireDeviceClaim, inspectDeviceClaims, @@ -108,7 +112,7 @@ const noopInvoke = async (_req: DaemonRequest): Promise => ({ function makeSessionStore(): SessionStore { const root = mkdtempForTestSync('agent-device-session-close-shutdown-'); - return new SessionStore(path.join(root, 'sessions')); + return new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); } function makeSession(name: string, device: SessionState['device']): SessionState { diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-without-session-mode.test.ts b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-without-session-mode.test.ts index 43cef9c985..47d1e771d1 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-without-session-mode.test.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-close-without-session-mode.test.ts @@ -8,7 +8,11 @@ import type { DaemonResponse } from '@agent-device/kernel/contracts'; import type { DaemonRequest } from '../../../../core/daemon-request.ts'; import { SessionStore } from '../../../../core/session-store.ts'; import { fakeCreateCommandSurface } from '../../../../__tests__/support/command-surface-fake.ts'; -import { mkdtempForTestSync, createTestHostEnvironment } from '@agent-device/test-kit'; +import { + createLiveTestHostEnvironment, + createTestHostEnvironment, + mkdtempForTestSync, +} from '@agent-device/test-kit'; const mockResolveTargetDevice = vi.hoisted(() => vi.fn()); @@ -43,6 +47,7 @@ const ANDROID_DEVICE: DeviceInfo = { test('an app-only kill close with no stored session carries mode kill to the platform close', async () => { const sessionStore = new SessionStore( path.join(mkdtempForTestSync('agent-device-session-close-without-session-'), 'sessions'), + createLiveTestHostEnvironment(), ); mockResolveTargetDevice.mockResolvedValue(ANDROID_DEVICE); const seenInputs: CloseApplicationInput[] = []; diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-open-runtime.fixtures.ts b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-open-runtime.fixtures.ts index 01924ac24c..f5c12da302 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-open-runtime.fixtures.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/__tests__/session-open-runtime.fixtures.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { SessionStore } from '../../../../core/session-store.ts'; import type { DaemonRequest } from '../../../../core/daemon-request.ts'; import type { SessionState } from '../../../../core/session-state.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; export const noopInvoke = async (_req: DaemonRequest): Promise => ({ ok: true, @@ -12,7 +12,7 @@ export const noopInvoke = async (_req: DaemonRequest): Promise = export function makeSessionStore(): SessionStore { const root = mkdtempForTestSync('agent-device-session-open-runtime-'); - return new SessionStore(path.join(root, 'sessions')); + return new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); } export function makeSession(name: string, device: SessionState['device']): SessionState { diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/session-close-lifecycle-teardown.test.ts b/packages/daemon/src/sessions/session-lifecycle/internal/session-close-lifecycle-teardown.test.ts index 25ff157843..8d3f4ae155 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/session-close-lifecycle-teardown.test.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/session-close-lifecycle-teardown.test.ts @@ -4,9 +4,10 @@ import type { DaemonResponse } from '@agent-device/kernel/contracts'; import type { DeviceInfo } from '@agent-device/kernel/device'; import type { CloseApplicationFinalizationInput } from '@agent-device/contracts/application-lifecycle-runtime'; import { - mkdtempForTestSync, bindTestCommandHost, + createLiveTestHostEnvironment, createTestHostEnvironment, + mkdtempForTestSync, } from '@agent-device/test-kit'; import { SessionStore } from '../../../core/session-store.ts'; import { LeaseRegistry } from '../../../resources/lease-registry.ts'; @@ -41,7 +42,7 @@ beforeEach(() => { function makeSessionStore(): SessionStore { const root = mkdtempForTestSync('agent-device-close-teardown-'); - return new SessionStore(path.join(root, 'sessions')); + return new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); } function publishSession( diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution-relaunch.test.ts b/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution-relaunch.test.ts index b92df41b6c..961427e5d8 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution-relaunch.test.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution-relaunch.test.ts @@ -8,9 +8,10 @@ import type { OpenApplicationInput, } from '@agent-device/contracts/application-lifecycle-runtime'; import { - mkdtempForTestSync, bindTestCommandHost, + createLiveTestHostEnvironment, createTestHostEnvironment, + mkdtempForTestSync, } from '@agent-device/test-kit'; import { SessionStore } from '../../../core/session-store.ts'; import type { DaemonRequest } from '../../../core/daemon-request.ts'; @@ -76,7 +77,7 @@ afterEach(() => { function makeSessionStore(): SessionStore { const root = mkdtempForTestSync('agent-device-open-relaunch-'); - return new SessionStore(path.join(root, 'sessions')); + return new SessionStore(path.join(root, 'sessions'), createLiveTestHostEnvironment()); } function publishSession( diff --git a/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution.ts b/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution.ts index 5918de576e..6261f196de 100644 --- a/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution.ts +++ b/packages/daemon/src/sessions/session-lifecycle/internal/session-open-execution.ts @@ -97,12 +97,13 @@ function applyOrdinaryScriptRecordingOpenOutcome(params: { existingSession: SessionState | undefined; saveScriptRequested: boolean; responseData: Record; + hostCwd: string; }): void { - const { session, existingSession, saveScriptRequested, responseData } = params; + const { session, existingSession, saveScriptRequested, responseData, hostCwd } = params; if (!existingSession && saveScriptRequested) { // The recorded `open` action's flag ingress applies the explicit path/force right after // this arm (`applyRecordedSaveScriptFlags`), exactly as the field writers used to split it. - armAuthoringOnOpen(session, {}); + armAuthoringOnOpen(session, {}, hostCwd); return; } if (!isAuthoringArmedSession(existingSession)) return; @@ -385,6 +386,7 @@ export async function completeOpenCommand(params: { existingSession: existingRef ? nextSession : undefined, saveScriptRequested: Boolean(req.flags?.saveScript), responseData: openResult, + hostCwd: sessionStore.host.cwd, }); sessionStore.recordAction(nextRef, { command: 'open', diff --git a/packages/daemon/src/sessions/session-replay-coordinator.ts b/packages/daemon/src/sessions/session-replay-coordinator.ts index befc95330f..b6a47ad730 100644 --- a/packages/daemon/src/sessions/session-replay-coordinator.ts +++ b/packages/daemon/src/sessions/session-replay-coordinator.ts @@ -62,8 +62,10 @@ export type ReplaySessionMutationStore = Readonly<{ export function createReplayCoordinator(params: { sessionStore: ReplaySessionStore; mutationStore: ReplaySessionMutationStore; + /** The runtime host's working directory, the base a step's `--save-script` path resolves against. */ + hostCwd: string; }): ReplayCoordinator { - const { sessionStore, mutationStore } = params; + const { sessionStore, mutationStore, hostCwd } = params; const current = (): Readonly | undefined => sessionStore.get(); const resumeStamper: ReplayResumeStamper = { @@ -90,13 +92,17 @@ export function createReplayCoordinator(params: { armStep(stepParams): void { mutationStore.update((session) => { - armRepairStep(session, { - saveScript: stepParams.saveScript, - force: stepParams.force, - sourcePath: stepParams.sourcePath, - healedSiblingPath: healedScriptSiblingPath(stepParams.sourcePath), - firstArm: stepParams.firstArm, - }); + armRepairStep( + session, + { + saveScript: stepParams.saveScript, + force: stepParams.force, + sourcePath: stepParams.sourcePath, + healedSiblingPath: healedScriptSiblingPath(stepParams.sourcePath), + firstArm: stepParams.firstArm, + }, + hostCwd, + ); }); }, diff --git a/packages/daemon/src/sessions/snapshot-quality-latch-disclosure.test.ts b/packages/daemon/src/sessions/snapshot-quality-latch-disclosure.test.ts index 9d4162ef34..51ac5f2eb2 100644 --- a/packages/daemon/src/sessions/snapshot-quality-latch-disclosure.test.ts +++ b/packages/daemon/src/sessions/snapshot-quality-latch-disclosure.test.ts @@ -16,7 +16,7 @@ import { SessionStore } from '../core/session-store.ts'; import type { SessionState } from '../core/session-state.ts'; import { legacyDispatchCapture } from '../__tests__/support/legacy-snapshot-capture-fixture.ts'; import { snapshotRuntimeFixture } from '../__tests__/support/snapshot-runtime-fixture.ts'; -import { mkdtempForTestSync } from '@agent-device/test-kit'; +import { createLiveTestHostEnvironment, mkdtempForTestSync } from '@agent-device/test-kit'; import { forwardingCreateCommandSurfaceWithAnnotations as testCreateCommandSurface } from '../__tests__/support/command-surface-fake.ts'; vi.mock('../core/snapshot-interactor-capture.ts', async () => { @@ -188,7 +188,10 @@ function scenario() { mkdtempForTestSync('agent-device-quality-latch'), `agent-device-quality-latch-${crypto.randomUUID()}`, ); - const sessionStore = new SessionStore(path.join(root, 'sessions')); + const sessionStore = new SessionStore( + path.join(root, 'sessions'), + createLiveTestHostEnvironment(), + ); const sessionName = 'default'; const session = makeIosSession(sessionName, { appBundleId: 'com.example.app' }); sessionStore.publish(sessionName, session); diff --git a/packages/daemon/test/nightly/concurrency-torture/harness.ts b/packages/daemon/test/nightly/concurrency-torture/harness.ts index 4d90acfb3e..3247ea4fa8 100644 --- a/packages/daemon/test/nightly/concurrency-torture/harness.ts +++ b/packages/daemon/test/nightly/concurrency-torture/harness.ts @@ -26,6 +26,7 @@ import os from 'node:os'; import path from 'node:path'; import type { DeviceInfo } from '@agent-device/kernel/device'; +import { createTestHostEnvironment } from '@agent-device/test-kit'; import { LeaseRegistry } from '../../../src/resources/lease-registry.ts'; import { SessionStore } from '../../../src/core/session-store.ts'; import type { SessionState } from '../../../src/core/session-state.ts'; @@ -126,7 +127,10 @@ class TortureWorld { this.opsPerClient = config.opsPerClient ?? 6 + this.prng.int(9); // 6..14 this.activeClients = this.clientCount; this.stateRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-device-torture-')); - this.sessionStore = new SessionStore(path.join(this.stateRoot, 'sessions')); + this.sessionStore = new SessionStore( + path.join(this.stateRoot, 'sessions'), + createTestHostEnvironment(), + ); this.leaseRegistry = new LeaseRegistry({ maxLeaseTtlMs: 10 * 60_000 }); } diff --git a/packages/daemon/test/nightly/concurrency-torture/real-scope-serialization.ts b/packages/daemon/test/nightly/concurrency-torture/real-scope-serialization.ts index dd938daa1d..965e8cc109 100644 --- a/packages/daemon/test/nightly/concurrency-torture/real-scope-serialization.ts +++ b/packages/daemon/test/nightly/concurrency-torture/real-scope-serialization.ts @@ -48,7 +48,7 @@ export async function measureRealScopeMaxOverlap(concurrency: number): Promise assert.equal(fs.existsSync(evidence), true)); - assert.equal(fs.readFileSync(evidence, 'utf8'), `${fs.realpathSync(hostCwd)}\n`); + await vi.waitFor(() => + assert.equal(fs.readFileSync(evidence, 'utf8'), `${fs.realpathSync(hostCwd)}\n`), + ); }); diff --git a/packages/session-journal/src/__tests__/session-event-log-window.test.ts b/packages/session-journal/src/__tests__/session-event-log-window.test.ts index cd0cd19637..d623762d0a 100644 --- a/packages/session-journal/src/__tests__/session-event-log-window.test.ts +++ b/packages/session-journal/src/__tests__/session-event-log-window.test.ts @@ -27,11 +27,16 @@ afterEach(() => { async function appendEvents(eventLogPath: string, from: number, to: number): Promise { for (let index = from; index < to; index += 1) { - appendSessionEvent(eventLogPath, 'default', { - kind: 'request.started', - command: 'snapshot', - summary: `event ${index}`, - }); + appendSessionEvent( + eventLogPath, + 'default', + { + kind: 'request.started', + command: 'snapshot', + summary: `event ${index}`, + }, + process.env, + ); } await flushSessionEventLogWrites(eventLogPath); } @@ -66,7 +71,7 @@ test('rotation caps events.ndjson while cursors stay absolute across the retaine // One entry is ~110 bytes; a 1 KB cap rotates every 9-10 entries, so 40 // entries force several rotations and at least one dropped generation. process.env.AGENT_DEVICE_EVENT_LOG_MAX_BYTES = '1024'; - assert.equal(resolveSessionEventLogMaxBytes(), 1024); + assert.equal(resolveSessionEventLogMaxBytes(process.env), 1024); await appendEvents(eventLogPath, 0, 40); @@ -225,7 +230,7 @@ test('the default cap leaves an ordinary session log unrotated, and a non-intege const eventLogPath = resolveSessionEventLogPath(dir); // "5MB" must not be read as 5 bytes, which would rotate on every entry. process.env.AGENT_DEVICE_EVENT_LOG_MAX_BYTES = '5MB'; - assert.equal(resolveSessionEventLogMaxBytes(), 5 * 1024 * 1024); + assert.equal(resolveSessionEventLogMaxBytes(process.env), 5 * 1024 * 1024); await appendEvents(eventLogPath, 0, 3); diff --git a/packages/session-journal/src/session-event-log-window.ts b/packages/session-journal/src/session-event-log-window.ts index 87c5bb4489..7d34e50c60 100644 --- a/packages/session-journal/src/session-event-log-window.ts +++ b/packages/session-journal/src/session-event-log-window.ts @@ -63,7 +63,7 @@ export type SessionEventLogWindow = { * `maxBytes` of history after any rotation. A non-integer value is ignored * rather than parsed leniently: `5MB` is not 5 bytes. */ -export function resolveSessionEventLogMaxBytes(env: NodeJS.ProcessEnv = process.env): number { +export function resolveSessionEventLogMaxBytes(env: NodeJS.ProcessEnv): number { const raw = env.AGENT_DEVICE_EVENT_LOG_MAX_BYTES?.trim(); if (raw === undefined || !/^\d+$/.test(raw)) return DEFAULT_EVENT_LOG_MAX_BYTES; const parsed = Number(raw); diff --git a/packages/session-journal/src/session-event-log.ts b/packages/session-journal/src/session-event-log.ts index 5ad29b8247..d39027d01a 100644 --- a/packages/session-journal/src/session-event-log.ts +++ b/packages/session-journal/src/session-event-log.ts @@ -64,6 +64,8 @@ export function appendSessionEvent( eventLogPath: string, sessionName: string, event: SessionEventLogInput, + /** The runtime host's environment: the source of the active-file rotation cap. */ + env: NodeJS.ProcessEnv, ): void { const entry = redactDiagnosticData({ version: EVENT_LOG_VERSION, @@ -71,7 +73,7 @@ export function appendSessionEvent( session: sessionName, ...event, } satisfies SessionEventLogEntry); - queueEventLogWrite(eventLogPath, `${JSON.stringify(entry)}\n`); + queueEventLogWrite(eventLogPath, `${JSON.stringify(entry)}\n`, env); } export async function flushSessionEventLogWrites(eventLogPath?: string): Promise { @@ -81,13 +83,13 @@ export async function flushSessionEventLogWrites(eventLogPath?: string): Promise await pending; } -function queueEventLogWrite(eventLogPath: string, line: string): void { +function queueEventLogWrite(eventLogPath: string, line: string, env: NodeJS.ProcessEnv): void { const previous = pendingEventLogWrites.get(eventLogPath) ?? Promise.resolve(); const pending = previous .catch(() => undefined) .then(async () => { await ensureEventLogDir(eventLogPath); - await rotateSessionEventLogIfNeeded(eventLogPath, resolveSessionEventLogMaxBytes()); + await rotateSessionEventLogIfNeeded(eventLogPath, resolveSessionEventLogMaxBytes(env)); await fs.promises.appendFile(eventLogPath, line, 'utf8'); }) .catch((error) => { @@ -131,14 +133,20 @@ export function appendActionEvent( eventLogPath: string, sessionName: string, action: SessionAction, + env: NodeJS.ProcessEnv, ): void { - appendSessionEvent(eventLogPath, sessionName, { - kind: 'action.recorded', - requestId: getDiagnosticsMeta().requestId, - command: action.command, - summary: buildActionSummary(action), - details: buildActionDetails(action), - }); + appendSessionEvent( + eventLogPath, + sessionName, + { + kind: 'action.recorded', + requestId: getDiagnosticsMeta().requestId, + command: action.command, + summary: buildActionSummary(action), + details: buildActionDetails(action), + }, + env, + ); } export function buildRequestStartedEvent(params: { req: DaemonRequest }): SessionEventLogInput { diff --git a/test/integration/provider-scenarios/android-tv-remote.test.ts b/test/integration/provider-scenarios/android-tv-remote.test.ts index 4f3fe41030..15be860595 100644 --- a/test/integration/provider-scenarios/android-tv-remote.test.ts +++ b/test/integration/provider-scenarios/android-tv-remote.test.ts @@ -34,6 +34,7 @@ test('Provider-backed integration Android TV remote flow sends D-pad keyevents', app: 'com.android.settings', platform: 'android', serial: PROVIDER_SCENARIO_ANDROID_TV.id, + testIme: false, }); await runProviderScenario(daemon, [ diff --git a/test/integration/provider-scenarios/android-world.ts b/test/integration/provider-scenarios/android-world.ts index 68f0c1e6e4..e1016997ab 100644 --- a/test/integration/provider-scenarios/android-world.ts +++ b/test/integration/provider-scenarios/android-world.ts @@ -89,9 +89,6 @@ export async function createAndroidSettingsWorld(options?: { const apkPath = path.join(tempRoot, 'Demo.apk'); const aabPath = path.join(tempRoot, 'Demo.aab'); const logcatProcessPath = createScriptedLogcatExecutable(tempRoot); - const previousAppEventTemplate = process.env.AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE; - process.env.AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE = - 'demo://agent-device/event?name={event}&payload={payload}&platform={platform}'; fs.writeFileSync(apkPath, 'placeholder apk'); fs.writeFileSync(aabPath, 'placeholder aab'); const manifestApkPath = await createAndroidManifestApk(tempRoot, { @@ -156,6 +153,10 @@ export async function createAndroidSettingsWorld(options?: { }; } const daemon = await createProviderScenarioHarness({ + env: { + AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE: + 'demo://agent-device/event?name={event}&payload={payload}&platform={platform}', + }, androidAdbProvider: () => adbProvider, deviceInventoryProvider: async (request) => { inventoryRequests.push({ ...request }); @@ -187,7 +188,6 @@ export async function createAndroidSettingsWorld(options?: { close: async () => { if (closed) return; closed = true; - restoreEnv('AGENT_DEVICE_ANDROID_APP_EVENT_URL_TEMPLATE', previousAppEventTemplate); hostAdbGuard.restore(); for (const child of spawnedLogcat) { if (!child.killed && typeof child.exitCode !== 'number') child.kill('SIGKILL'); diff --git a/test/wire-compat/ledger.json b/test/wire-compat/ledger.json index 152888f953..7a311acf92 100644 --- a/test/wire-compat/ledger.json +++ b/test/wire-compat/ledger.json @@ -74,8 +74,8 @@ "src/daemon-client/daemon-client-transport.ts#readRemoteDaemonHealth": "sha256:df174d1ccf73851ca58bd75533fb4660424003b98bd9e41fe942cf3ab3698e5f", "packages/daemon/src/server/downloadable-artifact-http.ts#DownloadableArtifactHttpAuthorizer": "sha256:1b2702a929ca9170db2ca97c08e3ab67e17edb3ee75325a576c4c1b9cdbebb44", "packages/daemon/src/server/downloadable-artifact-http.ts#DownloadableArtifactHttpRoute": "sha256:e63c4581ccde668913914149c9092d16ecf8a5cbd7ab33c6eb8617e77fc1015e", - "packages/daemon/src/server/downloadable-artifact-http.ts#handleArtifactDownload": "sha256:7f96d17b7c605230fb3cc21ceaa5b2e515d7445653ff95b2b0f6f15fa6214d4e", - "packages/daemon/src/server/downloadable-artifact-http.ts#handleArtifactInventory": "sha256:24f071c082af80f2c50fb4f390bd8949f0f2c91e0bb58e67518b6cdcf6108aff", + "packages/daemon/src/server/downloadable-artifact-http.ts#handleArtifactDownload": "sha256:9f51e86c19cd4f5d8097f1c5ce38dc1daa795b803a6afc89d16cd556371ea037", + "packages/daemon/src/server/downloadable-artifact-http.ts#handleArtifactInventory": "sha256:651806eabbd78d51d4bb21dab0b8a28847fb6087941f73dee735c24d9c2a79fa", "packages/daemon/src/server/downloadable-artifact-http.ts#readArtifactId": "sha256:3472ee52d951fdf7ef00ee500298602c68f0e1e3b8c44ff51b09981efabf348c", "packages/daemon/src/server/downloadable-artifact-http.ts#readRequestPathname": "sha256:6b3a49c72365cf7da4552abef59862c073461f746025e28d409048815dd586f2", "packages/daemon/src/server/downloadable-artifact-http.ts#resolveDownloadableArtifactHttpRoute": "sha256:9fcdc9619e8d378e8f04ef7b3584863af446e855702ddce83a16ec194a6d98af", @@ -256,8 +256,13 @@ }, { "declaration": "packages/daemon/src/server/downloadable-artifact-http.ts#handleArtifactDownload", - "digest": "sha256:7f96d17b7c605230fb3cc21ceaa5b2e515d7445653ff95b2b0f6f15fa6214d4e", - "rationale": "The mid-stream error branch moved verbatim into the shared `failStreamedHttpResponse` (listed): same status codes before headers, same destroy after. Response framing on the success path is untouched." + "digest": "sha256:9f51e86c19cd4f5d8097f1c5ce38dc1daa795b803a6afc89d16cd556371ea037", + "rationale": "#3753 threads the daemon runtime's host environment into the directory-archive spawn instead of the module reading process.env. Request parsing, authorization, response framing, headers, and cleanup are unchanged; no peer-visible field moves." + }, + { + "declaration": "packages/daemon/src/server/downloadable-artifact-http.ts#handleArtifactInventory", + "digest": "sha256:651806eabbd78d51d4bb21dab0b8a28847fb6087941f73dee735c24d9c2a79fa", + "rationale": "#3753 threads the daemon runtime's host environment into the same archive spawn from the inventory route. The JSON body it writes and the error path are unchanged; no peer-visible field moves." }, { "declaration": "packages/kernel/src/errors.ts#normalizeError",