Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/calm-story-observations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@fluentui-react-native/desktop-driver': patch
'@fluentui-react-native/storybook-desktop': patch
---

Add bounded, service-local story selection timings and per-case execution/lifecycle observations without changing authenticated readiness, preview remounts, or worker/session isolation. Part of #4435.

Use authenticated direct navigation for driver-owned story selection and cache complete extracted test maps by source content and observed helper digests within the owning run. Preserve normal args support, protected args refusal, manifest-digest verification, and the legacy HTTP default for direct library consumers. Report process-history truncation separately from possible per-case loss; preview generations infer remounts rather than independently counting them.
128 changes: 128 additions & 0 deletions packages/agentic/desktop-driver/references/current-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,134 @@ probe timed out after 10 seconds; bridge retirement occurred about 19 seconds
after injection. A later attempt waited for retirement and retained the
same Partial result. Physical Ctrl+C and DPI changes were not exercised.

## 2026-11-12: bounded refresh timing observations

Part of #4435. Story selection can return allowlisted, service-local timing
observations and a canonical native-verified PID/start-time pair. They are
response diagnostics, not authenticated marker fields, context identity, or
authority to clear quarantine. Request/run/epoch matching and fresh native
marker verification remain unchanged; provider payloads and private values
are not copied into these observations.

The instrumented baseline ran eight independently launched accessible cohorts
per macOS renderer, each with three passing ListboxItem callbacks and one
declared macOS-unsupported callback. Each passing callback selected a fresh
preview/context generation; keyed remounts are inferred from rendering
policy, not independently counted. Every cohort retained one verified app
incarnation and one authenticated bridge connection during its callbacks.
JS initialization/HMR was not observed independently and cannot be inferred
from hello counts. Fabric accepted one hello and Paper two on the same socket.
No activation command or main/focused-window change was observed in these
baseline cohorts; Paper recorded one application deactivation. Historical
per-case truncation counts are withdrawn: a process-lifetime history flag
and globally allocated sequence gaps did not establish per-case event loss.
A zero advisory count
is not visual qualification.
The Storybook evidence ledger,
`packages/agentic/storybook-desktop/references/current-status.md`, records
selection, timing, variability, and limitations.

This timing implementation changes no Swift source, physical-input
activation, foreground checks, hit-testing, focus, or window-switch policy.
It does not qualify those paths or remove provider-local checks. Windows
Fabric and Win32 Paper were **not run locally** for this change.

The direct-selection Fabric candidate preserved identical coverage, fresh
per-case generations, one app incarnation/connection per lifecycle, and no
within-lifecycle app replacements across eight paired-by-index repetitions.
Runner p50/p95 changed from 7939.07/8845.37 to 6680.04/6995.31 ms; navigation
p50 changed from 753.13 to 274.20 ms. Paper's equivalent eight-plus-eight
comparison changed runner p50/p95 from 7915.99/8755.09 to 7218.78/7811.59 ms
and navigation p50 from 756.77 to 340.59 ms, with identical coverage and
lifecycle invariants. Each side/renderer passed 24 callbacks plus eight
declared unsupported. Sequential before/after blocks, cold startup
differences, and the small observational cohort limit attribution;
native-verification p95 worsened on both renderers (Fabric 168.00→546.11 ms;
Paper 170.07→582.90 ms). There is no universal tail-latency, reliability,
startup, or flashing claim.

Window-scoped synchronized recordings on both sides/renderers contained four
encoded frames each, followed by held pixels. The unvalidated whole-content
uniformity detector returned zero candidates; persistent chrome and
window-only capture prevent interpreting this as zero blank-preview or
activation-flash episodes. No flash reduction was established.

Focused candidate qualification on both macOS renderers passed the complete
223-story render traversal plus five accessible callbacks (228 passes per
renderer), and a separate sparse physical selection with 15 callbacks
(238 passes per renderer). Button, Dropdown, native Callout, and modern
Callout paths retained fresh per-case contexts/workers and passed popup
isolation/dismiss/reopen checks. These are not full authored-input suites,
MCP descriptor qualification, native cancellation/fault injection, or
qualification of another branch's physical-input corrections.

