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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest";
import { mock } from "vitest-mock-extended";

import type { ResumeResolution } from "../ResumeDeploymentGuard/ResumeDeploymentGuard";
import type { DeploymentFlow } from "../useDeploymentFlow/useDeploymentFlow";
import type { DEPENDENCIES } from "./AutoDeployFlow";
import { AutoDeployFlow } from "./AutoDeployFlow";

Expand All @@ -14,31 +15,17 @@ type FlowResult = ReturnType<typeof DEPENDENCIES.useAutoDeploymentFlow>;
const CONTACT_SUPPORT_URL = "https://akash.network/discord";

describe(AutoDeployFlow.name, () => {
it("drives the phased flow with trial readiness, the resolved SDL, the resumed dseq, and intent params", () => {
const useAutoDeploymentFlow = vi.fn(() => buildFlow());
it("drives the autopilot with the shared flow, trial readiness, and the resolved SDL", () => {
const useAutoDeploymentFlow = vi.fn(() => buildAutopilot());
const trialError = new Error("trial failed");
setup({
sdl: "sdl-content",
templateId: "hello-world",
dseq: "555",
draftId: "d1",
trial: { isWalletReady: true, error: trialError },
useAutoDeploymentFlow
});
const flow = mock<DeploymentFlow>();
setup({ sdl: "sdl-content", flow, isWalletReady: true, trialError, useAutoDeploymentFlow });

expect(useAutoDeploymentFlow).toHaveBeenCalledWith({
sdl: "sdl-content",
isWalletReady: true,
trialError,
initialDseq: "555",
templateId: "hello-world",
draftId: "d1",
resumeLeases: []
});
expect(useAutoDeploymentFlow).toHaveBeenCalledWith({ sdl: "sdl-content", isWalletReady: true, trialError, resumeLeases: [], flow });
});

it("forwards the guard's resolved live leases to the flow so it can re-send the manifest", () => {
const useAutoDeploymentFlow = vi.fn(() => buildFlow());
const useAutoDeploymentFlow = vi.fn(() => buildAutopilot());
const activeLeases = [{ dseq: "555", gseq: 1, oseq: 2, provider: "akash1provider" }];
setup({ resume: { activeLeases }, useAutoDeploymentFlow });

Expand All @@ -49,7 +36,7 @@ describe(AutoDeployFlow.name, () => {
const PhasedDeployProgressScene = vi.fn(ComponentMock);
setup({
templateName: "my-app",
flow: { state: { kind: "matching" }, progressPercent: 42 },
autopilot: { state: { kind: "matching" }, progressPercent: 42 },
dependencies: { PhasedDeployProgressScene }
});

Expand All @@ -61,14 +48,14 @@ describe(AutoDeployFlow.name, () => {

it("focuses the scene on the matched provider address", () => {
const PhasedDeployProgressScene = vi.fn(ComponentMock);
setup({ flow: { matchedProviderAddress: "akash1provider" }, dependencies: { PhasedDeployProgressScene } });
setup({ autopilot: { matchedProviderAddress: "akash1provider" }, dependencies: { PhasedDeployProgressScene } });

expect(PhasedDeployProgressScene).toHaveBeenCalledWith(expect.objectContaining({ focusedProviderAddress: "akash1provider" }), expect.anything());
});

it("asks the flow to try again when the scene requests it", () => {
const tryAgain = vi.fn();
const { sceneProps } = setup({ flow: { tryAgain } });
const { sceneProps } = setup({ autopilot: { tryAgain } });

sceneProps().onTryAgain?.();

Expand All @@ -78,7 +65,7 @@ describe(AutoDeployFlow.name, () => {
it("resets the trial before restarting when the trial has errored, so the retry is not a dead-end", () => {
const tryAgain = vi.fn();
const retryTrial = vi.fn();
const { sceneProps } = setup({ flow: { tryAgain }, trial: { isWalletReady: false, error: new Error("trial failed"), retryTrial } });
const { sceneProps } = setup({ autopilot: { tryAgain }, isWalletReady: false, trialError: new Error("trial failed"), retryTrial });

sceneProps().onTryAgain?.();

Expand All @@ -88,13 +75,25 @@ describe(AutoDeployFlow.name, () => {

it("does not reset the trial on retry when there was no trial error", () => {
const retryTrial = vi.fn();
const { sceneProps } = setup({ flow: { tryAgain: vi.fn() }, trial: { isWalletReady: true, retryTrial } });
const { sceneProps } = setup({ autopilot: { tryAgain: vi.fn() }, isWalletReady: true, retryTrial });

sceneProps().onTryAgain?.();

expect(retryTrial).not.toHaveBeenCalled();
});

it("stops the autopilot and switches to manual bid selection when the user chooses a provider", () => {
const stopAutopilot = vi.fn();
const setBidStrategy = vi.fn();
const flow = mock<DeploymentFlow>({ actions: mock<DeploymentFlow["actions"]>({ setBidStrategy }) });
const { sceneProps } = setup({ flow, autopilot: { stopAutopilot } });

sceneProps().onChooseProvider?.();

expect(stopAutopilot).toHaveBeenCalledTimes(1);
expect(setBidStrategy).toHaveBeenCalledWith("select");
});

it("opens the configured contact-support URL when the scene requests support", () => {
const open = vi.spyOn(window, "open").mockImplementation(() => null);
const { sceneProps } = setup({ contactSupportUrl: "https://support.example" });
Expand All @@ -104,7 +103,7 @@ describe(AutoDeployFlow.name, () => {
expect(open).toHaveBeenCalledWith("https://support.example", "_blank", "noopener,noreferrer");
});

function buildFlow(overrides: Partial<FlowResult> = {}): FlowResult {
function buildAutopilot(overrides: Partial<FlowResult> = {}): FlowResult {
return {
state: { kind: "creating" },
progressPercent: 0,
Expand All @@ -115,6 +114,7 @@ describe(AutoDeployFlow.name, () => {
],
matchedProviderAddress: null,
tryAgain: vi.fn(),
stopAutopilot: vi.fn(),
...overrides
};
}
Expand All @@ -123,29 +123,19 @@ describe(AutoDeployFlow.name, () => {
input: {
templateName?: string;
sdl?: string;
templateId?: string;
dseq?: string;
draftId?: string;
resume?: ResumeResolution;
trial?: { isWalletReady?: boolean; error?: unknown; retryTrial?: () => void };
flow?: DeploymentFlow;
isWalletReady?: boolean;
trialError?: unknown;
retryTrial?: () => void;
contactSupportUrl?: string;
flow?: Partial<FlowResult>;
autopilot?: Partial<FlowResult>;
useAutoDeploymentFlow?: typeof DEPENDENCIES.useAutoDeploymentFlow;
dependencies?: Partial<typeof DEPENDENCIES>;
} = {}
) {
const PhasedDeployProgressScene = input.dependencies?.PhasedDeployProgressScene ?? vi.fn(ComponentMock);
const useAutoDeploymentFlow: typeof DEPENDENCIES.useAutoDeploymentFlow = input.useAutoDeploymentFlow ?? (() => buildFlow(input.flow));

// Built as a plain object (not mock<T>) so the passed-through trialError stays referentially intact rather than deep-mocked.
const useEnsureTrialStarted: typeof DEPENDENCIES.useEnsureTrialStarted = () =>
({
isWalletReady: input.trial?.isWalletReady ?? true,
isLoading: false,
error: input.trial?.error,
refreshWallet: vi.fn(),
retryTrial: input.trial?.retryTrial ?? vi.fn()
}) as ReturnType<typeof DEPENDENCIES.useEnsureTrialStarted>;
const useAutoDeploymentFlow: typeof DEPENDENCIES.useAutoDeploymentFlow = input.useAutoDeploymentFlow ?? (() => buildAutopilot(input.autopilot));
const useServices: typeof DEPENDENCIES.useServices = () =>
mock<ReturnType<typeof DEPENDENCIES.useServices>>({
publicConfig: { NEXT_PUBLIC_CONTACT_SUPPORT_URL: input.contactSupportUrl ?? CONTACT_SUPPORT_URL }
Expand All @@ -155,14 +145,14 @@ describe(AutoDeployFlow.name, () => {
<AutoDeployFlow
templateName={input.templateName ?? "Hello World"}
sdl={input.sdl ?? "sdl-content"}
templateId={input.templateId}
dseq={input.dseq}
draftId={input.draftId}
resume={input.resume ?? { activeLeases: [] }}
flow={input.flow ?? mock<DeploymentFlow>()}
isWalletReady={input.isWalletReady ?? true}
trialError={input.trialError}
retryTrial={input.retryTrial ?? vi.fn()}
dependencies={{
Layout: ComponentMock,
PhasedDeployProgressScene,
useEnsureTrialStarted,
useAutoDeploymentFlow,
useServices,
...input.dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import { useCallback } from "react";
import Layout from "@src/components/layout/Layout";
import { useServices } from "@src/context/ServicesProvider";
import { useAutoDeploymentFlow } from "@src/hooks/useAutoDeploymentFlow/useAutoDeploymentFlow";
import { useEnsureTrialStarted } from "@src/hooks/useEnsureTrialStarted";
import { PhasedDeployProgressScene } from "../DeployProgressOverlay/PhasedDeployProgressScene";
import type { ResumeResolution } from "../ResumeDeploymentGuard/ResumeDeploymentGuard";
import type { DeploymentFlow } from "../useDeploymentFlow/useDeploymentFlow";

export const DEPENDENCIES = {
Layout,
PhasedDeployProgressScene,
useEnsureTrialStarted,
useAutoDeploymentFlow,
useServices
};
Expand All @@ -22,41 +21,40 @@ type Props = {
templateName: string;
/** The resolved SDL to deploy, already derived from the intent's template on the configure screen. */
sdl: string;
/** The template the flow deploys; mirrored into the flow's intent so a URL resume preserves it. */
templateId?: string;
/** The deployment's dseq when resuming (a reload of the progress view); absent on a fresh start. */
dseq?: string;
/** The active configure draft id, preserved in the URL when the created dseq is written so a reload resolves the same session. */
draftId?: string;
/** The resume resolution from the {@link ResumeDeploymentGuard}: any live leases to reconstruct so the manifest re-sends. */
resume: ResumeResolution;
/** The shared base flow, created once by the `DeploymentFlowProvider` (seeded from the resolved intent, including a resumed dseq). */
flow: DeploymentFlow;
/** Whether the trial wallet can broadcast — gates the autopilot's create. */
isWalletReady: boolean;
/** A terminal start-trial failure, projected to the flow's error state so a held create fails instead of waiting forever. */
trialError: unknown;
/** Clears a terminal start-trial failure so "Try again" can re-attempt it. */
retryTrial: () => void;
dependencies?: typeof DEPENDENCIES;
};

/**
* The auto-deploy experience of the bid-screening view: when the configure intent is `sdl-strategy=default` +
* `bid-strategy=auto`, the screen skips the manual form and drives the deployment automatically through the
* globe + phased progress panel (create deployment → matching providers → preparing), then redirects to the
* deployment details. Trial readiness is ensured here (mirroring the onboarding picker) so the flow can wait
* on the trial wallet spinning up before broadcasting. Rendered only in the auto branch so the trial is never
* auto-started for manual configure visitors.
* deployment details. The base flow and trial readiness come from the `DeploymentFlowProvider` (shared with the
* manual branch), so this component only autopilots over the flow, waiting on the trial wallet spinning up before
* broadcasting.
*
* The created deployment's dseq is written into the URL by the underlying `useDeploymentFlow` state machine (the
* phased flow autopilots over it), so a reload resumes the same deployment — picking up at matching, or at
* preparing if a lease already exists — instead of creating a new one. Deploy-success navigation is likewise owned
* by that flow's built-in redirect.
*/
export const AutoDeployFlow: FC<Props> = ({ templateName, sdl, templateId, dseq, draftId, resume, dependencies: d = DEPENDENCIES }) => {
const { isWalletReady, error: trialError, retryTrial } = d.useEnsureTrialStarted();
export const AutoDeployFlow: FC<Props> = ({ templateName, sdl, resume, flow, isWalletReady, trialError, retryTrial, dependencies: d = DEPENDENCIES }) => {
const { publicConfig } = d.useServices();
const { state, progressPercent, phases, matchedProviderAddress, tryAgain } = d.useAutoDeploymentFlow({
const { state, progressPercent, phases, matchedProviderAddress, tryAgain, stopAutopilot } = d.useAutoDeploymentFlow({
sdl,
isWalletReady,
trialError,
initialDseq: dseq,
templateId,
draftId,
resumeLeases: resume.activeLeases
resumeLeases: resume.activeLeases,
flow
});

/**
Expand All @@ -80,6 +78,12 @@ export const AutoDeployFlow: FC<Props> = ({ templateName, sdl, templateId, dseq,
onContactSupport={() => {
window.open(publicConfig.NEXT_PUBLIC_CONTACT_SUPPORT_URL, "_blank", "noopener,noreferrer");
}}
onChooseProvider={() => {
// Halt the autopilot before flipping to manual: the bid-strategy change only unmounts this flow once the URL
// propagates, so stopping here keeps the autopilot from leasing the shared deployment during that window.
stopAutopilot();
flow.actions.setBidStrategy("select");
}}
/>
</d.Layout>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { mock } from "vitest-mock-extended";
import sdlStore from "@src/store/sdlStore";
import type { TemplateCreation } from "@src/types";
import { helloWorldTemplate } from "@src/utils/templates";
import type { DeploymentFlow } from "../useDeploymentFlow/useDeploymentFlow";
import type { DEPENDENCIES } from "./ConfigureDeployment";
import { ConfigureDeployment } from "./ConfigureDeployment";

Expand Down Expand Up @@ -103,15 +104,36 @@ describe(ConfigureDeployment.name, () => {
expect(ConfigureDeploymentForm).toHaveBeenCalledWith(expect.objectContaining({ initialName: "resumed-name" }), expect.anything());
});

it("routes an auto-deploy intent to the auto flow with the shared flow, not the manual form", () => {
const { AutoDeployFlow, ConfigureDeploymentForm, DeploymentFlowProvider } = setup({
templateId: helloWorldTemplate.code,
sdlStrategy: "default",
bidStrategy: "auto"
});

expect(ConfigureDeploymentForm).not.toHaveBeenCalled();
expect(DeploymentFlowProvider).toHaveBeenCalledWith(
expect.objectContaining({ intent: expect.objectContaining({ sdlStrategy: "default", bidStrategy: "auto" }) }),
expect.anything()
);
expect(AutoDeployFlow).toHaveBeenCalledWith(expect.objectContaining({ sdl: helloWorldTemplate.content, flow: expect.anything() }), expect.anything());
});

function setup(input: {
templateId?: string | null;
sdlStrategy?: string;
bidStrategy?: string;
draftId?: string;
persistedSdl?: string;
persistedName?: string;
template?: { isLoading?: boolean; isError?: boolean; data?: TemplateOutput };
deploySdl?: TemplateCreation | null;
}) {
const ConfigureDeploymentForm = vi.fn(() => <div data-testid="form-mock" />);
const AutoDeployFlow = vi.fn(() => <div data-testid="auto-mock" />);
const DeploymentFlowProvider = vi.fn(({ children }) => (
<>{children({ flow: mock<DeploymentFlow>(), isWalletReady: true, trialError: undefined, retryTrial: vi.fn() })}</>
));
const enqueueSnackbar = vi.fn();
const usePublicTemplate = vi.fn(() => mock<ReturnType<typeof DEPENDENCIES.usePublicTemplate>>(input.template as never));
const save = vi.fn();
Expand All @@ -128,13 +150,16 @@ describe(ConfigureDeployment.name, () => {

const query: Record<string, string> = {};
if (input.templateId) query.templateId = input.templateId;
if (input.sdlStrategy) query["sdl-strategy"] = input.sdlStrategy;
if (input.bidStrategy) query["bid-strategy"] = input.bidStrategy;
const params = new URLSearchParams(query);

const dependencies: typeof DEPENDENCIES = {
Layout: vi.fn(({ children }) => <div data-testid="layout-mock">{children}</div>) as never,
NextSeo: vi.fn(() => null) as never,
AutoDeployFlow: vi.fn(() => null) as never,
AutoDeployFlow: AutoDeployFlow as never,
ConfigureDeploymentForm: ConfigureDeploymentForm as never,
DeploymentFlowProvider: DeploymentFlowProvider as never,
ResumeDeploymentGuard: vi.fn(({ children }) => <>{children({ activeLeases: [] })}</>) as never,
usePublicTemplate: usePublicTemplate as never,
useConfigureDraft: useConfigureDraft as never,
Expand All @@ -153,6 +178,6 @@ describe(ConfigureDeployment.name, () => {
</JotaiStoreProvider>
);

return { ConfigureDeploymentForm, usePublicTemplate, useConfigureDraft, enqueueSnackbar };
return { ConfigureDeploymentForm, AutoDeployFlow, DeploymentFlowProvider, usePublicTemplate, useConfigureDraft, enqueueSnackbar };
}
});
Loading