yaver-feedback-react-native 0.9.10 → 0.9.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,6 +17,13 @@ export interface DogfoodProject {
17
17
  workDir: string;
18
18
  framework: string;
19
19
  lane: DogfoodLane;
20
+ /**
21
+ * Optional automatic recovery lane. The shared onboarding flow uses the
22
+ * browser lane here when a user prefers Hermes or WebRTC for a project that
23
+ * can also render in a browser. The failed preferred attempt remains in the
24
+ * live console; the fallback is never silent.
25
+ */
26
+ fallbackLane?: DogfoodLane;
20
27
  /** Optional source URL for drivers that can clone missing source. */
21
28
  repositoryUrl?: string;
22
29
  /** Optional native target from /remote-runtime/capabilities for WebRTC. */
@@ -29,6 +36,12 @@ export interface DogfoodLaneOption {
29
36
  default: boolean;
30
37
  reason?: string;
31
38
  }
39
+ export interface DogfoodLanePlan {
40
+ preferred: DogfoodLane;
41
+ /** Browser is the cheap, portable recovery lane for RN/Expo/Flutter/web. */
42
+ fallback?: DogfoodLane;
43
+ options: DogfoodLaneOption[];
44
+ }
32
45
  /** One framework-to-lane matrix for Yaver and third-party consumers. */
33
46
  export declare function dogfoodLaneOptions(framework: string, capabilities?: {
34
47
  nativeRuntimeAvailable?: boolean;
@@ -40,6 +53,19 @@ export declare function defaultDogfoodLane(framework: string, capabilities?: {
40
53
  browserRuntimeAvailable?: boolean;
41
54
  selfDevelopment?: boolean;
42
55
  }): DogfoodLane;
56
+ /**
57
+ * Resolve one ordered lane policy for Yaver and embedded SDK hosts.
58
+ *
59
+ * Browser is the onboarding default for browser-capable React Native, Expo,
60
+ * Flutter, and web projects. When the user explicitly prefers Hermes or
61
+ * WebRTC, browser becomes the automatic second attempt. Native-only projects
62
+ * remain WebRTC-only and never advertise a browser recovery they cannot run.
63
+ */
64
+ export declare function dogfoodLanePlan(framework: string, capabilities?: {
65
+ nativeRuntimeAvailable?: boolean;
66
+ browserRuntimeAvailable?: boolean;
67
+ selfDevelopment?: boolean;
68
+ }, preferred?: DogfoodLane | null): DogfoodLanePlan;
43
69
  export interface DogfoodLogLine {
44
70
  text: string;
45
71
  at: number;
@@ -15,6 +15,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.DogfoodController = exports.dogfoodLogLinesFromDevEvent = exports.DogfoodRuntimeError = void 0;
16
16
  exports.dogfoodLaneOptions = dogfoodLaneOptions;
17
17
  exports.defaultDogfoodLane = defaultDogfoodLane;
18
+ exports.dogfoodLanePlan = dogfoodLanePlan;
18
19
  exports.validateDogfoodProject = validateDogfoodProject;
19
20
  exports.runtimeLogLinesFromDevEvent = runtimeLogLinesFromDevEvent;
20
21
  /** One framework-to-lane matrix for Yaver and third-party consumers. */
@@ -49,6 +50,26 @@ function defaultDogfoodLane(framework, capabilities = {}) {
49
50
  || options.find((option) => option.supported)?.lane
50
51
  || 'browser';
51
52
  }
53
+ /**
54
+ * Resolve one ordered lane policy for Yaver and embedded SDK hosts.
55
+ *
56
+ * Browser is the onboarding default for browser-capable React Native, Expo,
57
+ * Flutter, and web projects. When the user explicitly prefers Hermes or
58
+ * WebRTC, browser becomes the automatic second attempt. Native-only projects
59
+ * remain WebRTC-only and never advertise a browser recovery they cannot run.
60
+ */
61
+ function dogfoodLanePlan(framework, capabilities = {}, preferred) {
62
+ const options = dogfoodLaneOptions(framework, capabilities);
63
+ const supported = (lane) => !!lane && options.some((option) => option.lane === lane && option.supported);
64
+ const resolved = supported(preferred)
65
+ ? preferred
66
+ : defaultDogfoodLane(framework, capabilities);
67
+ return {
68
+ preferred: resolved,
69
+ fallback: resolved !== 'browser' && supported('browser') ? 'browser' : undefined,
70
+ options,
71
+ };
72
+ }
52
73
  class DogfoodRuntimeError extends Error {
53
74
  constructor(failure) {
54
75
  super(failure.error);
@@ -173,8 +194,8 @@ class DogfoodController {
173
194
  phase: 'preparing', attempt, project: this.project,
174
195
  message: `Preparing ${this.project.name}…`, logs: [], startedAt: Date.now(),
175
196
  });
176
- const context = {
177
- project: this.project,
197
+ const makeContext = (project) => ({
198
+ project,
178
199
  attempt,
179
200
  log: (line) => {
180
201
  if (generation !== this.generation)
@@ -201,16 +222,69 @@ class DogfoodController {
201
222
  this.cleanups.set(generation, owned);
202
223
  },
203
224
  isCurrent: () => generation === this.generation,
204
- };
225
+ });
226
+ let activeProject = this.project;
227
+ let context = makeContext(activeProject);
205
228
  try {
206
- await this.driver.prepare?.(context);
207
- if (!context.isCurrent())
208
- throw new DogfoodRuntimeError({
209
- code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this one.',
210
- remedy: 'Wait for the newer attempt.', retryable: true,
229
+ const start = async () => {
230
+ await this.driver.prepare?.(context);
231
+ if (!context.isCurrent())
232
+ throw new DogfoodRuntimeError({
233
+ code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this one.',
234
+ remedy: 'Wait for the newer attempt.', retryable: true,
235
+ });
236
+ context.setPhase('starting', `Starting ${activeProject.name} on the ${activeProject.lane} lane…`);
237
+ return this.driver.start(context);
238
+ };
239
+ let result;
240
+ try {
241
+ result = await start();
242
+ }
243
+ catch (primaryError) {
244
+ const primaryFailure = failureFrom(primaryError);
245
+ const fallbackLane = activeProject.fallbackLane;
246
+ const mayFallback = !!fallbackLane
247
+ && fallbackLane !== activeProject.lane
248
+ && primaryFailure.retryable
249
+ && primaryFailure.code !== 'DOGFOOD_ATTEMPT_REPLACED';
250
+ if (!mayFallback)
251
+ throw primaryError;
252
+ await this.runCleanups('all', generation);
253
+ const fallbackProject = {
254
+ ...activeProject,
255
+ lane: fallbackLane,
256
+ fallbackLane: undefined,
257
+ };
258
+ const fallbackInvalid = validateDogfoodProject(fallbackProject);
259
+ if (fallbackInvalid)
260
+ throw primaryError;
261
+ const fallbackLine = {
262
+ text: `[fallback] ${activeProject.lane} failed (${primaryFailure.code}); trying ${fallbackLane}`,
263
+ at: Date.now(),
264
+ stream: 'system',
265
+ };
266
+ activeProject = fallbackProject;
267
+ this.replace({
268
+ ...this.state,
269
+ project: activeProject,
270
+ phase: 'preparing',
271
+ message: `Recovering ${activeProject.name} in the ${fallbackLane} lane…`,
272
+ logs: [...this.state.logs, fallbackLine].slice(-this.maxLogLines),
273
+ lastOutputAt: fallbackLine.at,
274
+ failure: undefined,
211
275
  });
212
- context.setPhase('starting', `Starting ${this.project.name} on the ${this.project.lane} lane…`);
213
- const result = await this.driver.start(context);
276
+ context = makeContext(activeProject);
277
+ result = await start();
278
+ result = {
279
+ ...result,
280
+ metadata: {
281
+ ...result.metadata,
282
+ preferredLane: this.project.lane,
283
+ fallbackFrom: this.project.lane,
284
+ fallbackReason: primaryFailure.code,
285
+ },
286
+ };
287
+ }
214
288
  if (!context.isCurrent()) {
215
289
  await this.runCleanups('all', generation);
216
290
  throw new DogfoodRuntimeError({
@@ -218,7 +292,7 @@ class DogfoodController {
218
292
  remedy: 'Wait for the newer attempt.', retryable: true,
219
293
  });
220
294
  }
221
- this.replace({ ...this.state, phase: 'ready', message: `${this.project.name} is ready`, result, failure: undefined });
295
+ this.replace({ ...this.state, project: activeProject, phase: 'ready', message: `${activeProject.name} is ready`, result, failure: undefined });
222
296
  return result;
223
297
  }
224
298
  catch (error) {
@@ -32,6 +32,7 @@ export declare const DogfoodStatusRail: React.FC<{
32
32
  export declare const DogfoodLanePicker: React.FC<{
33
33
  options: readonly DogfoodLaneOption[];
34
34
  selected: DogfoodLane;
35
+ fallbackLane?: DogfoodLane;
35
36
  onSelect: (lane: DogfoodLane) => void;
36
37
  colors?: Partial<DogfoodUiColors>;
37
38
  showUnsupportedReasons?: boolean;
@@ -54,7 +54,7 @@ const DogfoodStatusRail = ({ steps, colors: colorOverrides }) => {
54
54
  };
55
55
  exports.DogfoodStatusRail = DogfoodStatusRail;
56
56
  /** One lane selector and one default policy across Yaver, SFMG, and Talos. */
57
- const DogfoodLanePicker = ({ options, selected, onSelect, colors: colorOverrides, showUnsupportedReasons = true }) => {
57
+ const DogfoodLanePicker = ({ options, selected, fallbackLane, onSelect, colors: colorOverrides, showUnsupportedReasons = true }) => {
58
58
  const colors = resolvedColors(colorOverrides);
59
59
  return (<react_native_1.View accessibilityRole="radiogroup" accessibilityLabel="Dogfood runtime lane">
60
60
  <react_native_1.View style={styles.choiceRow}>
@@ -69,7 +69,8 @@ const DogfoodLanePicker = ({ options, selected, onSelect, colors: colorOverrides
69
69
  },
70
70
  ]}>
71
71
  <react_native_1.Text style={[styles.choiceText, { color: colors.text }, active && styles.choiceTextActive]}>
72
- {option.label}{option.default ? ' · default' : ''}
72
+ {option.label}
73
+ {active ? ' · preferred' : fallbackLane === option.lane ? ' · automatic fallback' : option.default ? ' · default' : ''}
73
74
  </react_native_1.Text>
74
75
  </react_native_1.Pressable>);
75
76
  })}
@@ -316,12 +316,17 @@ const FeedbackModal = () => {
316
316
  return;
317
317
  await dogfoodControllerRef.current?.stop().catch(() => { });
318
318
  const framework = dogfoodProject.framework || onboarding.framework || 'expo';
319
+ const lanePlan = (0, DogfoodRuntime_1.dogfoodLanePlan)(framework, {
320
+ nativeRuntimeAvailable: dogfoodNativeAvailable,
321
+ browserRuntimeAvailable: dogfoodBrowserAvailable,
322
+ }, dogfoodLane);
319
323
  const controller = new DogfoodRuntime_1.DogfoodController({
320
324
  name: dogfoodProject.name,
321
325
  workDir: dogfoodProject.path,
322
326
  framework,
323
- lane: dogfoodLane,
324
- nativeTargetId: dogfoodLane === 'webrtc' ? dogfoodNativeTargetId : undefined,
327
+ lane: lanePlan.preferred,
328
+ fallbackLane: lanePlan.fallback,
329
+ nativeTargetId: lanePlan.preferred === 'webrtc' ? dogfoodNativeTargetId : undefined,
325
330
  }, (0, P2PDogfoodDriver_1.createP2PDogfoodDriver)(client), {
326
331
  onChange: (snapshot) => { if (mountedRef.current)
327
332
  setDogfoodRuntime(snapshot); },
@@ -533,7 +538,11 @@ const FeedbackModal = () => {
533
538
  setShowVibeInput(authenticated || directDogfood);
534
539
  setVibePrompt('');
535
540
  if (onboarding) {
536
- setActiveTab(authenticated ? 'chat' : 'settings');
541
+ // A Dogfood shortcut is an explicit setup/runtime intent. Opening on
542
+ // Chat hid the machine/runner/checkout gate for signed-in SFMG users;
543
+ // keep the SDK-owned Dogfood surface visible, then show its live logs
544
+ // immediately when Start is tapped.
545
+ setActiveTab('settings');
537
546
  setDogfoodSetupStage('setup');
538
547
  setDogfoodExpandedStep(null);
539
548
  setDogfoodRuntime(null);
@@ -958,6 +967,10 @@ const FeedbackModal = () => {
958
967
  nativeRuntimeAvailable: dogfoodNativeAvailable,
959
968
  browserRuntimeAvailable: dogfoodBrowserAvailable,
960
969
  });
970
+ const dogfoodLanePolicy = (0, DogfoodRuntime_1.dogfoodLanePlan)(dogfoodFramework, {
971
+ nativeRuntimeAvailable: dogfoodNativeAvailable,
972
+ browserRuntimeAvailable: dogfoodBrowserAvailable,
973
+ }, dogfoodLane);
961
974
  const selectedDogfoodNativeTarget = dogfoodNativeTargets.find((target) => target.id === dogfoodNativeTargetId) || null;
962
975
  const dogfoodLaneReady = dogfoodLaneChoices.some((option) => option.lane === dogfoodLane && option.supported)
963
976
  && (dogfoodLane !== 'webrtc' || !!selectedDogfoodNativeTarget?.enabled);
@@ -994,11 +1007,12 @@ const FeedbackModal = () => {
994
1007
  },
995
1008
  ];
996
1009
  const dogfoodStartBlocked = !dogfoodSetupReady || !dogfoodLaneReady;
997
- const dogfoodSourceLabel = dogfoodLane === 'webrtc'
1010
+ const activeDogfoodLane = dogfoodRuntime?.project.lane || dogfoodLane;
1011
+ const dogfoodSourceLabel = activeDogfoodLane === 'webrtc'
998
1012
  ? selectedDogfoodNativeTarget
999
1013
  ? [selectedDogfoodNativeTarget.label, selectedDogfoodNativeTarget.platform].filter(Boolean).join(' · ')
1000
1014
  : 'Native simulator, emulator, or device'
1001
- : dogfoodLane === 'hermes'
1015
+ : activeDogfoodLane === 'hermes'
1002
1016
  ? `Hermes build · ${machineCard.title}`
1003
1017
  : `${dogfoodFramework === 'flutter' ? 'Flutter web compiler' : 'Metro / browser build'} · ${machineCard.title}`;
1004
1018
  // Once the user fires off a vibe task, swap the entire modal body
@@ -1141,7 +1155,7 @@ const FeedbackModal = () => {
1141
1155
  <react_native_1.Text style={styles.dogfoodSmallActionText}>Back</react_native_1.Text>
1142
1156
  </react_native_1.Pressable>
1143
1157
  </react_native_1.View>
1144
- <DogfoodSessionUi_1.DogfoodLanePicker options={dogfoodLaneChoices} selected={dogfoodLane} colors={FeedbackModalTheme_1.FEEDBACK_DOGFOOD_LIGHT_COLORS} onSelect={(lane) => {
1158
+ <DogfoodSessionUi_1.DogfoodLanePicker options={dogfoodLaneChoices} selected={dogfoodLane} fallbackLane={dogfoodLanePolicy.fallback} colors={FeedbackModalTheme_1.FEEDBACK_DOGFOOD_LIGHT_COLORS} onSelect={(lane) => {
1145
1159
  setDogfoodLane(lane);
1146
1160
  const appId = YaverFeedback_1.YaverFeedback.getDogfoodOnboarding()?.appId;
1147
1161
  if (appId)
@@ -78,6 +78,37 @@ describe('DogfoodController', () => {
78
78
  await controller.stop();
79
79
  expect(stopSecond).toHaveBeenCalledTimes(1);
80
80
  });
81
+ test('keeps a failed preferred lane in the console and automatically recovers through browser', async () => {
82
+ const stopPreferred = jest.fn();
83
+ const lanes = [];
84
+ const controller = new DogfoodRuntime_1.DogfoodController({
85
+ ...expo,
86
+ lane: 'hermes',
87
+ fallbackLane: 'browser',
88
+ }, {
89
+ async start(ctx) {
90
+ lanes.push(ctx.project.lane);
91
+ if (ctx.project.lane === 'hermes') {
92
+ ctx.registerCleanup(stopPreferred);
93
+ throw new DogfoodRuntime_1.DogfoodRuntimeError({
94
+ code: 'DOGFOOD_HERMES_BUILD_FAILED',
95
+ error: 'Hermes build failed',
96
+ remedy: 'Use the browser build.',
97
+ retryable: true,
98
+ });
99
+ }
100
+ return { lane: 'browser', url: 'http://agent/dev/', metadata: { recovered: true } };
101
+ },
102
+ });
103
+ await expect(controller.trigger()).resolves.toMatchObject({
104
+ lane: 'browser',
105
+ metadata: { fallbackFrom: 'hermes', fallbackReason: 'DOGFOOD_HERMES_BUILD_FAILED' },
106
+ });
107
+ expect(lanes).toEqual(['hermes', 'browser']);
108
+ expect(stopPreferred).toHaveBeenCalledTimes(1);
109
+ expect(controller.snapshot()).toMatchObject({ phase: 'ready', project: { lane: 'browser' } });
110
+ expect(controller.snapshot().logs.map((line) => line.text)).toContain('[fallback] hermes failed (DOGFOOD_HERMES_BUILD_FAILED); trying browser');
111
+ });
81
112
  });
82
113
  describe('Dogfood lanes and console events', () => {
83
114
  test('Flutter is first-class on browser but cannot be mislabeled Hermes', () => {
@@ -95,6 +126,20 @@ describe('Dogfood lanes and console events', () => {
95
126
  expect((0, DogfoodRuntime_1.defaultDogfoodLane)('flutter')).toBe('browser');
96
127
  expect(flutter.find((option) => option.lane === 'hermes')?.supported).toBe(false);
97
128
  });
129
+ test('uses browser as the framework-aware default and as second choice after an explicit native preference', () => {
130
+ expect((0, DogfoodRuntime_1.dogfoodLanePlan)('flutter', { nativeRuntimeAvailable: true })).toMatchObject({
131
+ preferred: 'browser', fallback: undefined,
132
+ });
133
+ expect((0, DogfoodRuntime_1.dogfoodLanePlan)('expo', { nativeRuntimeAvailable: true }, 'hermes')).toMatchObject({
134
+ preferred: 'hermes', fallback: 'browser',
135
+ });
136
+ expect((0, DogfoodRuntime_1.dogfoodLanePlan)('flutter', { nativeRuntimeAvailable: true }, 'webrtc')).toMatchObject({
137
+ preferred: 'webrtc', fallback: 'browser',
138
+ });
139
+ expect((0, DogfoodRuntime_1.dogfoodLanePlan)('swift', { nativeRuntimeAvailable: true }, 'webrtc')).toMatchObject({
140
+ preferred: 'webrtc', fallback: undefined,
141
+ });
142
+ });
98
143
  test('keeps Yaver self-development on the same RN three-lane contract', () => {
99
144
  const options = (0, DogfoodRuntime_1.dogfoodLaneOptions)('expo', { nativeRuntimeAvailable: true, selfDevelopment: true });
100
145
  expect(options).toHaveLength(3);
@@ -39,11 +39,16 @@ describe('FeedbackModal authenticated chat contract', () => {
39
39
  .toBeGreaterThanOrEqual(4.5);
40
40
  });
41
41
  it('uses Chat as the authenticated entry surface without legacy command buttons', () => {
42
- expect(source).toContain("setActiveTab(authenticated ? 'chat' : 'settings')");
42
+ expect(source).toContain("useState<'chat' | 'settings'>('chat')");
43
43
  expect(source).toContain('setShowVibeInput(authenticated || directDogfood)');
44
44
  expect(source).not.toContain('Screenshot & Fix');
45
45
  expect(source).not.toContain('<DeployPanel');
46
46
  });
47
+ it('opens explicit Dogfood onboarding on setup and makes the runtime console the first live surface', () => {
48
+ expect(source).toContain("setActiveTab('settings')");
49
+ expect(source).toContain("setDogfoodSetupStage('runtime')");
50
+ expect(source).toMatch(/dogfoodSetupStage === 'runtime'[\s\S]*?<DogfoodLiveConsole/);
51
+ });
47
52
  it('keeps Dogfood setup to box, runner, and checkout before asking for a runtime', () => {
48
53
  const setupSteps = source.match(/const dogfoodSetupSteps = \[([\s\S]*?)\n \];/)?.[1] || '';
49
54
  expect([...setupSteps.matchAll(/key: '([^']+)'/g)].map((match) => match[1]))
@@ -56,7 +61,9 @@ describe('FeedbackModal authenticated chat contract', () => {
56
61
  expect(source).toContain('label="Choose runtime"');
57
62
  });
58
63
  it('passes the selected native target and labels the live log source', () => {
59
- expect(source).toContain("nativeTargetId: dogfoodLane === 'webrtc' ? dogfoodNativeTargetId : undefined");
64
+ expect(source).toContain("nativeTargetId: lanePlan.preferred === 'webrtc' ? dogfoodNativeTargetId : undefined");
65
+ expect(source).toContain('fallbackLane: lanePlan.fallback');
66
+ expect(source).toContain('fallbackLane={dogfoodLanePolicy.fallback}');
60
67
  expect(source).toMatch(/<DogfoodLiveConsole[\s\S]*?sourceLabel=\{dogfoodSourceLabel\}/);
61
68
  expect(source).toContain('Simulator, emulator, or device');
62
69
  });
package/dist/index.d.ts CHANGED
@@ -56,8 +56,8 @@ export { resolveSDKDogfood } from './dogfoodPolicy';
56
56
  export type { DogfoodAccessSnapshot, DogfoodFlowSnapshot, SDKDogfoodConfig, SDKDogfoodStatus } from './dogfoodPolicy';
57
57
  export { YaverDeviceDogfood } from './deviceDogfood';
58
58
  export type { DeviceDogfoodOptions, DeviceDogfoodSession, DeviceDogfoodState } from './deviceDogfood';
59
- export { DogfoodController, DogfoodRuntimeError, defaultDogfoodLane, dogfoodLaneOptions, dogfoodLogLinesFromDevEvent, runtimeLogLinesFromDevEvent, validateDogfoodProject, } from './DogfoodRuntime';
60
- export type { DogfoodControllerOptions, DogfoodDriver, DogfoodFailure, DogfoodLane, DogfoodLaneOption, DogfoodLogLine, DogfoodPhase, DogfoodProject, DogfoodResult, DogfoodRunContext, DogfoodSnapshot, } from './DogfoodRuntime';
59
+ export { DogfoodController, DogfoodRuntimeError, defaultDogfoodLane, dogfoodLanePlan, dogfoodLaneOptions, dogfoodLogLinesFromDevEvent, runtimeLogLinesFromDevEvent, validateDogfoodProject, } from './DogfoodRuntime';
60
+ export type { DogfoodControllerOptions, DogfoodDriver, DogfoodFailure, DogfoodLane, DogfoodLaneOption, DogfoodLanePlan, DogfoodLogLine, DogfoodPhase, DogfoodProject, DogfoodResult, DogfoodRunContext, DogfoodSnapshot, } from './DogfoodRuntime';
61
61
  export { DogfoodLanePicker, DogfoodLiveConsole, DogfoodStatusRail } from './DogfoodSessionUi';
62
62
  export type { DogfoodStatusStep, DogfoodStatusTone, DogfoodUiColors, } from './DogfoodSessionUi';
63
63
  export { FeedbackModal } from './FeedbackModal';
package/dist/index.js CHANGED
@@ -28,8 +28,8 @@
28
28
  * ```
29
29
  */
30
30
  Object.defineProperty(exports, "__esModule", { value: true });
31
- exports.getToken = exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.setPreferredDogfoodLane = exports.getPreferredDogfoodLane = exports.clearQuickIconDisabled = exports.setQuickIconDisabled = exports.getQuickIconDisabled = exports.FixReport = exports.QuickActionIcon = exports.DogfoodQuickControls = exports.FeedbackModal = exports.DogfoodStatusRail = exports.DogfoodLiveConsole = exports.DogfoodLanePicker = exports.validateDogfoodProject = exports.runtimeLogLinesFromDevEvent = exports.dogfoodLogLinesFromDevEvent = exports.dogfoodLaneOptions = exports.defaultDogfoodLane = exports.DogfoodRuntimeError = exports.DogfoodController = exports.YaverDeviceDogfood = exports.resolveSDKDogfood = exports.isYaverModeBadgeHidden = exports.showYaverModeBadge = exports.hideYaverModeBadge = exports.YaverModeBadge = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.PairDeviceModal = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.RELOAD_APP_PATH = exports.RELOAD_PATH = exports.describeReloadFailure = exports.reloadFrameworkFamily = exports.reloadRequest = exports.reloadActions = exports.createP2PDogfoodDriver = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.captureStoreScreenshots = exports.YaverFeedback = void 0;
32
- exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.pickFeedbackFile = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getDogfoodAccountAccess = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = void 0;
31
+ exports.getWebBaseUrl = exports.getConvexSiteUrl = exports.configureAuthEndpoints = exports.setPreferredDogfoodLane = exports.getPreferredDogfoodLane = exports.clearQuickIconDisabled = exports.setQuickIconDisabled = exports.getQuickIconDisabled = exports.FixReport = exports.QuickActionIcon = exports.DogfoodQuickControls = exports.FeedbackModal = exports.DogfoodStatusRail = exports.DogfoodLiveConsole = exports.DogfoodLanePicker = exports.validateDogfoodProject = exports.runtimeLogLinesFromDevEvent = exports.dogfoodLogLinesFromDevEvent = exports.dogfoodLaneOptions = exports.dogfoodLanePlan = exports.defaultDogfoodLane = exports.DogfoodRuntimeError = exports.DogfoodController = exports.YaverDeviceDogfood = exports.resolveSDKDogfood = exports.isYaverModeBadgeHidden = exports.showYaverModeBadge = exports.hideYaverModeBadge = exports.YaverModeBadge = exports.FloatingButton = exports.ShakeDetector = exports.AuthOverlay = exports.PairDeviceModal = exports.YaverMachinePickerScreen = exports.YaverLoginScreen = exports.YaverConnectionScreen = exports.RELOAD_APP_PATH = exports.RELOAD_PATH = exports.describeReloadFailure = exports.reloadFrameworkFamily = exports.reloadRequest = exports.reloadActions = exports.createP2PDogfoodDriver = exports.P2PClient = exports.YaverDiscovery = exports.initExpo = exports.YaverUpdates = exports.BlackBox = exports.captureStoreScreenshots = exports.YaverFeedback = void 0;
32
+ exports.uploadFeedback = exports.isVideoRecording = exports.stopVideoRecording = exports.startVideoRecording = exports.pickFeedbackFile = exports.captureScreenshot = exports.DEFAULT_OAUTH_REDIRECT = exports.DEFAULT_WEB_BASE_URL = exports.DEFAULT_CONVEX_SITE_URL = exports.listReachableDevices = exports.loginWithEmail = exports.signupWithEmail = exports.signInWithOAuth = exports.signInWithApple = exports.validateToken = exports.clearSelectedDeviceId = exports.saveSelectedDeviceId = exports.getDogfoodAccountAccess = exports.getSelectedDeviceId = exports.saveUser = exports.getUser = exports.clearToken = exports.saveToken = exports.getToken = void 0;
33
33
  var YaverFeedback_1 = require("./YaverFeedback");
34
34
  Object.defineProperty(exports, "YaverFeedback", { enumerable: true, get: function () { return YaverFeedback_1.YaverFeedback; } });
35
35
  var storeShots_1 = require("./storeShots");
@@ -83,6 +83,7 @@ var DogfoodRuntime_1 = require("./DogfoodRuntime");
83
83
  Object.defineProperty(exports, "DogfoodController", { enumerable: true, get: function () { return DogfoodRuntime_1.DogfoodController; } });
84
84
  Object.defineProperty(exports, "DogfoodRuntimeError", { enumerable: true, get: function () { return DogfoodRuntime_1.DogfoodRuntimeError; } });
85
85
  Object.defineProperty(exports, "defaultDogfoodLane", { enumerable: true, get: function () { return DogfoodRuntime_1.defaultDogfoodLane; } });
86
+ Object.defineProperty(exports, "dogfoodLanePlan", { enumerable: true, get: function () { return DogfoodRuntime_1.dogfoodLanePlan; } });
86
87
  Object.defineProperty(exports, "dogfoodLaneOptions", { enumerable: true, get: function () { return DogfoodRuntime_1.dogfoodLaneOptions; } });
87
88
  Object.defineProperty(exports, "dogfoodLogLinesFromDevEvent", { enumerable: true, get: function () { return DogfoodRuntime_1.dogfoodLogLinesFromDevEvent; } });
88
89
  Object.defineProperty(exports, "runtimeLogLinesFromDevEvent", { enumerable: true, get: function () { return DogfoodRuntime_1.runtimeLogLinesFromDevEvent; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yaver-feedback-react-native",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "description": "Visual feedback SDK for Yaver — bug reports, screen recording, voice vibe coding, and local-first developer workflows",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -27,6 +27,13 @@ export interface DogfoodProject {
27
27
  workDir: string;
28
28
  framework: string;
29
29
  lane: DogfoodLane;
30
+ /**
31
+ * Optional automatic recovery lane. The shared onboarding flow uses the
32
+ * browser lane here when a user prefers Hermes or WebRTC for a project that
33
+ * can also render in a browser. The failed preferred attempt remains in the
34
+ * live console; the fallback is never silent.
35
+ */
36
+ fallbackLane?: DogfoodLane;
30
37
  /** Optional source URL for drivers that can clone missing source. */
31
38
  repositoryUrl?: string;
32
39
  /** Optional native target from /remote-runtime/capabilities for WebRTC. */
@@ -41,6 +48,13 @@ export interface DogfoodLaneOption {
41
48
  reason?: string;
42
49
  }
43
50
 
51
+ export interface DogfoodLanePlan {
52
+ preferred: DogfoodLane;
53
+ /** Browser is the cheap, portable recovery lane for RN/Expo/Flutter/web. */
54
+ fallback?: DogfoodLane;
55
+ options: DogfoodLaneOption[];
56
+ }
57
+
44
58
  /** One framework-to-lane matrix for Yaver and third-party consumers. */
45
59
  export function dogfoodLaneOptions(
46
60
  framework: string,
@@ -89,6 +103,36 @@ export function defaultDogfoodLane(
89
103
  || 'browser';
90
104
  }
91
105
 
106
+ /**
107
+ * Resolve one ordered lane policy for Yaver and embedded SDK hosts.
108
+ *
109
+ * Browser is the onboarding default for browser-capable React Native, Expo,
110
+ * Flutter, and web projects. When the user explicitly prefers Hermes or
111
+ * WebRTC, browser becomes the automatic second attempt. Native-only projects
112
+ * remain WebRTC-only and never advertise a browser recovery they cannot run.
113
+ */
114
+ export function dogfoodLanePlan(
115
+ framework: string,
116
+ capabilities: {
117
+ nativeRuntimeAvailable?: boolean;
118
+ browserRuntimeAvailable?: boolean;
119
+ selfDevelopment?: boolean;
120
+ } = {},
121
+ preferred?: DogfoodLane | null,
122
+ ): DogfoodLanePlan {
123
+ const options = dogfoodLaneOptions(framework, capabilities);
124
+ const supported = (lane: DogfoodLane | null | undefined) =>
125
+ !!lane && options.some((option) => option.lane === lane && option.supported);
126
+ const resolved = supported(preferred)
127
+ ? preferred!
128
+ : defaultDogfoodLane(framework, capabilities);
129
+ return {
130
+ preferred: resolved,
131
+ fallback: resolved !== 'browser' && supported('browser') ? 'browser' : undefined,
132
+ options,
133
+ };
134
+ }
135
+
92
136
  export interface DogfoodLogLine {
93
137
  text: string;
94
138
  at: number;
@@ -291,8 +335,8 @@ export class DogfoodController {
291
335
  message: `Preparing ${this.project.name}…`, logs: [], startedAt: Date.now(),
292
336
  });
293
337
 
294
- const context: DogfoodRunContext = {
295
- project: this.project,
338
+ const makeContext = (project: DogfoodProject): DogfoodRunContext => ({
339
+ project,
296
340
  attempt,
297
341
  log: (line) => {
298
342
  if (generation !== this.generation) return;
@@ -316,16 +360,69 @@ export class DogfoodController {
316
360
  this.cleanups.set(generation, owned);
317
361
  },
318
362
  isCurrent: () => generation === this.generation,
319
- };
363
+ });
364
+
365
+ let activeProject = this.project;
366
+ let context = makeContext(activeProject);
320
367
 
321
368
  try {
322
- await this.driver.prepare?.(context);
323
- if (!context.isCurrent()) throw new DogfoodRuntimeError({
324
- code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this one.',
325
- remedy: 'Wait for the newer attempt.', retryable: true,
326
- });
327
- context.setPhase('starting', `Starting ${this.project.name} on the ${this.project.lane} lane…`);
328
- const result = await this.driver.start(context);
369
+ const start = async (): Promise<DogfoodResult> => {
370
+ await this.driver.prepare?.(context);
371
+ if (!context.isCurrent()) throw new DogfoodRuntimeError({
372
+ code: 'DOGFOOD_ATTEMPT_REPLACED', error: 'A newer Dogfood attempt replaced this one.',
373
+ remedy: 'Wait for the newer attempt.', retryable: true,
374
+ });
375
+ context.setPhase('starting', `Starting ${activeProject.name} on the ${activeProject.lane} lane…`);
376
+ return this.driver.start(context);
377
+ };
378
+
379
+ let result: DogfoodResult;
380
+ try {
381
+ result = await start();
382
+ } catch (primaryError) {
383
+ const primaryFailure = failureFrom(primaryError);
384
+ const fallbackLane = activeProject.fallbackLane;
385
+ const mayFallback = !!fallbackLane
386
+ && fallbackLane !== activeProject.lane
387
+ && primaryFailure.retryable
388
+ && primaryFailure.code !== 'DOGFOOD_ATTEMPT_REPLACED';
389
+ if (!mayFallback) throw primaryError;
390
+
391
+ await this.runCleanups('all', generation);
392
+ const fallbackProject: DogfoodProject = {
393
+ ...activeProject,
394
+ lane: fallbackLane!,
395
+ fallbackLane: undefined,
396
+ };
397
+ const fallbackInvalid = validateDogfoodProject(fallbackProject);
398
+ if (fallbackInvalid) throw primaryError;
399
+ const fallbackLine: DogfoodLogLine = {
400
+ text: `[fallback] ${activeProject.lane} failed (${primaryFailure.code}); trying ${fallbackLane}`,
401
+ at: Date.now(),
402
+ stream: 'system',
403
+ };
404
+ activeProject = fallbackProject;
405
+ this.replace({
406
+ ...this.state,
407
+ project: activeProject,
408
+ phase: 'preparing',
409
+ message: `Recovering ${activeProject.name} in the ${fallbackLane} lane…`,
410
+ logs: [...this.state.logs, fallbackLine].slice(-this.maxLogLines),
411
+ lastOutputAt: fallbackLine.at,
412
+ failure: undefined,
413
+ });
414
+ context = makeContext(activeProject);
415
+ result = await start();
416
+ result = {
417
+ ...result,
418
+ metadata: {
419
+ ...result.metadata,
420
+ preferredLane: this.project.lane,
421
+ fallbackFrom: this.project.lane,
422
+ fallbackReason: primaryFailure.code,
423
+ },
424
+ };
425
+ }
329
426
  if (!context.isCurrent()) {
330
427
  await this.runCleanups('all', generation);
331
428
  throw new DogfoodRuntimeError({
@@ -333,7 +430,7 @@ export class DogfoodController {
333
430
  remedy: 'Wait for the newer attempt.', retryable: true,
334
431
  });
335
432
  }
336
- this.replace({ ...this.state, phase: 'ready', message: `${this.project.name} is ready`, result, failure: undefined });
433
+ this.replace({ ...this.state, project: activeProject, phase: 'ready', message: `${activeProject.name} is ready`, result, failure: undefined });
337
434
  return result;
338
435
  } catch (error) {
339
436
  const failure = failureFrom(error);
@@ -101,10 +101,11 @@ export const DogfoodStatusRail: React.FC<{
101
101
  export const DogfoodLanePicker: React.FC<{
102
102
  options: readonly DogfoodLaneOption[];
103
103
  selected: DogfoodLane;
104
+ fallbackLane?: DogfoodLane;
104
105
  onSelect: (lane: DogfoodLane) => void;
105
106
  colors?: Partial<DogfoodUiColors>;
106
107
  showUnsupportedReasons?: boolean;
107
- }> = ({ options, selected, onSelect, colors: colorOverrides, showUnsupportedReasons = true }) => {
108
+ }> = ({ options, selected, fallbackLane, onSelect, colors: colorOverrides, showUnsupportedReasons = true }) => {
108
109
  const colors = resolvedColors(colorOverrides);
109
110
  return (
110
111
  <View accessibilityRole="radiogroup" accessibilityLabel="Dogfood runtime lane">
@@ -128,7 +129,8 @@ export const DogfoodLanePicker: React.FC<{
128
129
  ]}
129
130
  >
130
131
  <Text style={[styles.choiceText, { color: colors.text }, active && styles.choiceTextActive]}>
131
- {option.label}{option.default ? ' · default' : ''}
132
+ {option.label}
133
+ {active ? ' · preferred' : fallbackLane === option.lane ? ' · automatic fallback' : option.default ? ' · default' : ''}
132
134
  </Text>
133
135
  </Pressable>
134
136
  );
@@ -40,6 +40,7 @@ import {
40
40
  import {
41
41
  DogfoodController,
42
42
  defaultDogfoodLane,
43
+ dogfoodLanePlan,
43
44
  dogfoodLaneOptions,
44
45
  type DogfoodLane,
45
46
  type DogfoodSnapshot,
@@ -389,12 +390,17 @@ export const FeedbackModal: React.FC = () => {
389
390
  if (!client || !dogfoodProject || !onboarding) return;
390
391
  await dogfoodControllerRef.current?.stop().catch(() => {});
391
392
  const framework = dogfoodProject.framework || onboarding.framework || 'expo';
393
+ const lanePlan = dogfoodLanePlan(framework, {
394
+ nativeRuntimeAvailable: dogfoodNativeAvailable,
395
+ browserRuntimeAvailable: dogfoodBrowserAvailable,
396
+ }, dogfoodLane);
392
397
  const controller = new DogfoodController({
393
398
  name: dogfoodProject.name,
394
399
  workDir: dogfoodProject.path,
395
400
  framework,
396
- lane: dogfoodLane,
397
- nativeTargetId: dogfoodLane === 'webrtc' ? dogfoodNativeTargetId : undefined,
401
+ lane: lanePlan.preferred,
402
+ fallbackLane: lanePlan.fallback,
403
+ nativeTargetId: lanePlan.preferred === 'webrtc' ? dogfoodNativeTargetId : undefined,
398
404
  }, createP2PDogfoodDriver(client), {
399
405
  onChange: (snapshot) => { if (mountedRef.current) setDogfoodRuntime(snapshot); },
400
406
  });
@@ -608,7 +614,11 @@ export const FeedbackModal: React.FC = () => {
608
614
  setShowVibeInput(authenticated || directDogfood);
609
615
  setVibePrompt('');
610
616
  if (onboarding) {
611
- setActiveTab(authenticated ? 'chat' : 'settings');
617
+ // A Dogfood shortcut is an explicit setup/runtime intent. Opening on
618
+ // Chat hid the machine/runner/checkout gate for signed-in SFMG users;
619
+ // keep the SDK-owned Dogfood surface visible, then show its live logs
620
+ // immediately when Start is tapped.
621
+ setActiveTab('settings');
612
622
  setDogfoodSetupStage('setup');
613
623
  setDogfoodExpandedStep(null);
614
624
  setDogfoodRuntime(null);
@@ -1056,6 +1066,10 @@ export const FeedbackModal: React.FC = () => {
1056
1066
  nativeRuntimeAvailable: dogfoodNativeAvailable,
1057
1067
  browserRuntimeAvailable: dogfoodBrowserAvailable,
1058
1068
  });
1069
+ const dogfoodLanePolicy = dogfoodLanePlan(dogfoodFramework, {
1070
+ nativeRuntimeAvailable: dogfoodNativeAvailable,
1071
+ browserRuntimeAvailable: dogfoodBrowserAvailable,
1072
+ }, dogfoodLane);
1059
1073
  const selectedDogfoodNativeTarget = dogfoodNativeTargets.find((target) => target.id === dogfoodNativeTargetId) || null;
1060
1074
  const dogfoodLaneReady = dogfoodLaneChoices.some((option) => option.lane === dogfoodLane && option.supported)
1061
1075
  && (dogfoodLane !== 'webrtc' || !!selectedDogfoodNativeTarget?.enabled);
@@ -1092,11 +1106,12 @@ export const FeedbackModal: React.FC = () => {
1092
1106
  },
1093
1107
  ];
1094
1108
  const dogfoodStartBlocked = !dogfoodSetupReady || !dogfoodLaneReady;
1095
- const dogfoodSourceLabel = dogfoodLane === 'webrtc'
1109
+ const activeDogfoodLane = dogfoodRuntime?.project.lane || dogfoodLane;
1110
+ const dogfoodSourceLabel = activeDogfoodLane === 'webrtc'
1096
1111
  ? selectedDogfoodNativeTarget
1097
1112
  ? [selectedDogfoodNativeTarget.label, selectedDogfoodNativeTarget.platform].filter(Boolean).join(' · ')
1098
1113
  : 'Native simulator, emulator, or device'
1099
- : dogfoodLane === 'hermes'
1114
+ : activeDogfoodLane === 'hermes'
1100
1115
  ? `Hermes build · ${machineCard.title}`
1101
1116
  : `${dogfoodFramework === 'flutter' ? 'Flutter web compiler' : 'Metro / browser build'} · ${machineCard.title}`;
1102
1117
 
@@ -1347,6 +1362,7 @@ export const FeedbackModal: React.FC = () => {
1347
1362
  <DogfoodLanePicker
1348
1363
  options={dogfoodLaneChoices}
1349
1364
  selected={dogfoodLane}
1365
+ fallbackLane={dogfoodLanePolicy.fallback}
1350
1366
  colors={FEEDBACK_DOGFOOD_LIGHT_COLORS}
1351
1367
  onSelect={(lane) => {
1352
1368
  setDogfoodLane(lane);
@@ -2,6 +2,7 @@ import {
2
2
  DogfoodController,
3
3
  DogfoodRuntimeError,
4
4
  defaultDogfoodLane,
5
+ dogfoodLanePlan,
5
6
  dogfoodLaneOptions,
6
7
  dogfoodLogLinesFromDevEvent,
7
8
  validateDogfoodProject,
@@ -92,6 +93,41 @@ describe('DogfoodController', () => {
92
93
  await controller.stop();
93
94
  expect(stopSecond).toHaveBeenCalledTimes(1);
94
95
  });
96
+
97
+ test('keeps a failed preferred lane in the console and automatically recovers through browser', async () => {
98
+ const stopPreferred = jest.fn();
99
+ const lanes: string[] = [];
100
+ const controller = new DogfoodController({
101
+ ...expo,
102
+ lane: 'hermes',
103
+ fallbackLane: 'browser',
104
+ }, {
105
+ async start(ctx) {
106
+ lanes.push(ctx.project.lane);
107
+ if (ctx.project.lane === 'hermes') {
108
+ ctx.registerCleanup(stopPreferred);
109
+ throw new DogfoodRuntimeError({
110
+ code: 'DOGFOOD_HERMES_BUILD_FAILED',
111
+ error: 'Hermes build failed',
112
+ remedy: 'Use the browser build.',
113
+ retryable: true,
114
+ });
115
+ }
116
+ return { lane: 'browser', url: 'http://agent/dev/', metadata: { recovered: true } };
117
+ },
118
+ });
119
+
120
+ await expect(controller.trigger()).resolves.toMatchObject({
121
+ lane: 'browser',
122
+ metadata: { fallbackFrom: 'hermes', fallbackReason: 'DOGFOOD_HERMES_BUILD_FAILED' },
123
+ });
124
+ expect(lanes).toEqual(['hermes', 'browser']);
125
+ expect(stopPreferred).toHaveBeenCalledTimes(1);
126
+ expect(controller.snapshot()).toMatchObject({ phase: 'ready', project: { lane: 'browser' } });
127
+ expect(controller.snapshot().logs.map((line) => line.text)).toContain(
128
+ '[fallback] hermes failed (DOGFOOD_HERMES_BUILD_FAILED); trying browser',
129
+ );
130
+ });
95
131
  });
96
132
 
97
133
  describe('Dogfood lanes and console events', () => {
@@ -112,6 +148,21 @@ describe('Dogfood lanes and console events', () => {
112
148
  expect(flutter.find((option) => option.lane === 'hermes')?.supported).toBe(false);
113
149
  });
114
150
 
151
+ test('uses browser as the framework-aware default and as second choice after an explicit native preference', () => {
152
+ expect(dogfoodLanePlan('flutter', { nativeRuntimeAvailable: true })).toMatchObject({
153
+ preferred: 'browser', fallback: undefined,
154
+ });
155
+ expect(dogfoodLanePlan('expo', { nativeRuntimeAvailable: true }, 'hermes')).toMatchObject({
156
+ preferred: 'hermes', fallback: 'browser',
157
+ });
158
+ expect(dogfoodLanePlan('flutter', { nativeRuntimeAvailable: true }, 'webrtc')).toMatchObject({
159
+ preferred: 'webrtc', fallback: 'browser',
160
+ });
161
+ expect(dogfoodLanePlan('swift', { nativeRuntimeAvailable: true }, 'webrtc')).toMatchObject({
162
+ preferred: 'webrtc', fallback: undefined,
163
+ });
164
+ });
165
+
115
166
  test('keeps Yaver self-development on the same RN three-lane contract', () => {
116
167
  const options = dogfoodLaneOptions('expo', { nativeRuntimeAvailable: true, selfDevelopment: true });
117
168
  expect(options).toHaveLength(3);
@@ -46,12 +46,18 @@ describe('FeedbackModal authenticated chat contract', () => {
46
46
  });
47
47
 
48
48
  it('uses Chat as the authenticated entry surface without legacy command buttons', () => {
49
- expect(source).toContain("setActiveTab(authenticated ? 'chat' : 'settings')");
49
+ expect(source).toContain("useState<'chat' | 'settings'>('chat')");
50
50
  expect(source).toContain('setShowVibeInput(authenticated || directDogfood)');
51
51
  expect(source).not.toContain('Screenshot & Fix');
52
52
  expect(source).not.toContain('<DeployPanel');
53
53
  });
54
54
 
55
+ it('opens explicit Dogfood onboarding on setup and makes the runtime console the first live surface', () => {
56
+ expect(source).toContain("setActiveTab('settings')");
57
+ expect(source).toContain("setDogfoodSetupStage('runtime')");
58
+ expect(source).toMatch(/dogfoodSetupStage === 'runtime'[\s\S]*?<DogfoodLiveConsole/);
59
+ });
60
+
55
61
  it('keeps Dogfood setup to box, runner, and checkout before asking for a runtime', () => {
56
62
  const setupSteps = source.match(/const dogfoodSetupSteps = \[([\s\S]*?)\n \];/)?.[1] || '';
57
63
  expect([...setupSteps.matchAll(/key: '([^']+)'/g)].map((match) => match[1]))
@@ -65,7 +71,9 @@ describe('FeedbackModal authenticated chat contract', () => {
65
71
  });
66
72
 
67
73
  it('passes the selected native target and labels the live log source', () => {
68
- expect(source).toContain("nativeTargetId: dogfoodLane === 'webrtc' ? dogfoodNativeTargetId : undefined");
74
+ expect(source).toContain("nativeTargetId: lanePlan.preferred === 'webrtc' ? dogfoodNativeTargetId : undefined");
75
+ expect(source).toContain('fallbackLane: lanePlan.fallback');
76
+ expect(source).toContain('fallbackLane={dogfoodLanePolicy.fallback}');
69
77
  expect(source).toMatch(/<DogfoodLiveConsole[\s\S]*?sourceLabel=\{dogfoodSourceLabel\}/);
70
78
  expect(source).toContain('Simulator, emulator, or device');
71
79
  });
package/src/index.ts CHANGED
@@ -92,6 +92,7 @@ export {
92
92
  DogfoodController,
93
93
  DogfoodRuntimeError,
94
94
  defaultDogfoodLane,
95
+ dogfoodLanePlan,
95
96
  dogfoodLaneOptions,
96
97
  dogfoodLogLinesFromDevEvent,
97
98
  runtimeLogLinesFromDevEvent,
@@ -103,6 +104,7 @@ export type {
103
104
  DogfoodFailure,
104
105
  DogfoodLane,
105
106
  DogfoodLaneOption,
107
+ DogfoodLanePlan,
106
108
  DogfoodLogLine,
107
109
  DogfoodPhase,
108
110
  DogfoodProject,