The unchanged standalone macOS helper passed a release build with
`-Xswiftc -warnings-as-errors` and all 448 self-test checks. Focused
`FURN_NATIVE_DRIVER_TEST=1` contracts passed 87 tests, with 16 skipped
(Windows contracts not run locally). An earlier local aggregate passed with `TF_BUILD=1` serializing package Jest
runs and with project-local fixture overrides; this is not PR CI mode.
The PR workflow uses parallel `yarn lage buildci` without those overrides.
The Storybook evidence ledger records exact commands, failed attempts,
package counts, and the subsequent plain-command review qualification.

### Activation trace and deferred flashing change

The W3C click path activates before its server-side hit test. The operation
click path conditionally raises an externally obscured target and rechecks;
in-window obstruction remains a no-effect refusal. The native physical-click
provider independently activates under its physical-input scope, rereads the
target, and verifies ownership of the click point. These are separate
postconditions, not interchangeable duplicate calls. Removing the server
activation can move hit-testing before foreground establishment; removing the
provider activation assumes ownership cannot change between commands and
weakens callers that use the provider independently.

macOS activation sets main/raise attributes even when already frontmost.
The menu test helper `inWindowWith` also calls activating `switchToWindow`
while polling each candidate window and restoring its owner. Both are
plausible flashing contributors, not established visual causes in the
observational ListboxItem cohort.

