feat(launcher): add Native Test Mode for profile/command preview

Enables testing the LaunchProfile system from any device (no Mac/Electron
needed). When active, the Open button simulates the full native flow and
shows a debug popup with everything that WOULD be sent to Electron.

- ae_events_stores__launcher_defaults.ts: add native_test_mode boolean
  (persisted, default false) to LauncherLocState and launcher_loc_defaults.

- launcher_cfg_app_modes.svelte: add Native Test Mode checkbox toggle in
  the Advanced Toggles (Edit Mode Only) section with active-state warning.

- launcher_file_cont.svelte:
  - Add test_mode_popup_open/test_mode_popup_data state vars.
  - Add branch 0 in handle_open_file(): when native_test_mode + app_mode=native,
    skip all Electron calls; resolve the real LaunchProfile, build a data
    snapshot, open the debug popup.
  - Debug popup shows: file info, simulated temp path, cache/copy pass,
    resolved LaunchProfile fields, set_display_layout call, open command
    (run_cmd or open_local_file_v2 fallback), sleep delay, post-script
    (AppleScript or shell: prefix). Click backdrop or Close to dismiss.
This commit is contained in:
Scott Idem
2026-05-12 12:33:24 -04:00
parent 422c9c341c
commit ff824ebbe5
3 changed files with 194 additions and 1 deletions

View File

