@elizaos/app-core 2.0.0-alpha.70 → 2.0.0-alpha.71
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.
- package/.turbo/turbo-build.log +1 -0
- package/package.json +4 -4
- package/src/api/client.ts +3 -0
- package/src/bridge/electrobun-rpc.ts +1 -0
- package/src/components/FlaminaGuide.tsx +2 -2
- package/src/components/avatar/VrmEngine.ts +7 -0
- package/src/components/avatar/VrmViewer.tsx +2 -2
- package/src/components/onboarding/ConnectionStep.tsx +7 -5
- package/src/components/onboarding/OnboardingStepNav.tsx +4 -1
- package/src/config/branding.ts +2 -0
- package/src/hooks/useMiladyBar.ts +4 -4
- package/src/state/AppContext.tsx +50 -21
- package/src/state/types.ts +15 -0
- package/src/state/vrm.ts +58 -26
- package/test/app/cloud-login-lock.test.ts +4 -0
- package/test/app/connection-mode-persistence.test.ts +411 -0
- package/test/app/onboarding-finish-lock.test.ts +3 -0
- package/test/app/startup-backend-missing.e2e.test.ts +8 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -2,3 +2,4 @@ $ bun run build:dist
|
|
|
2
2
|
$ test -f ../ui/dist/index.d.ts || (cd ../ui && bun run build) && rimraf dist && tsc -p tsconfig.build.json && node ../../scripts/copy-package-assets.mjs packages/app-core src/styles src/i18n/locales && node ../../scripts/prepare-package-dist.mjs packages/app-core
|
|
3
3
|
error: Script not found "build"
|
|
4
4
|
error: script "build:dist" exited with code 1
|
|
5
|
+
error: script "build" exited with code 1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elizaos/app-core",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.71",
|
|
4
4
|
"description": "Shared application core for Milady shells and white-label apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
"@capacitor/haptics": "8.0.0",
|
|
69
69
|
"@capacitor/keyboard": "8.0.0",
|
|
70
70
|
"@capacitor/preferences": "^8.0.1",
|
|
71
|
-
"@elizaos/autonomous": "2.0.0-alpha.
|
|
72
|
-
"@elizaos/ui": "2.0.0-alpha.
|
|
71
|
+
"@elizaos/autonomous": "2.0.0-alpha.71",
|
|
72
|
+
"@elizaos/ui": "2.0.0-alpha.71",
|
|
73
73
|
"@sparkjsdev/spark": "^0.1.10",
|
|
74
74
|
"lucide-react": "^0.575.0",
|
|
75
75
|
"three": "^0.182.0",
|
|
@@ -87,5 +87,5 @@
|
|
|
87
87
|
"typescript": "^5.9.3",
|
|
88
88
|
"vitest": "^4.0.18"
|
|
89
89
|
},
|
|
90
|
-
"gitHead": "
|
|
90
|
+
"gitHead": "c150a8cf4bffddd7e934f3a6073a092ca1cd6520"
|
|
91
91
|
}
|
package/src/api/client.ts
CHANGED
|
@@ -177,7 +177,7 @@ export function DeferredSetupChecklist({
|
|
|
177
177
|
</div>
|
|
178
178
|
|
|
179
179
|
<div className="space-y-2">
|
|
180
|
-
{onboardingDeferredTasks.map((task) => (
|
|
180
|
+
{(onboardingDeferredTasks as FlaminaGuideTopic[]).map((task) => (
|
|
181
181
|
<div
|
|
182
182
|
key={task}
|
|
183
183
|
className="flex flex-col gap-2 rounded-xl border border-border/50 bg-bg/50 px-3 py-3 md:flex-row md:items-center md:justify-between"
|
|
@@ -199,7 +199,7 @@ export function DeferredSetupChecklist({
|
|
|
199
199
|
<button
|
|
200
200
|
type="button"
|
|
201
201
|
className="rounded-full border border-border/60 px-3 py-1.5 text-[11px] font-semibold uppercase tracking-[0.12em] text-muted transition-colors hover:text-txt"
|
|
202
|
-
onClick={() => markDone(task)}
|
|
202
|
+
onClick={() => markDone(task as FlaminaGuideTopic)}
|
|
203
203
|
>
|
|
204
204
|
Done
|
|
205
205
|
</button>
|
|
@@ -838,6 +838,9 @@ export class VrmEngine {
|
|
|
838
838
|
if (!this.scene || !this.renderer) return;
|
|
839
839
|
|
|
840
840
|
const { SparkRenderer } = await this.loadSparkModule();
|
|
841
|
+
// Re-check after async import — the engine may have been disposed during
|
|
842
|
+
// the await (e.g. React StrictMode double-mount).
|
|
843
|
+
if (!this.scene || !this.renderer) return;
|
|
841
844
|
const sparkRenderer = new SparkRenderer({
|
|
842
845
|
renderer: this.renderer as THREE.WebGLRenderer,
|
|
843
846
|
apertureAngle: 0.0,
|
|
@@ -1911,7 +1914,11 @@ export class VrmEngine {
|
|
|
1911
1914
|
}
|
|
1912
1915
|
|
|
1913
1916
|
await this.ensureSparkRenderer();
|
|
1917
|
+
// Re-check after async — the engine may have been disposed during the
|
|
1918
|
+
// await (e.g. React StrictMode double-mount or rapid navigation).
|
|
1919
|
+
if (!this.scene || this.loadingAborted || requestId !== this.worldLoadRequestId) return;
|
|
1914
1920
|
const spark = await this.loadSparkModule();
|
|
1921
|
+
if (!this.scene || this.loadingAborted || requestId !== this.worldLoadRequestId) return;
|
|
1915
1922
|
const { SplatMesh } = spark;
|
|
1916
1923
|
let worldAnchor = new THREE.Vector3(0, 0, 0);
|
|
1917
1924
|
let worldRevealRadius = 1;
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* the `mouthOpen` prop. Sized to fill its parent container.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { resolveAppAssetUrl } from "@elizaos/app-core/utils";
|
|
9
8
|
import { useEffect, useEffectEvent, useRef } from "react";
|
|
9
|
+
import { getVrmUrl } from "../../state/vrm";
|
|
10
10
|
import {
|
|
11
11
|
type CameraProfile,
|
|
12
12
|
type InteractionMode,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
type VrmEngineState,
|
|
16
16
|
} from "./VrmEngine";
|
|
17
17
|
|
|
18
|
-
const DEFAULT_VRM_PATH =
|
|
18
|
+
const DEFAULT_VRM_PATH = getVrmUrl(1);
|
|
19
19
|
|
|
20
20
|
export type VrmViewerProps = {
|
|
21
21
|
/** When false the loaded scene stays resident but the render loop is paused */
|
|
@@ -293,17 +293,19 @@ export function ConnectionStep() {
|
|
|
293
293
|
}
|
|
294
294
|
};
|
|
295
295
|
|
|
296
|
-
// On native (mobile)
|
|
297
|
-
//
|
|
296
|
+
// On native (mobile) or when branding requires cloud-only, force sandbox mode
|
|
297
|
+
// — skip straight to Eliza Cloud login. These platforms cannot run a local
|
|
298
|
+
// backend, so cloud is the only option.
|
|
299
|
+
const forceCloud = isNative || branding.cloudOnly;
|
|
298
300
|
useEffect(() => {
|
|
299
|
-
if (
|
|
301
|
+
if (forceCloud && !onboardingRunMode) {
|
|
300
302
|
setState("onboardingRunMode", "cloud");
|
|
301
303
|
setState("onboardingCloudProvider", "elizacloud");
|
|
302
304
|
setState("onboardingProvider", "");
|
|
303
305
|
setState("onboardingApiKey", "");
|
|
304
306
|
setState("onboardingPrimaryModel", "");
|
|
305
307
|
}
|
|
306
|
-
}, [
|
|
308
|
+
}, [forceCloud, onboardingRunMode, setState]);
|
|
307
309
|
|
|
308
310
|
if (!showProviderSelection) {
|
|
309
311
|
if (!onboardingRunMode) {
|
|
@@ -319,7 +321,7 @@ export function ConnectionStep() {
|
|
|
319
321
|
{t("onboarding.hostingQuestion")}
|
|
320
322
|
</div>
|
|
321
323
|
<div className="onboarding-provider-grid">
|
|
322
|
-
{!isNative && (
|
|
324
|
+
{!isNative && !branding.cloudOnly && (
|
|
323
325
|
<button
|
|
324
326
|
type="button"
|
|
325
327
|
className="onboarding-provider-card onboarding-provider-card--recommended"
|
|
@@ -6,11 +6,14 @@ export function OnboardingStepNav() {
|
|
|
6
6
|
const branding = useBranding();
|
|
7
7
|
|
|
8
8
|
const isEliza = branding.appName === "Eliza";
|
|
9
|
+
const isCloudOnly = !!branding.cloudOnly;
|
|
9
10
|
const activeSteps = isEliza
|
|
10
11
|
? ONBOARDING_STEPS.filter(
|
|
11
12
|
(s) => s.id === "connection" || s.id === "activate",
|
|
12
13
|
)
|
|
13
|
-
:
|
|
14
|
+
: isCloudOnly
|
|
15
|
+
? ONBOARDING_STEPS.filter((s) => s.id !== "wakeUp")
|
|
16
|
+
: ONBOARDING_STEPS;
|
|
14
17
|
|
|
15
18
|
const currentIndex = activeSteps.findIndex((s) => s.id === onboardingStep);
|
|
16
19
|
|
package/src/config/branding.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface BrandingConfig {
|
|
|
43
43
|
packageScope: string;
|
|
44
44
|
/** Custom providers injected by the app into the onboarding flow */
|
|
45
45
|
customProviders?: CustomProviderOption[];
|
|
46
|
+
/** When true, the app requires Eliza Cloud — local backend mode is disabled. */
|
|
47
|
+
cloudOnly?: boolean;
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
export const DEFAULT_BRANDING: BrandingConfig = {
|
|
@@ -18,10 +18,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
18
18
|
import {
|
|
19
19
|
type DetectedProvider,
|
|
20
20
|
invokeDesktopBridgeRequest,
|
|
21
|
-
scanAndValidateProviderCredentials,
|
|
22
21
|
scanProviderCredentials,
|
|
23
22
|
subscribeDesktopBridgeEvent,
|
|
24
23
|
} from "../bridge/electrobun-rpc";
|
|
24
|
+
import type { Tab } from "../navigation";
|
|
25
25
|
import { isDesktopPlatform } from "../platform";
|
|
26
26
|
import { useApp } from "../state";
|
|
27
27
|
|
|
@@ -423,8 +423,8 @@ export function useMiladyBar() {
|
|
|
423
423
|
// Validated scan — for background refresh and manual refresh
|
|
424
424
|
const runValidatedScan = useCallback(() => {
|
|
425
425
|
if (!isDesktopPlatform()) return;
|
|
426
|
-
|
|
427
|
-
.then((results) => {
|
|
426
|
+
scanProviderCredentials()
|
|
427
|
+
.then((results: DetectedProvider[]) => {
|
|
428
428
|
setScannedProviders(results);
|
|
429
429
|
setLastRefreshAt(Date.now());
|
|
430
430
|
setNow(Date.now());
|
|
@@ -514,7 +514,7 @@ export function useMiladyBar() {
|
|
|
514
514
|
}
|
|
515
515
|
if (itemId.startsWith("navigate-")) {
|
|
516
516
|
const target = itemId.replace("navigate-", "");
|
|
517
|
-
setTab?.(target);
|
|
517
|
+
setTab?.(target as Tab);
|
|
518
518
|
void invokeDesktopBridgeRequest({
|
|
519
519
|
rpcMethod: "desktopShowWindow",
|
|
520
520
|
ipcChannel: "desktop:showWindow",
|
package/src/state/AppContext.tsx
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
useState,
|
|
15
15
|
} from "react";
|
|
16
16
|
import { prepareDraftForSave } from "../actions/character";
|
|
17
|
+
import { BrandingContext, DEFAULT_BRANDING } from "../config/branding";
|
|
17
18
|
import {
|
|
18
19
|
type AgentStartupDiagnostics,
|
|
19
20
|
type AgentStatus,
|
|
@@ -424,7 +425,13 @@ function getFlaminaTopicForOnboardingStep(
|
|
|
424
425
|
|
|
425
426
|
// ── Provider ───────────────────────────────────────────────────────────
|
|
426
427
|
|
|
427
|
-
export function AppProvider({
|
|
428
|
+
export function AppProvider({
|
|
429
|
+
children,
|
|
430
|
+
branding: brandingOverride,
|
|
431
|
+
}: {
|
|
432
|
+
children: ReactNode;
|
|
433
|
+
branding?: Partial<import("../config/branding").BrandingConfig>;
|
|
434
|
+
}) {
|
|
428
435
|
const [lastNativeTab, setLastNativeTabState] =
|
|
429
436
|
useState<Tab>(loadLastNativeTab);
|
|
430
437
|
// --- Core state ---
|
|
@@ -804,7 +811,9 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
804
811
|
|
|
805
812
|
// --- Onboarding ---
|
|
806
813
|
const [onboardingStep, setOnboardingStepRaw] = useState<OnboardingStep>(
|
|
807
|
-
() =>
|
|
814
|
+
() =>
|
|
815
|
+
loadPersistedOnboardingStep() ??
|
|
816
|
+
(brandingOverride?.cloudOnly ? "identity" : "wakeUp"),
|
|
808
817
|
);
|
|
809
818
|
const [onboardingMode, setOnboardingMode] =
|
|
810
819
|
useState<AppState["onboardingMode"]>("basic");
|
|
@@ -826,8 +835,10 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
826
835
|
const [onboardingStyle, setOnboardingStyle] = useState("");
|
|
827
836
|
const [onboardingRunMode, setOnboardingRunMode] = useState<
|
|
828
837
|
"local" | "cloud" | ""
|
|
829
|
-
>("");
|
|
830
|
-
const [onboardingCloudProvider, setOnboardingCloudProvider] = useState(
|
|
838
|
+
>(brandingOverride?.cloudOnly ? "cloud" : "");
|
|
839
|
+
const [onboardingCloudProvider, setOnboardingCloudProvider] = useState(
|
|
840
|
+
brandingOverride?.cloudOnly ? "elizacloud" : "",
|
|
841
|
+
);
|
|
831
842
|
const [onboardingSmallModel, setOnboardingSmallModel] = useState(
|
|
832
843
|
"moonshotai/kimi-k2-turbo",
|
|
833
844
|
);
|
|
@@ -2201,7 +2212,8 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
2201
2212
|
|
|
2202
2213
|
const notifyHeartbeatEvent = useCallback(
|
|
2203
2214
|
(event: StreamEventEnvelope) => {
|
|
2204
|
-
|
|
2215
|
+
// biome-ignore lint/suspicious/noExplicitAny: heartbeat payloads are loosely typed
|
|
2216
|
+
const payload = event.payload as any;
|
|
2205
2217
|
const status =
|
|
2206
2218
|
typeof payload.status === "string"
|
|
2207
2219
|
? payload.status.trim().toLowerCase()
|
|
@@ -4020,7 +4032,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
4020
4032
|
setCharacterSaveSuccess(null);
|
|
4021
4033
|
try {
|
|
4022
4034
|
const draft = prepareDraftForSave(characterDraft);
|
|
4023
|
-
if (!draft.name?.trim()) {
|
|
4035
|
+
if (!(draft.name as string | undefined)?.trim()) {
|
|
4024
4036
|
throw new Error("Character name is required before saving.");
|
|
4025
4037
|
}
|
|
4026
4038
|
const { agentName } = await client.updateCharacter(draft);
|
|
@@ -4214,13 +4226,13 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
4214
4226
|
if (isSandboxMode) {
|
|
4215
4227
|
// Provision a sandbox agent on Eliza Cloud
|
|
4216
4228
|
const cloudApiBase = (
|
|
4217
|
-
(window as Record<string, unknown>).__ELIZA_CLOUD_API_BASE__ ??
|
|
4229
|
+
(window as unknown as Record<string, unknown>).__ELIZA_CLOUD_API_BASE__ ??
|
|
4218
4230
|
"https://api.eliza.ai"
|
|
4219
4231
|
) as string;
|
|
4220
4232
|
|
|
4221
4233
|
// Get the auth token from the cloud login state
|
|
4222
4234
|
const authToken = (
|
|
4223
|
-
(window as Record<string, unknown>).__ELIZA_CLOUD_AUTH_TOKEN__ ?? ""
|
|
4235
|
+
(window as unknown as Record<string, unknown>).__ELIZA_CLOUD_AUTH_TOKEN__ ?? ""
|
|
4224
4236
|
) as string;
|
|
4225
4237
|
|
|
4226
4238
|
if (!authToken) {
|
|
@@ -4229,7 +4241,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
4229
4241
|
);
|
|
4230
4242
|
}
|
|
4231
4243
|
|
|
4232
|
-
|
|
4244
|
+
await client.provisionCloudSandbox({
|
|
4233
4245
|
cloudApiBase,
|
|
4234
4246
|
authToken,
|
|
4235
4247
|
name: onboardingName,
|
|
@@ -4257,9 +4269,12 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
4257
4269
|
ipcChannel: "agent:start",
|
|
4258
4270
|
});
|
|
4259
4271
|
} catch {
|
|
4260
|
-
// May not be on desktop — try the Capacitor agent plugin fallback
|
|
4272
|
+
// May not be on desktop — try the Capacitor agent plugin fallback.
|
|
4273
|
+
// Use a variable to prevent Vite static analysis from failing on
|
|
4274
|
+
// this optional peer dependency (only available in milady app builds).
|
|
4261
4275
|
try {
|
|
4262
|
-
const
|
|
4276
|
+
const agentPluginId = "@miladyai/capacitor-agent";
|
|
4277
|
+
const { Agent } = await import(/* @vite-ignore */ agentPluginId);
|
|
4263
4278
|
await Agent.start();
|
|
4264
4279
|
} catch {
|
|
4265
4280
|
// Not on desktop or native — dev mode where agent is already running
|
|
@@ -4614,7 +4629,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
4614
4629
|
const hasBackend = Boolean(client.getBaseUrl());
|
|
4615
4630
|
const cloudApiBase =
|
|
4616
4631
|
((typeof window !== "undefined" &&
|
|
4617
|
-
(window as Record<string, unknown>).__ELIZA_CLOUD_API_BASE__) as string) ||
|
|
4632
|
+
(window as unknown as Record<string, unknown>).__ELIZA_CLOUD_API_BASE__) as string) ||
|
|
4618
4633
|
"https://api.eliza.ai";
|
|
4619
4634
|
const useDirectAuth = !hasBackend;
|
|
4620
4635
|
|
|
@@ -4701,9 +4716,9 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
4701
4716
|
|
|
4702
4717
|
// Store the cloud auth token for provisioning
|
|
4703
4718
|
if (poll.token && typeof window !== "undefined") {
|
|
4704
|
-
(window as Record<string, unknown>).__ELIZA_CLOUD_AUTH_TOKEN__ =
|
|
4719
|
+
(window as unknown as Record<string, unknown>).__ELIZA_CLOUD_AUTH_TOKEN__ =
|
|
4705
4720
|
poll.token;
|
|
4706
|
-
(window as Record<string, unknown>).__ELIZA_CLOUD_API_BASE__ =
|
|
4721
|
+
(window as unknown as Record<string, unknown>).__ELIZA_CLOUD_API_BASE__ =
|
|
4707
4722
|
cloudApiBase;
|
|
4708
4723
|
}
|
|
4709
4724
|
|
|
@@ -4786,6 +4801,11 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
4786
4801
|
}
|
|
4787
4802
|
}, [setActionNotice]);
|
|
4788
4803
|
|
|
4804
|
+
const handleCloudOnboardingFinish = useCallback(() => {
|
|
4805
|
+
setOnboardingComplete(true);
|
|
4806
|
+
setTab("chat");
|
|
4807
|
+
}, []);
|
|
4808
|
+
|
|
4789
4809
|
// ── Updates ────────────────────────────────────────────────────────
|
|
4790
4810
|
|
|
4791
4811
|
const handleChannelChange = useCallback(
|
|
@@ -5182,9 +5202,9 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
5182
5202
|
const persistedConnection = loadPersistedConnectionMode();
|
|
5183
5203
|
const hasApiBase = Boolean(
|
|
5184
5204
|
(typeof window !== "undefined" &&
|
|
5185
|
-
(window as Record<string, unknown>).__MILADY_API_BASE__) ||
|
|
5205
|
+
(window as unknown as Record<string, unknown>).__MILADY_API_BASE__) ||
|
|
5186
5206
|
(typeof window !== "undefined" &&
|
|
5187
|
-
(window as Record<string, unknown>).__ELIZA_API_BASE__),
|
|
5207
|
+
(window as unknown as Record<string, unknown>).__ELIZA_API_BASE__),
|
|
5188
5208
|
);
|
|
5189
5209
|
|
|
5190
5210
|
if (!persistedConnection && !hasApiBase) {
|
|
@@ -5912,6 +5932,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
5912
5932
|
const shouldStartAtCharacterSelect = shouldStartAtCharacterSelectOnLaunch(
|
|
5913
5933
|
{
|
|
5914
5934
|
onboardingNeedsOptions,
|
|
5935
|
+
onboardingMode,
|
|
5915
5936
|
navPath,
|
|
5916
5937
|
urlTab,
|
|
5917
5938
|
},
|
|
@@ -6363,6 +6384,7 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
6363
6384
|
handleOnboardingUseLocalBackend,
|
|
6364
6385
|
handleCloudLogin,
|
|
6365
6386
|
handleCloudDisconnect,
|
|
6387
|
+
handleCloudOnboardingFinish,
|
|
6366
6388
|
loadUpdateStatus,
|
|
6367
6389
|
handleChannelChange,
|
|
6368
6390
|
checkExtensionStatus,
|
|
@@ -6376,11 +6398,18 @@ export function AppProvider({ children }: { children: ReactNode }) {
|
|
|
6376
6398
|
copyToClipboard,
|
|
6377
6399
|
};
|
|
6378
6400
|
|
|
6401
|
+
const mergedBranding = useMemo(
|
|
6402
|
+
() => ({ ...DEFAULT_BRANDING, ...brandingOverride }),
|
|
6403
|
+
[brandingOverride],
|
|
6404
|
+
);
|
|
6405
|
+
|
|
6379
6406
|
return (
|
|
6380
|
-
<
|
|
6381
|
-
{
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6407
|
+
<BrandingContext.Provider value={mergedBranding}>
|
|
6408
|
+
<AppContext.Provider value={value}>
|
|
6409
|
+
{children}
|
|
6410
|
+
<ConfirmModal {...modalProps} />
|
|
6411
|
+
<PromptModal {...promptModalProps} />
|
|
6412
|
+
</AppContext.Provider>
|
|
6413
|
+
</BrandingContext.Provider>
|
|
6385
6414
|
);
|
|
6386
6415
|
}
|
package/src/state/types.ts
CHANGED
|
@@ -109,8 +109,13 @@ export const ONBOARDING_STEPS: OnboardingStepMeta[] = [
|
|
|
109
109
|
},
|
|
110
110
|
];
|
|
111
111
|
|
|
112
|
+
export type OnboardingMode = "basic" | "advanced" | "elizacloudonly";
|
|
113
|
+
|
|
114
|
+
export type FlaminaGuideTopic = "provider" | "rpc" | "permissions" | "voice";
|
|
115
|
+
|
|
112
116
|
export interface OnboardingNextOptions {
|
|
113
117
|
allowPermissionBypass?: boolean;
|
|
118
|
+
skipTask?: string;
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
export const ONBOARDING_PERMISSION_LABELS: Record<SystemPermissionId, string> =
|
|
@@ -431,8 +436,15 @@ export interface AppState {
|
|
|
431
436
|
importError: string | null;
|
|
432
437
|
importSuccess: string | null;
|
|
433
438
|
|
|
439
|
+
// Startup
|
|
440
|
+
startupStatus: string | null;
|
|
441
|
+
|
|
434
442
|
// Onboarding
|
|
435
443
|
onboardingStep: OnboardingStep;
|
|
444
|
+
onboardingMode: OnboardingMode;
|
|
445
|
+
onboardingActiveGuide: string | null;
|
|
446
|
+
onboardingDeferredTasks: string[];
|
|
447
|
+
postOnboardingChecklistDismissed: boolean;
|
|
436
448
|
onboardingOptions: OnboardingOptions | null;
|
|
437
449
|
onboardingName: string;
|
|
438
450
|
onboardingOwnerName: string;
|
|
@@ -548,6 +560,8 @@ export interface AppActions {
|
|
|
548
560
|
handleReset: () => Promise<void>;
|
|
549
561
|
retryStartup: () => void;
|
|
550
562
|
dismissRestartBanner: () => void;
|
|
563
|
+
showRestartBanner: () => void;
|
|
564
|
+
relaunchDesktop: () => Promise<void>;
|
|
551
565
|
triggerRestart: () => Promise<void>;
|
|
552
566
|
dismissBackendDisconnectedBanner: () => void;
|
|
553
567
|
retryBackendConnection: () => void;
|
|
@@ -669,6 +683,7 @@ export interface AppActions {
|
|
|
669
683
|
// Cloud
|
|
670
684
|
handleCloudLogin: () => Promise<void>;
|
|
671
685
|
handleCloudDisconnect: () => Promise<void>;
|
|
686
|
+
handleCloudOnboardingFinish: () => void;
|
|
672
687
|
|
|
673
688
|
// Updates
|
|
674
689
|
loadUpdateStatus: (force?: boolean) => Promise<void>;
|
package/src/state/vrm.ts
CHANGED
|
@@ -1,48 +1,82 @@
|
|
|
1
1
|
import { resolveAppAssetUrl } from "../utils/asset-url";
|
|
2
2
|
import type { UiTheme } from "./ui-preferences";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Bundled VRM asset roster
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
interface BundledVrmAsset {
|
|
9
|
+
title: string;
|
|
10
|
+
slug: string;
|
|
11
|
+
}
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
14
|
+
* Default Eliza avatar roster (4 slots).
|
|
15
|
+
* Apps can override this at startup by setting window.__APP_VRM_ASSETS__
|
|
16
|
+
* before mounting the React tree.
|
|
12
17
|
*/
|
|
13
|
-
const
|
|
18
|
+
const DEFAULT_ASSETS: BundledVrmAsset[] = [
|
|
19
|
+
{ title: "ELIZA-01", slug: "eliza-1" },
|
|
20
|
+
{ title: "ELIZA-04", slug: "eliza-4" },
|
|
21
|
+
{ title: "ELIZA-05", slug: "eliza-5" },
|
|
22
|
+
{ title: "ELIZA-09", slug: "eliza-9" },
|
|
23
|
+
];
|
|
14
24
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
declare global {
|
|
26
|
+
interface Window {
|
|
27
|
+
__APP_VRM_ASSETS__?: BundledVrmAsset[];
|
|
28
|
+
}
|
|
19
29
|
}
|
|
20
30
|
|
|
21
|
-
function
|
|
31
|
+
function getAssets(): BundledVrmAsset[] {
|
|
32
|
+
if (typeof window !== "undefined" && Array.isArray(window.__APP_VRM_ASSETS__)) {
|
|
33
|
+
return window.__APP_VRM_ASSETS__;
|
|
34
|
+
}
|
|
35
|
+
return DEFAULT_ASSETS;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Number of bundled VRM avatars shipped with the app. */
|
|
39
|
+
export function getVrmCount(): number {
|
|
40
|
+
return getAssets().length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Legacy constant — prefer getVrmCount() for dynamic rosters.
|
|
44
|
+
export const VRM_COUNT = DEFAULT_ASSETS.length;
|
|
45
|
+
|
|
46
|
+
export function normalizeAvatarIndex(index: number): number {
|
|
22
47
|
if (!Number.isFinite(index)) return 1;
|
|
23
48
|
const n = Math.trunc(index);
|
|
24
49
|
if (n === 0) return 0;
|
|
25
|
-
|
|
50
|
+
const count = getAssets().length;
|
|
51
|
+
if (n < 1 || n > count) return 1;
|
|
26
52
|
return n;
|
|
27
53
|
}
|
|
28
54
|
|
|
29
55
|
/** Resolve a bundled VRM index (1–N) to its public asset URL. */
|
|
30
56
|
export function getVrmUrl(index: number): string {
|
|
31
|
-
const
|
|
32
|
-
|
|
57
|
+
const assets = getAssets();
|
|
58
|
+
const n = normalizeAvatarIndex(index);
|
|
59
|
+
const safe = n > 0 ? n : 1;
|
|
60
|
+
const slug = assets[safe - 1]?.slug ?? assets[0].slug;
|
|
61
|
+
return resolveAppAssetUrl(`vrms/${slug}.vrm.gz`);
|
|
33
62
|
}
|
|
34
63
|
|
|
35
64
|
/** Resolve a bundled VRM index (1–N) to its preview thumbnail URL. */
|
|
36
65
|
export function getVrmPreviewUrl(index: number): string {
|
|
37
|
-
const
|
|
38
|
-
|
|
66
|
+
const assets = getAssets();
|
|
67
|
+
const n = normalizeAvatarIndex(index);
|
|
68
|
+
const safe = n > 0 ? n : 1;
|
|
69
|
+
const slug = assets[safe - 1]?.slug ?? assets[0].slug;
|
|
70
|
+
return resolveAppAssetUrl(`vrms/previews/${slug}.png`);
|
|
39
71
|
}
|
|
40
72
|
|
|
41
73
|
/** Resolve a bundled VRM index (1-N) to its custom background URL. */
|
|
42
74
|
export function getVrmBackgroundUrl(index: number): string {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
75
|
+
const assets = getAssets();
|
|
76
|
+
const n = normalizeAvatarIndex(index);
|
|
77
|
+
const safe = n > 0 ? n : 1;
|
|
78
|
+
const slug = assets[safe - 1]?.slug ?? assets[0].slug;
|
|
79
|
+
return resolveAppAssetUrl(`vrms/backgrounds/${slug}.png`);
|
|
46
80
|
}
|
|
47
81
|
|
|
48
82
|
const COMPANION_THEME_BACKGROUND_INDEX: Record<UiTheme, number> = {
|
|
@@ -57,8 +91,10 @@ export function getCompanionBackgroundUrl(theme: UiTheme): string {
|
|
|
57
91
|
|
|
58
92
|
/** Human-readable roster title for bundled avatars. */
|
|
59
93
|
export function getVrmTitle(index: number): string {
|
|
60
|
-
const
|
|
61
|
-
|
|
94
|
+
const assets = getAssets();
|
|
95
|
+
const n = normalizeAvatarIndex(index);
|
|
96
|
+
const safe = n > 0 ? n : 1;
|
|
97
|
+
return assets[safe - 1]?.title ?? assets[0].title;
|
|
62
98
|
}
|
|
63
99
|
|
|
64
100
|
/** Whether a bundled index points to the official Eliza avatar set. */
|
|
@@ -67,10 +103,6 @@ export function isOfficialVrmIndex(_index: number): boolean {
|
|
|
67
103
|
}
|
|
68
104
|
|
|
69
105
|
/** Whether a VRM index requires an explicit 180° face-camera flip instead of auto-detection. */
|
|
70
|
-
export function getVrmNeedsFlip(
|
|
71
|
-
const normalized = normalizeAvatarIndex(index);
|
|
72
|
-
if (normalized <= BASE_VRM_COUNT) return false;
|
|
106
|
+
export function getVrmNeedsFlip(_index: number): boolean {
|
|
73
107
|
return false;
|
|
74
108
|
}
|
|
75
|
-
|
|
76
|
-
export { normalizeAvatarIndex };
|
|
@@ -54,6 +54,7 @@ const { mockClient } = vi.hoisted(() => ({
|
|
|
54
54
|
triggers: [],
|
|
55
55
|
todos: [],
|
|
56
56
|
})),
|
|
57
|
+
getBaseUrl: vi.fn(() => "http://localhost:2138"),
|
|
57
58
|
cloudLogin: vi.fn(async () => ({
|
|
58
59
|
ok: false,
|
|
59
60
|
browserUrl: "",
|
|
@@ -126,6 +127,9 @@ describe("cloud login locking", () => {
|
|
|
126
127
|
setInterval: globalThis.setInterval,
|
|
127
128
|
clearInterval: globalThis.clearInterval,
|
|
128
129
|
open: vi.fn(() => null),
|
|
130
|
+
// Simulate an available backend so the startup flow doesn't skip to
|
|
131
|
+
// fresh-install onboarding immediately.
|
|
132
|
+
__MILADY_API_BASE__: "http://localhost:2138",
|
|
129
133
|
});
|
|
130
134
|
Object.assign(document.documentElement, { setAttribute: vi.fn() });
|
|
131
135
|
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tests for the PersistedConnectionMode persistence layer and
|
|
5
|
+
* startup fresh-install detection added in the onboarding/connection rework.
|
|
6
|
+
*
|
|
7
|
+
* Covers:
|
|
8
|
+
* - Persistence save/load/clear round-trips
|
|
9
|
+
* - Fresh install detection (no persisted mode + no API base → skip backend polling)
|
|
10
|
+
* - Returning user restoration (persisted mode restores client connection)
|
|
11
|
+
* - Mobile sandbox-only enforcement
|
|
12
|
+
* - Cloud provisioning client method
|
|
13
|
+
* - Connection key auto-generation (milady-side)
|
|
14
|
+
* - Direct cloud auth (no backend path)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
18
|
+
|
|
19
|
+
// Mock the API client at top level to avoid hoisting warnings
|
|
20
|
+
const { mockClient } = vi.hoisted(() => ({
|
|
21
|
+
mockClient: {
|
|
22
|
+
hasToken: vi.fn(() => false),
|
|
23
|
+
getAuthStatus: vi.fn(async () => ({
|
|
24
|
+
required: false,
|
|
25
|
+
pairingEnabled: false,
|
|
26
|
+
expiresAt: null,
|
|
27
|
+
})),
|
|
28
|
+
getOnboardingStatus: vi.fn(async () => ({ complete: false })),
|
|
29
|
+
disconnectWs: vi.fn(),
|
|
30
|
+
getCodingAgentStatus: vi.fn(async () => null),
|
|
31
|
+
setToken: vi.fn(),
|
|
32
|
+
},
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
vi.mock("@elizaos/app-core/api", () => ({
|
|
36
|
+
client: mockClient,
|
|
37
|
+
SkillScanReportSummary: {},
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// ── Persistence layer tests ────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe("PersistedConnectionMode persistence", () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
localStorage.clear();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns null when no connection mode is persisted", async () => {
|
|
48
|
+
const { loadPersistedConnectionMode } = await import(
|
|
49
|
+
"../../src/state/persistence"
|
|
50
|
+
);
|
|
51
|
+
expect(loadPersistedConnectionMode()).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("round-trips a local connection mode", async () => {
|
|
55
|
+
const { loadPersistedConnectionMode, savePersistedConnectionMode } =
|
|
56
|
+
await import("../../src/state/persistence");
|
|
57
|
+
|
|
58
|
+
savePersistedConnectionMode({ runMode: "local" });
|
|
59
|
+
const loaded = loadPersistedConnectionMode();
|
|
60
|
+
expect(loaded).toEqual({ runMode: "local" });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("round-trips a cloud connection mode with auth", async () => {
|
|
64
|
+
const { loadPersistedConnectionMode, savePersistedConnectionMode } =
|
|
65
|
+
await import("../../src/state/persistence");
|
|
66
|
+
|
|
67
|
+
savePersistedConnectionMode({
|
|
68
|
+
runMode: "cloud",
|
|
69
|
+
cloudApiBase: "https://api.eliza.ai",
|
|
70
|
+
cloudAuthToken: "token-123",
|
|
71
|
+
});
|
|
72
|
+
const loaded = loadPersistedConnectionMode();
|
|
73
|
+
expect(loaded).toEqual({
|
|
74
|
+
runMode: "cloud",
|
|
75
|
+
cloudApiBase: "https://api.eliza.ai",
|
|
76
|
+
cloudAuthToken: "token-123",
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("round-trips a remote connection mode", async () => {
|
|
81
|
+
const { loadPersistedConnectionMode, savePersistedConnectionMode } =
|
|
82
|
+
await import("../../src/state/persistence");
|
|
83
|
+
|
|
84
|
+
savePersistedConnectionMode({
|
|
85
|
+
runMode: "remote",
|
|
86
|
+
remoteApiBase: "https://my-agent.example.com",
|
|
87
|
+
remoteAccessToken: "key-abc",
|
|
88
|
+
});
|
|
89
|
+
const loaded = loadPersistedConnectionMode();
|
|
90
|
+
expect(loaded).toEqual({
|
|
91
|
+
runMode: "remote",
|
|
92
|
+
remoteApiBase: "https://my-agent.example.com",
|
|
93
|
+
remoteAccessToken: "key-abc",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("clears persisted connection mode", async () => {
|
|
98
|
+
const {
|
|
99
|
+
clearPersistedConnectionMode,
|
|
100
|
+
loadPersistedConnectionMode,
|
|
101
|
+
savePersistedConnectionMode,
|
|
102
|
+
} = await import("../../src/state/persistence");
|
|
103
|
+
|
|
104
|
+
savePersistedConnectionMode({ runMode: "local" });
|
|
105
|
+
expect(loadPersistedConnectionMode()).not.toBeNull();
|
|
106
|
+
|
|
107
|
+
clearPersistedConnectionMode();
|
|
108
|
+
expect(loadPersistedConnectionMode()).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("returns null for corrupted JSON", async () => {
|
|
112
|
+
const { loadPersistedConnectionMode } = await import(
|
|
113
|
+
"../../src/state/persistence"
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
localStorage.setItem("eliza:connection-mode", "not-json{{{");
|
|
117
|
+
expect(loadPersistedConnectionMode()).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("returns null for invalid runMode value", async () => {
|
|
121
|
+
const { loadPersistedConnectionMode } = await import(
|
|
122
|
+
"../../src/state/persistence"
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
localStorage.setItem(
|
|
126
|
+
"eliza:connection-mode",
|
|
127
|
+
JSON.stringify({ runMode: "invalid" }),
|
|
128
|
+
);
|
|
129
|
+
expect(loadPersistedConnectionMode()).toBeNull();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("returns null for non-object stored value", async () => {
|
|
133
|
+
const { loadPersistedConnectionMode } = await import(
|
|
134
|
+
"../../src/state/persistence"
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
localStorage.setItem("eliza:connection-mode", JSON.stringify([1, 2, 3]));
|
|
138
|
+
expect(loadPersistedConnectionMode()).toBeNull();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ── Fresh install detection ────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
describe("fresh install detection (startup)", () => {
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
localStorage.clear();
|
|
147
|
+
delete (window as Record<string, unknown>).__MILADY_API_BASE__;
|
|
148
|
+
delete (window as Record<string, unknown>).__ELIZA_API_BASE__;
|
|
149
|
+
Object.assign(document.documentElement, { setAttribute: vi.fn() });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("skips backend polling on fresh install (no persisted mode, no API base)", async () => {
|
|
153
|
+
const React = await import("react");
|
|
154
|
+
const TestRenderer = await import("react-test-renderer");
|
|
155
|
+
const { AppProvider, useApp } = await import("@elizaos/app-core/state");
|
|
156
|
+
|
|
157
|
+
let latest: {
|
|
158
|
+
onboardingComplete: boolean;
|
|
159
|
+
onboardingLoading: boolean;
|
|
160
|
+
startupPhase: string;
|
|
161
|
+
startupError: unknown;
|
|
162
|
+
} | null = null;
|
|
163
|
+
|
|
164
|
+
function Probe() {
|
|
165
|
+
const app = useApp();
|
|
166
|
+
React.useEffect(() => {
|
|
167
|
+
latest = {
|
|
168
|
+
onboardingComplete: app.onboardingComplete,
|
|
169
|
+
onboardingLoading: app.onboardingLoading,
|
|
170
|
+
startupPhase: app.startupPhase,
|
|
171
|
+
startupError: app.startupError,
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let tree: TestRenderer.ReactTestRenderer | null = null;
|
|
178
|
+
await TestRenderer.act(async () => {
|
|
179
|
+
tree = TestRenderer.create(
|
|
180
|
+
React.createElement(AppProvider, null, React.createElement(Probe)),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await TestRenderer.act(async () => {
|
|
185
|
+
await Promise.resolve();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// On fresh install: should immediately be ready for onboarding
|
|
189
|
+
// without waiting for a backend
|
|
190
|
+
expect(latest).not.toBeNull();
|
|
191
|
+
expect(latest!.onboardingComplete).toBe(false);
|
|
192
|
+
expect(latest!.onboardingLoading).toBe(false);
|
|
193
|
+
expect(latest!.startupError).toBeNull();
|
|
194
|
+
|
|
195
|
+
// The backend should NOT have been polled
|
|
196
|
+
expect(mockClient.getAuthStatus).not.toHaveBeenCalled();
|
|
197
|
+
expect(mockClient.getOnboardingStatus).not.toHaveBeenCalled();
|
|
198
|
+
|
|
199
|
+
await TestRenderer.act(async () => {
|
|
200
|
+
tree?.unmount();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ── API client cloud provisioning ──────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
describe("MiladyClient.provisionCloudSandbox", () => {
|
|
208
|
+
beforeEach(() => {
|
|
209
|
+
vi.restoreAllMocks();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("provisions a sandbox agent through create → provision → poll", async () => {
|
|
213
|
+
const { MiladyClient } = await import("../../src/api/client");
|
|
214
|
+
const client = new MiladyClient("http://localhost:2138");
|
|
215
|
+
|
|
216
|
+
const fetchMock = vi.fn();
|
|
217
|
+
|
|
218
|
+
// Create agent response
|
|
219
|
+
fetchMock.mockResolvedValueOnce({
|
|
220
|
+
ok: true,
|
|
221
|
+
json: async () => ({ id: "agent-1" }),
|
|
222
|
+
});
|
|
223
|
+
// Provision response
|
|
224
|
+
fetchMock.mockResolvedValueOnce({
|
|
225
|
+
ok: true,
|
|
226
|
+
json: async () => ({ jobId: "job-1" }),
|
|
227
|
+
});
|
|
228
|
+
// Poll - still pending
|
|
229
|
+
fetchMock.mockResolvedValueOnce({
|
|
230
|
+
ok: true,
|
|
231
|
+
json: async () => ({ status: "in_progress" }),
|
|
232
|
+
});
|
|
233
|
+
// Poll - completed
|
|
234
|
+
fetchMock.mockResolvedValueOnce({
|
|
235
|
+
ok: true,
|
|
236
|
+
json: async () => ({
|
|
237
|
+
status: "completed",
|
|
238
|
+
result: { bridgeUrl: "https://bridge.eliza.ai/agent-1" },
|
|
239
|
+
}),
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
globalThis.fetch = fetchMock;
|
|
243
|
+
|
|
244
|
+
const progressUpdates: string[] = [];
|
|
245
|
+
const result = await client.provisionCloudSandbox({
|
|
246
|
+
cloudApiBase: "https://api.eliza.ai",
|
|
247
|
+
authToken: "token-123",
|
|
248
|
+
name: "TestAgent",
|
|
249
|
+
onProgress: (status) => progressUpdates.push(status),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(result.bridgeUrl).toBe("https://bridge.eliza.ai/agent-1");
|
|
253
|
+
expect(result.agentId).toBe("agent-1");
|
|
254
|
+
expect(progressUpdates).toContain("creating");
|
|
255
|
+
expect(progressUpdates).toContain("provisioning");
|
|
256
|
+
expect(progressUpdates).toContain("ready");
|
|
257
|
+
|
|
258
|
+
// Verify API calls
|
|
259
|
+
expect(fetchMock).toHaveBeenCalledTimes(4);
|
|
260
|
+
expect(fetchMock.mock.calls[0][0]).toBe(
|
|
261
|
+
"https://api.eliza.ai/api/v1/milady/agents",
|
|
262
|
+
);
|
|
263
|
+
expect(fetchMock.mock.calls[1][0]).toBe(
|
|
264
|
+
"https://api.eliza.ai/api/v1/milady/agents/agent-1/provision",
|
|
265
|
+
);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("throws on provisioning failure", async () => {
|
|
269
|
+
const { MiladyClient } = await import("../../src/api/client");
|
|
270
|
+
const client = new MiladyClient("http://localhost:2138");
|
|
271
|
+
|
|
272
|
+
const fetchMock = vi.fn();
|
|
273
|
+
|
|
274
|
+
// Create succeeds
|
|
275
|
+
fetchMock.mockResolvedValueOnce({
|
|
276
|
+
ok: true,
|
|
277
|
+
json: async () => ({ id: "agent-1" }),
|
|
278
|
+
});
|
|
279
|
+
// Provision succeeds
|
|
280
|
+
fetchMock.mockResolvedValueOnce({
|
|
281
|
+
ok: true,
|
|
282
|
+
json: async () => ({ jobId: "job-1" }),
|
|
283
|
+
});
|
|
284
|
+
// Poll - failed
|
|
285
|
+
fetchMock.mockResolvedValueOnce({
|
|
286
|
+
ok: true,
|
|
287
|
+
json: async () => ({
|
|
288
|
+
status: "failed",
|
|
289
|
+
error: "Insufficient credits",
|
|
290
|
+
}),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
globalThis.fetch = fetchMock;
|
|
294
|
+
|
|
295
|
+
await expect(
|
|
296
|
+
client.provisionCloudSandbox({
|
|
297
|
+
cloudApiBase: "https://api.eliza.ai",
|
|
298
|
+
authToken: "token-123",
|
|
299
|
+
name: "TestAgent",
|
|
300
|
+
}),
|
|
301
|
+
).rejects.toThrow("Provisioning failed: Insufficient credits");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("throws on agent creation failure", async () => {
|
|
305
|
+
const { MiladyClient } = await import("../../src/api/client");
|
|
306
|
+
const client = new MiladyClient("http://localhost:2138");
|
|
307
|
+
|
|
308
|
+
globalThis.fetch = vi.fn().mockResolvedValueOnce({
|
|
309
|
+
ok: false,
|
|
310
|
+
text: async () => "Unauthorized",
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await expect(
|
|
314
|
+
client.provisionCloudSandbox({
|
|
315
|
+
cloudApiBase: "https://api.eliza.ai",
|
|
316
|
+
authToken: "bad-token",
|
|
317
|
+
name: "TestAgent",
|
|
318
|
+
}),
|
|
319
|
+
).rejects.toThrow("Failed to create cloud agent: Unauthorized");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ── Direct cloud auth ──────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
describe("MiladyClient direct cloud auth", () => {
|
|
326
|
+
beforeEach(() => {
|
|
327
|
+
vi.restoreAllMocks();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("cloudLoginDirect calls the cloud API directly", async () => {
|
|
331
|
+
const { MiladyClient } = await import("../../src/api/client");
|
|
332
|
+
const client = new MiladyClient("http://localhost:2138");
|
|
333
|
+
|
|
334
|
+
globalThis.fetch = vi.fn().mockResolvedValueOnce({
|
|
335
|
+
ok: true,
|
|
336
|
+
json: async () => ({
|
|
337
|
+
ok: true,
|
|
338
|
+
browserUrl: "https://eliza.ai/login?session=abc",
|
|
339
|
+
sessionId: "abc",
|
|
340
|
+
}),
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const result = await client.cloudLoginDirect("https://api.eliza.ai");
|
|
344
|
+
expect(result.ok).toBe(true);
|
|
345
|
+
expect(result.sessionId).toBe("abc");
|
|
346
|
+
expect(result.browserUrl).toContain("login");
|
|
347
|
+
|
|
348
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
349
|
+
"https://api.eliza.ai/api/v1/auth/login",
|
|
350
|
+
expect.objectContaining({ method: "POST" }),
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("cloudLoginPollDirect polls the cloud API directly", async () => {
|
|
355
|
+
const { MiladyClient } = await import("../../src/api/client");
|
|
356
|
+
const client = new MiladyClient("http://localhost:2138");
|
|
357
|
+
|
|
358
|
+
globalThis.fetch = vi.fn().mockResolvedValueOnce({
|
|
359
|
+
ok: true,
|
|
360
|
+
json: async () => ({
|
|
361
|
+
status: "authenticated",
|
|
362
|
+
token: "auth-token-xyz",
|
|
363
|
+
userId: "user-1",
|
|
364
|
+
}),
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const result = await client.cloudLoginPollDirect(
|
|
368
|
+
"https://api.eliza.ai",
|
|
369
|
+
"session-123",
|
|
370
|
+
);
|
|
371
|
+
expect(result.status).toBe("authenticated");
|
|
372
|
+
expect(result.token).toBe("auth-token-xyz");
|
|
373
|
+
|
|
374
|
+
expect(globalThis.fetch).toHaveBeenCalledWith(
|
|
375
|
+
"https://api.eliza.ai/api/v1/auth/login/status?sessionId=session-123",
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("cloudLoginDirect returns error on failure", async () => {
|
|
380
|
+
const { MiladyClient } = await import("../../src/api/client");
|
|
381
|
+
const client = new MiladyClient("http://localhost:2138");
|
|
382
|
+
|
|
383
|
+
globalThis.fetch = vi.fn().mockResolvedValueOnce({
|
|
384
|
+
ok: false,
|
|
385
|
+
status: 500,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const result = await client.cloudLoginDirect("https://api.eliza.ai");
|
|
389
|
+
expect(result.ok).toBe(false);
|
|
390
|
+
expect(result.error).toContain("500");
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// ── API client getBaseUrl ──────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
describe("MiladyClient.getBaseUrl", () => {
|
|
397
|
+
it("returns the current base URL", async () => {
|
|
398
|
+
const { MiladyClient } = await import("../../src/api/client");
|
|
399
|
+
const client = new MiladyClient("http://my-agent.example.com");
|
|
400
|
+
expect(client.getBaseUrl()).toBe("http://my-agent.example.com");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("returns empty string when no base URL is configured", async () => {
|
|
404
|
+
// In jsdom with about:blank protocol, no injected base
|
|
405
|
+
delete (window as Record<string, unknown>).__MILADY_API_BASE__;
|
|
406
|
+
const { MiladyClient } = await import("../../src/api/client");
|
|
407
|
+
const client = new MiladyClient();
|
|
408
|
+
// May return "" or a fallback depending on window.location.protocol
|
|
409
|
+
expect(typeof client.getBaseUrl()).toBe("string");
|
|
410
|
+
});
|
|
411
|
+
});
|
|
@@ -237,6 +237,9 @@ describe("onboarding finish locking", () => {
|
|
|
237
237
|
setInterval: globalThis.setInterval,
|
|
238
238
|
clearInterval: globalThis.clearInterval,
|
|
239
239
|
alert: vi.fn(),
|
|
240
|
+
// Simulate an available backend so the startup flow doesn't skip to
|
|
241
|
+
// onboarding immediately (fresh install detection).
|
|
242
|
+
__MILADY_API_BASE__: "http://localhost:2138",
|
|
240
243
|
});
|
|
241
244
|
Object.assign(document.documentElement, { setAttribute: vi.fn() });
|
|
242
245
|
localStorage.clear();
|
|
@@ -53,6 +53,13 @@ describe("startup failure: backend missing", () => {
|
|
|
53
53
|
beforeEach(() => {
|
|
54
54
|
vi.useFakeTimers();
|
|
55
55
|
Object.assign(document.documentElement, { setAttribute: vi.fn() });
|
|
56
|
+
// Simulate a returning user with a persisted local connection so the
|
|
57
|
+
// startup flow proceeds to backend polling (fresh installs now skip
|
|
58
|
+
// backend polling and go straight to onboarding).
|
|
59
|
+
localStorage.setItem(
|
|
60
|
+
"eliza:connection-mode",
|
|
61
|
+
JSON.stringify({ runMode: "local" }),
|
|
62
|
+
);
|
|
56
63
|
mockClient.hasToken.mockReturnValue(false);
|
|
57
64
|
mockClient.disconnectWs.mockImplementation(() => {});
|
|
58
65
|
mockClient.getAuthStatus.mockResolvedValue({
|
|
@@ -73,6 +80,7 @@ describe("startup failure: backend missing", () => {
|
|
|
73
80
|
|
|
74
81
|
afterEach(() => {
|
|
75
82
|
vi.useRealTimers();
|
|
83
|
+
localStorage.removeItem("eliza:connection-mode");
|
|
76
84
|
});
|
|
77
85
|
|
|
78
86
|
it("fails fast on backend 404 and surfaces backend-unreachable", async () => {
|