No activation optimization is shipped here. Concurrent physical-input
corrections retain foreground/main-window postconditions, add a workspace
fallback when acknowledged AX activation is ineffective, and keep genuine
occlusion refused. An unqualified dedupe must not fork those semantics.
Zero activation counters in this read-only cohort do not meet the optimization
gate. The compatible experiment and its qualification conditions are tracked
in [remaining work](remaining-work.md#macos-activation-and-popup-polling).

## 2026-11-12: refresh-diagnostic review corrections

Part of #4435. The Storybook report now separates the native process-lifetime
`eventsTruncated` flag into `processEventsTruncated` from a possible
per-case `eventsTruncated` warning. A case warning requires truncated
history whose earliest retained event is newer than the case cursor.
Globally allocated sequence gaps are not loss evidence; the unsupported
command-truncation inference was removed. Historical 16/17-of-24
comparisons are withdrawn because the original bounded reports cannot
reconstruct per-case loss. The duplicate mount-generation field was also
removed: fresh preview generations infer remounting but do not count mounts
independently. No native producer or activation behavior changed.

The earlier `TF_BUILD=1` aggregate was local serialization, not PR CI mode.
Under the shared native lock, plain
`yarn clean-lage-cache && yarn lage buildci` with no environment overrides
**failed in both candidate runs** on the `boundedCommand` termination-signal
assertion; `privateStorage` passed in both. The identical
command on independently installed, detached base `aadc2f359` **failed**
that same assertion and a `privateStorage` timeout. The base worktree was
removed after terminating its verified orphaned Jest worker. Those
pre-existing tests and implementations were not changed.

Review regressions **passed 112 tests** across four Storybook suites.
macOS Paper manifest/bundle and full-index accessible smoke **passed**
(223 rendered stories plus five callbacks, three extractions/two cache
hits). A harness PATH failure before selection was corrected by invoking
it through Yarn, without a product change. macOS Fabric and physical
lanes were **not rerun for this follow-up**; Windows Fabric and Win32 Paper
were **not run locally**. The earlier paired timings and window recordings
were not repeated, and no new performance or flashing claim is made.

## 2026-11-12: macOS exact application identity backend

Source `76a2be59abfde3a7bcb0abd8d86a2052e7378274`, macOS 27.2.1 arm64,
Expand Down
10 changes: 10 additions & 0 deletions packages/agentic/desktop-driver/references/protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,16 @@ detached root reports `stale element reference`.

## Storybook readiness

Part of #4435. Successful selection/reset responses may include `diagnostics`
with fixed numeric preparation, HTTP completion, correlated-ready,
native-verification and marker-poll observations, authenticated hello/connection
counts, and the native-verified application PID/start time. Providers fill an
optional local `StoryProviderContext.diagnostics`; it is never sent to the
runtime. The service allowlists finite nonnegative numbers before returning
them. These observations are not authenticated marker fields, are not part of
context identity, and never authorize readiness or recovery. In particular,
hello counts are not JavaScript initialization counts.

When a target provides a `StoryOrchestrator`, the consumer validates:

- instance, target, endpoint, and catalog identity;
Expand Down
21 changes: 21 additions & 0 deletions packages/agentic/desktop-driver/references/remaining-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,27 @@ contracts into the owning references and remove completed work here.

## 1. Architecture decision

### macOS activation and popup polling

Part of #4435. The 2026-11-12 refresh-reduction work deliberately leaves
activation semantics unchanged; the [status ledger](current-status.md)
records the source trace and bounded observational evidence.

Measure activating menu/popup polling before changing it. Trial explicitly
non-activating, window-scoped reads rather than silently changing
`switchToWindow`. Separately, skipping a raise requires fresh foreground,
main-window, and visibility evidence within the provider's physical-input
scope. Neither recent server activation nor an acknowledged AX request alone
proves that postcondition. Preserve independent provider callers, real
occlusion refusal, hit-testing, focus, cancellation, and input serialization.

Completion requires paired target-window recordings and matching physical,
accessibility, popup, and multiwindow qualification on both macOS renderers,
compatible with the concurrent physical-input corrections. No implementation
or flashing reduction is claimed by this measurement-only decision.

### Shared operation architecture

**Build an MCP-first agent interface over a transport-neutral operation layer
inside the existing driver service. Keep the CLI as a thin secondary client.
Do not create another permanent daemon.**
Expand Down
42 changes: 37 additions & 5 deletions packages/agentic/desktop-driver/src/server/sessionStories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { OperationCurrentStory, OperationStories } from '../operations/runt
import { HostStaleError, WebDriverError } from '../protocol/errors.js';
import { attachFailureEffect, readFailureEffect } from '../protocol/settlement.js';
import { withCommandTimeout } from '../protocol/timeouts.js';
import type { StoryOrchestrator, StoryReadyResult, StorySelectionRequest } from '../storybook.js';
import type { StoryOrchestrator, StoryReadyResult, StorySelectionDiagnostics, StorySelectionRequest } from '../storybook.js';
import { matchesStoryMarker, parseStoryMarker, StoryBridgeUnavailableError } from '../storybook.js';
import type { DesktopSession, SessionManager } from './SessionManager.js';

Expand All @@ -32,7 +32,8 @@ export async function changeStory(
try {
return await withCommandTimeout(
async (signal) => {
const context = { lifetimeId: session.id, signal };
const diagnostics: StorySelectionDiagnostics = {};
const context = { lifetimeId: session.id, signal, diagnostics };
const result = await (change === 'reset'
? orchestrator.resetStory(request, context)
: orchestrator.selectStory(request, context));
Expand All @@ -43,11 +44,36 @@ export async function changeStory(
) {
throw new WebDriverError('unknown error', 'Storybook returned readiness for a different selection.');
}
const verified = await verifyStoryMarker(session, result, signal);
const started = performance.now();
const verified = await verifyStoryMarker(session, result, signal, diagnostics);
diagnostics.nativeVerificationMs = performance.now() - started;
signal.throwIfAborted();
unavailableContexts.delete(session);
session.story = result;
return { result, verified };
const observations: NonNullable<StoryReadyResult['diagnostics']> = {};
for (const key of [
'preparationMs',
'httpMs',
'runtimeReadyMs',
'nativeVerificationMs',
'nativeMarkerPolls',
'authenticatedHellos',
'bridgeConnections',
] as const) {
const value = diagnostics[key];
if (typeof value === 'number' && Number.isFinite(value) && value >= 0) observations[key] = value;
}
const { processId, processStartedAt } = session.lease;
if (
Number.isSafeInteger(processId) &&
processId! > 0 &&
processStartedAt &&
processStartedAt.length <= 64 &&
Number.isFinite(Date.parse(processStartedAt))
) {
observations.application = { processId: processId!, processStartedAt: new Date(processStartedAt).toISOString() };
}
return { result: { ...result, diagnostics: observations }, verified };
},
session.desktopTimeouts.storyRender,
'Changing Storybook context',
Expand Down Expand Up @@ -152,7 +178,12 @@ export async function assertStoryContext(session: DesktopSession, sessions: Sess
}

/** Polls the story root until it shows `expected`. Returns false when the target has no native story root. */
async function verifyStoryMarker(session: DesktopSession, expected: StoryReadyResult, signal: AbortSignal): Promise<boolean> {
async function verifyStoryMarker(
session: DesktopSession,
expected: StoryReadyResult,
signal: AbortSignal,
diagnostics?: StorySelectionDiagnostics,
): Promise<boolean> {
const testId = session.target.storyRootTestId;
if (!testId) {
if (expected.markerVersion === 1) {
Expand All @@ -162,6 +193,7 @@ async function verifyStoryMarker(session: DesktopSession, expected: StoryReadyRe
}
for (;;) {
signal.throwIfAborted();
if (diagnostics) diagnostics.nativeMarkerPolls = (diagnostics.nativeMarkerPolls ?? 0) + 1;
const markers = await readMarkers(session, testId, signal);
if (markers.some((marker) => matchesStoryMarker(marker, expected))) {
return true;
Expand Down
24 changes: 24 additions & 0 deletions packages/agentic/desktop-driver/src/server/storyOperations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ async function withStories(body: (fixture: Fixture) => Promise<void>, options: F
}

describe('story operations', () => {
test('selection reports local phase timing and the verified app incarnation without serializing provider context', async () => {
await withStories(async ({ harness, session }) => {
const state = harness.server.sessions.get(session.id);
state.lease.processId = 123;
state.lease.processStartedAt = '2026-11-12T00:00:00.000Z';
const select = harness.storyOrchestrator!.selectStory.bind(harness.storyOrchestrator);
harness.storyOrchestrator!.selectStory = async (request, context) => {
Object.assign(context?.diagnostics ?? {}, { preparationMs: 2, runtimeReadyMs: 3, nonce: 'never-report-this' });
return select(request, context);
};
const ready = await session.selectStory(manifest.entries[0].id);
expect(ready.diagnostics).toMatchObject({
preparationMs: 2,
runtimeReadyMs: 3,
nativeVerificationMs: expect.any(Number),
nativeMarkerPolls: 1,
application: { processId: 123, processStartedAt: '2026-11-12T00:00:00.000Z' },
});
expect(JSON.stringify(ready)).not.toContain('never-report-this');
expect(ready).not.toHaveProperty('signal');
expect(ready).not.toHaveProperty('lifetimeId');
});
});

test('built-in manifest, current, reset and args routes all propagate local provider context', async () => {
await withStories(async ({ harness, session }) => {
const orchestrator = harness.storyOrchestrator!;
Expand Down
16 changes: 16 additions & 0 deletions packages/agentic/desktop-driver/src/storybook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,28 @@ export type StoryReadyResult = {
requestId?: string;
runId: string;
storyId: string;
/** Service-local observations, not part of the authenticated runtime marker or readiness decision. */
diagnostics?: StorySelectionDiagnostics & {
application?: { processId: number; processStartedAt: string };
};
};

export type StorySelectionDiagnostics = {
preparationMs?: number;
httpMs?: number;
runtimeReadyMs?: number;
nativeVerificationMs?: number;
nativeMarkerPolls?: number;
authenticatedHellos?: number;
bridgeConnections?: number;
};

/** Local provider lifetime; never included in channel messages or serialized operation inputs. */
export type StoryProviderContext = {
lifetimeId: string;
signal: AbortSignal;
/** Optional, fixed numeric observations. Never include runtime payloads or credentials here. */
diagnostics?: StorySelectionDiagnostics;
};

export class StoryProviderError extends Error {
Expand Down
8 changes: 8 additions & 0 deletions packages/agentic/storybook-desktop/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ Read [integration](references/integration.md) for app wiring and
[story-tests](references/story-tests.md) to author inline `wdio` callbacks.
Use `--reporter json` for bounded machine-readable summaries or NDJSON for
live progress.
Executable story artifacts include bounded phase/lifecycle observations;
see [story-test diagnostics](references/story-tests.md#timing-and-lifecycle-observations).
Part of #4435.
Driver-owned navigation selects directly on the authenticated runtime socket,
then verifies correlated readiness and the native marker without a competing
HTTP mutation; see [readiness](references/readiness.md). Test extraction is
cached by source content within its owning run, without reusing previews,
workers, or sessions.
Agent-served channels reject alternate HTTP mutations and never relay client
WebSocket frames; selection/reset go through the session-owning driver grant,
and args remain unsupported. Non-agent channels are explicitly legacy
Expand Down
1 change: 1 addition & 0 deletions packages/agentic/storybook-desktop/config/server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ function createChannelOrchestrator({ channelServer, driverManifest, serverUrl, S
serverUrl,
recovery,
controlled: driverManifest.agent !== undefined,
selectionTransport: 'direct',
});
}

Expand Down
Loading
Loading