@@ -78,6 +78,13 @@ export interface LauncherLocState {
controller: string;
controller_group_code: string;
controller_client_id: string | null;
/**
* Native test mode: simulates the full native-branch open flow without Electron.
* Shows a debug popup with the resolved profile, commands, and AppleScript instead
* of actually launching files. Useful for testing LaunchProfile config from any
* device/OS without deploying to the Mac laptop.
*/
native_test_mode: boolean;
}
export interface LauncherSessState {
@@ -198,7 +205,8 @@ export const launcher_loc_defaults: LauncherLocState = {
controller: 'local',
controller_group_code: 'launcher-00',
controller_client_id: null
controller_client_id: null,
native_test_mode: false
// controller_cmd: null,
// controller_trigger_send: null,
};

View File

@@ -230,6 +230,32 @@ function apply_mode(mode: 'poster' | 'oral') {
</label>
</div>
</div>
<!-- 5. Native Test Mode (Edit Mode Only) -->
<!-- Simulates the full native open flow without Electron/Mac hardware.
Shows what commands, profile, and AppleScript WOULD be sent,
as a popup, instead of actually running them. -->
<div
class="border-surface-500/20 col-span-full mt-1 flex flex-col gap-2 border-t pt-3">
<p class="ml-1 text-[9px] font-bold uppercase opacity-50">
Dev / Testing
</p>
<label class="group flex cursor-pointer items-center gap-2 p-1">
<input
type="checkbox"
bind:checked={$events_loc.launcher.native_test_mode}
class="checkbox checkbox-sm" />
<span class="group-hover:text-warning-500 text-xs italic">
Native Test Mode
</span>
</label>
{#if $events_loc.launcher.native_test_mode}
<p class="ml-1 text-[8px] leading-tight italic text-yellow-400/70">
⚠ Active: Open buttons will simulate native launch and
show a debug popup instead of running commands.
</p>
{/if}
</div>
{/if}
</div>
</Launcher_Cfg_Section>

View File

@@ -91,6 +91,10 @@ let open_file_status: null | string = $state(null);
let open_file_status_message: null | string = $state(null);
let open_file_error_detail: string | null = $state(null);
/** State for the native test mode debug popup */
let test_mode_popup_open: boolean = $state(false);
let test_mode_popup_data: Record<string, any> | null = $state(null);
/** Simple promise-based delay for post-open script timing */
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
@@ -132,6 +136,43 @@ async function handle_open_file() {
$events_slct.event_file_id = event_file_id;
$events_slct.event_file_obj = event_file_obj;
// 0. NATIVE TEST MODE — simulate full native flow, show debug popup instead of running commands
// Active when native_test_mode toggle is on (regardless of is_native / app_mode).
// Lets you preview the resolved profile, open command, and post-script from any device.
if ($events_loc.launcher.native_test_mode && $events_loc.launcher.app_mode === 'native') {
open_file_clicked = true;
open_file_status = 'checking_cache';
open_file_status_message = 'Test Mode: simulating cache check...';
await sleep(400); // Brief simulated cache check
open_file_status = 'opening_file';
open_file_status_message = 'Test Mode: resolving launch profile...';
const profile = get_launch_profile(event_file_obj.extension, event_file_obj);
const open_cmd_resolved = profile.open_cmd
? profile.open_cmd.replaceAll('{{path}}', `/tmp/ae_test/${event_file_obj.filename}`)
: null;
test_mode_popup_data = {
filename: event_file_obj.filename,
extension: event_file_obj.extension,
hash_sha256: event_file_obj.hash_sha256,
simulated_temp_path: `/tmp/ae_test/${event_file_obj.filename}`,
profile,
open_cmd_resolved,
display_override: event_file_obj?.cfg_json?.display_override ?? null,
cache_check: 'PASS (simulated)',
copy_to_temp: 'PASS (simulated)'
};
test_mode_popup_open = true;
open_file_status = 'open';
open_file_status_message = 'Test Mode: profile resolved — see popup';
setTimeout(() => (open_file_clicked = false), 6000);
return true;
}
// 1. NATIVE MODE (Electron)
if ($ae_loc.is_native && $events_loc.launcher.app_mode === 'native') {
const cache_root = $ae_loc.local_file_cache_path;
@@ -595,3 +636,121 @@ function prevent_default<T extends Event>(fn: (event: T) => void) {
</span>
</span>
</div>
<!-- Native Test Mode Debug Popup -->
<!-- Shows what WOULD be sent to Electron: resolved profile, open command, post-script.
Appears when native_test_mode is active and a file is "opened". -->
{#if test_mode_popup_open && test_mode_popup_data}
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 p-4"
role="presentation"
onclick={() => (test_mode_popup_open = false)}>
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
<div
class="bg-surface-900 border-warning-500/40 relative flex max-h-[90vh] w-full max-w-2xl flex-col gap-0 overflow-hidden rounded-xl border shadow-2xl"
role="dialog"
aria-modal="true"
aria-label="Native Test Mode Debug Info"
tabindex="-1"
onclick={(e) => e.stopPropagation()}>
<!-- Header -->
<div class="bg-warning-500/10 border-warning-500/30 flex items-center gap-2 border-b px-4 py-3">
<span class="text-warning-400 font-mono text-xs font-bold uppercase tracking-wider">
🧪 Native Test Mode What would run on Mac
</span>
<button
type="button"
onclick={() => (test_mode_popup_open = false)}
class="btn btn-xs preset-tonal-surface ml-auto">
Close
</button>
</div>
<!-- Scrollable content -->
<div class="flex flex-col gap-3 overflow-y-auto p-4 font-mono text-xs">
<!-- File info -->
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">File</span>
<div class="rounded bg-black/30 px-3 py-2 leading-relaxed">
<div><span class="opacity-50">filename: </span>{test_mode_popup_data.filename}</div>
<div><span class="opacity-50">extension: </span>{test_mode_popup_data.extension}</div>
<div><span class="opacity-50">hash: </span><span class="opacity-60">{test_mode_popup_data.hash_sha256}</span></div>
<div><span class="opacity-50">temp path: </span><span class="text-green-400">{test_mode_popup_data.simulated_temp_path}</span></div>
</div>
</div>
<!-- Cache / copy -->
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Steps 12</span>
<div class="rounded bg-black/30 px-3 py-2 leading-relaxed">
<div><span class="opacity-50">check_hash_file_cache: </span><span class="text-green-400">{test_mode_popup_data.cache_check}</span></div>
<div><span class="opacity-50">copy_from_cache_to_temp: </span><span class="text-green-400">{test_mode_popup_data.copy_to_temp}</span></div>
</div>
</div>
<!-- Resolved profile -->
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Resolved LaunchProfile</span>
<div class="rounded bg-black/30 px-3 py-2 leading-relaxed">
<div><span class="opacity-50">app: </span><span class="text-primary-300">{test_mode_popup_data.profile.app}</span></div>
<div><span class="opacity-50">display_mode: </span><span class:text-primary-300={test_mode_popup_data.profile.display_mode === 'extend'} class:text-yellow-300={test_mode_popup_data.profile.display_mode === 'mirror'} class:opacity-40={test_mode_popup_data.profile.display_mode === 'none'}>{test_mode_popup_data.profile.display_mode}</span></div>
{#if test_mode_popup_data.display_override}
<div><span class="text-yellow-400 opacity-80">display_override (cfg_json): </span><span class="text-yellow-300">{test_mode_popup_data.display_override}</span></div>
{/if}
<div><span class="opacity-50">post_delay_ms: </span>{test_mode_popup_data.profile.post_delay_ms ?? 2000}</div>
</div>
</div>
<!-- Step 3: set_display_layout -->
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Step 3 set_display_layout</span>
<div class="rounded bg-black/30 px-3 py-2">
{#if test_mode_popup_data.profile.display_mode !== 'none'}
<span class="text-primary-300">native.set_display_layout({{ mode: '{test_mode_popup_data.profile.display_mode}' }})</span>
{:else}
<span class="opacity-40">skipped (display_mode: none)</span>
{/if}
</div>
</div>
<!-- Step 4: open command -->
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Step 4 — Open File</span>
<div class="rounded bg-black/30 px-3 py-2">
{#if test_mode_popup_data.open_cmd_resolved}
<div class="mb-1 opacity-50 text-[9px]">native.run_cmd()</div>
<pre class="whitespace-pre-wrap break-all text-green-300">{test_mode_popup_data.open_cmd_resolved}</pre>
{:else}
<div class="opacity-50 text-[9px]">native.open_local_file_v2()</div>
<span class="text-yellow-300">{test_mode_popup_data.simulated_temp_path}</span>
<span class="ml-2 opacity-40">(OS default handler)</span>
{/if}
</div>
</div>
<!-- Step 5: post-script -->
<div class="flex flex-col gap-1">
<span class="text-[9px] font-bold uppercase opacity-50">Steps 56 — Wait + Post-Script</span>
<div class="rounded bg-black/30 px-3 py-2">
<div class="mb-1 opacity-50">sleep({test_mode_popup_data.profile.post_delay_ms ?? 2000}ms)</div>
{#if test_mode_popup_data.profile.post_script}
{#if test_mode_popup_data.profile.post_script.startsWith('shell:')}
<div class="mb-1 opacity-50 text-[9px]">native.run_cmd() [shell prefix]</div>
<pre class="whitespace-pre-wrap break-all text-yellow-300">{test_mode_popup_data.profile.post_script.slice(6)}</pre>
{:else}
<div class="mb-1 opacity-50 text-[9px]">native.run_osascript() [AppleScript]</div>
<pre class="whitespace-pre-wrap text-purple-300">{test_mode_popup_data.profile.post_script}</pre>
{/if}
{:else}
<span class="opacity-40">no post_script</span>
{/if}
</div>
</div>
</div><!-- end scroll -->
</div>
</div>
{/if}