Skip to content
Open
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 @@ -4,6 +4,7 @@ import { mock } from "vitest-mock-extended";
import type { BidScreeningConfig } from "@src/bid-screening/config/env.config";
import type { FeatureFlagsService } from "@src/core/services/feature-flags/feature-flags.service";
import type { BidScreeningRequest } from "../../http-schemas/bid-screening.schema";
import { BidScreeningRequestSchema } from "../../http-schemas/bid-screening.schema";
import { BidScreeningController } from "./bid-screening.controller";

describe(BidScreeningController.name, () => {
Expand All @@ -27,6 +28,19 @@ describe(BidScreeningController.name, () => {
expect(JSON.parse(init?.body as string)).toEqual(request);
});

it("forwards a schema-parsed request body without BigInt serialization errors", async () => {
const fetchSpy = mockFetchJson({ providers: [] });
const { controller } = setup({});

const parsed = BidScreeningRequestSchema.parse(makeRequest());
await controller.screenProviders(parsed);

const [, init] = fetchSpy.mock.calls[0];
const body = JSON.parse(init?.body as string);
expect(body.resources[0].resource.cpu.units.val).toBe("1000");
expect(body.resources[0].resource.gpu.units.val).toBe("0");
});

it("short-circuits to an empty list when the feature flag is disabled", async () => {
const fetchSpy = vi.spyOn(globalThis, "fetch");
const { controller } = setup({ flagEnabled: false });
Expand Down
20 changes: 10 additions & 10 deletions apps/api/src/bid-screening/http-schemas/bid-screening.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ import { z } from "@hono/zod-openapi";

const UIntStringSchema = z.string().regex(/^\d+$/, "Must be an unsigned integer string");

/**
* Validates the resource value but does NOT transform it. This service only proxies the request to
* provider-inventory, which parses the value itself; transforming to BigInt here would break the
* `JSON.stringify` forward in the controller. Value is a non-negative integer string or its
* protobuf base64-encoded representation.
*/
const ResourceValueSchema = z.object({
val: z
.string()
.max(80)
.transform(str => {
if (/^\d+$/.test(str)) return BigInt(str);
const parsed = Buffer.from(str, "base64").toString("utf-8");
if (/^\d+$/.test(parsed)) return BigInt(parsed);
return NaN;
})
.refine(
(val): val is bigint => !Number.isFinite(val) && typeof val === "bigint" && val >= 0n,
"Must be a non-negative integer or its protobuf base64-encoded representation"
)
.refine(str => {
const decoded = /^\d+$/.test(str) ? str : Buffer.from(str, "base64").toString("utf-8");
return /^\d+$/.test(decoded);
}, "Must be a non-negative integer or its protobuf base64-encoded representation")
});

// Mirrors AttributeNameRegexpStringWildcard in akash-network/chain-sdk
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const bidScreeningRouter = new OpenApiHonoHandler();

const postBidScreeningRoute = createRoute({
method: "post",
operationId: "screenProviders",
path: "/v1/bid-screening",
summary: "Screen providers by deployment resource requirements",
tags: ["Bid Screening"],
Expand Down
10 changes: 9 additions & 1 deletion apps/api/swagger/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -16378,6 +16378,7 @@
},
"/v1/bid-screening": {
"post": {
"operationId": "screenProviders",
"summary": "Screen providers by deployment resource requirements",
"tags": [
"Bid Screening"
Expand Down Expand Up @@ -16736,13 +16737,20 @@
"format": "date-time",
"description": "ISO 8601 timestamp marking when the provider was first enrolled in the inventory",
"example": "2026-01-01T00:00:00.000Z"
},
"location": {
"type": "string",
"description": "Provider region from the location-region attribute (signed preferred, else self-declared); null if unset",
"example": "us-west",
"nullable": true
}
},
"required": [
"owner",
"hostUri",
"isAudited",
"createdAt"
"createdAt",
"location"
]
}
}
Expand Down
1 change: 1 addition & 0 deletions apps/api/test/functional/__snapshots__/docs.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2739,6 +2739,7 @@ exports[`API Docs > GET /v1/doc > returns docs with all routes expected 1`] = `
},
"/v1/bid-screening": {
"post": {
"operationId": "screenProviders",
"requestBody": {
"content": {
"application/json": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,19 @@ import { render, screen } from "@testing-library/react";
describe("ConfigurationPane", () => {
it("shows the selected service title", () => {
const values = defaultServiceWithPlacement({ title: "api" });
setup({ values, selectedServiceId: values.services[0].id as string });
setup({ values, selectedServiceId: values.services[0].id });

expect(screen.getByText("api")).toBeInTheDocument();
});

it("shows no target when nothing is selected", () => {
it("shows no target when the selection matches no service", () => {
const values = defaultServiceWithPlacement({ title: "api" });
setup({ values, selectedServiceId: null });
setup({ values, selectedServiceId: "missing" });

expect(screen.queryByText("api")).not.toBeInTheDocument();
});

function setup(input: { values: SdlBuilderFormValuesType; selectedServiceId: string | null }) {
function setup(input: { values: SdlBuilderFormValuesType; selectedServiceId: string }) {
const Wrapper = ({ children }: PropsWithChildren) => {
const form = useForm<SdlBuilderFormValuesType>({ defaultValues: input.values });
return <FormProvider {...form}>{children}</FormProvider>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useFormContext, useWatch } from "react-hook-form";
import type { SdlBuilderFormValuesType } from "@src/types";

type Props = {
selectedServiceId: string | null;
selectedServiceId: string;
};

export const ConfigurationPane: FC<Props> = ({ selectedServiceId }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,19 @@ const TWO_SERVICE_SDL = [

describe(ConfigureDeploymentForm.name, () => {
it("seeds the panes with the carried-in template SDL", () => {
const { ConfigureDeploymentPanes } = setup({ initialSdl: 'version: "2.0"' });
const { ConfigureDeploymentPanes } = setup({ initialSdl: VALID_SDL });

expect(ConfigureDeploymentPanes).toHaveBeenCalledWith(expect.objectContaining({ sdl: VALID_SDL }), expect.anything());
});

it("falls back to a default deployment when the carried-in SDL has no services", () => {
const { ConfigureDeploymentPanes, enqueueSnackbar } = setup({ initialSdl: 'version: "2.0"' });

expect(ConfigureDeploymentPanes).toHaveBeenCalledWith(expect.objectContaining({ sdl: 'version: "2.0"' }), expect.anything());
expect(enqueueSnackbar).not.toHaveBeenCalled();
expect(ConfigureDeploymentPanes).toHaveBeenCalledWith(
expect.objectContaining({ sdl: expect.stringContaining('version: "2.0"'), selectedServiceId: expect.any(String) }),
expect.anything()
);
});

it("generates the SDL of the default deployment when no SDL was provided", () => {
Expand Down Expand Up @@ -180,7 +190,7 @@ describe(ConfigureDeploymentForm.name, () => {

interface ProbePanesProps {
sdl: string;
selectedServiceId: string | null;
selectedServiceId: string;
}

/** Panes stand-in that mutates the shared form to drive the SDL preview subscription. */
Expand Down Expand Up @@ -210,7 +220,7 @@ function SelectionProbePanes({ selectedServiceId }: ProbePanesProps) {
const firstServiceId = Array.isArray(services) ? (services as SdlBuilderFormValuesType["services"])[0]?.id : undefined;
return (
<div>
<div data-testid="selected">{selectedServiceId ?? ""}</div>
<div data-testid="selected">{selectedServiceId}</div>
<div data-testid="first-service-id">{firstServiceId ?? ""}</div>
<button type="button" onClick={() => setValue("services", [defaultService(getValues("placements")[0].id, { title: "service-2" })])}>
replace services
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import type { FC } from "react";
import { useEffect, useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { FormProvider, useForm, useWatch } from "react-hook-form";
import { Snackbar } from "@akashnetwork/ui/components";
import { zodResolver } from "@hookform/resolvers/zod";
import debounce from "lodash/debounce";
Expand Down Expand Up @@ -31,13 +31,16 @@ type Props = {
export const ConfigureDeploymentForm: FC<Props> = ({ initialSdl, dependencies: d = DEPENDENCIES }) => {
const [initialState] = useState(() => getInitialState(initialSdl));
const [sdl, setSdl] = useState(initialState.sdl);
const [selectedServiceId, setSelectedServiceId] = useState<string | null>(initialState.selectedServiceId);
const [selectedServiceId, setSelectedServiceId] = useState<string>(initialState.selectedServiceId);
const { enqueueSnackbar } = d.useSnackbar();
const form = useForm<SdlBuilderFormValuesType>({
defaultValues: initialState.values,
mode: "onChange",
resolver: zodResolver(SdlBuilderFormValuesSchema)
});
const services = useWatch({ control: form.control, name: "services" });
const placements = useWatch({ control: form.control, name: "placements" });
const selectedPlacementName = resolveSelectedPlacementName(services, placements, selectedServiceId);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

useEffect(
function notifyOnImportError() {
Expand Down Expand Up @@ -83,7 +86,12 @@ export const ConfigureDeploymentForm: FC<Props> = ({ initialSdl, dependencies: d
<d.ConfigureDeploymentHeader />
</div>
<div className="mt-6 flex min-h-0 flex-1">
<d.ConfigureDeploymentPanes sdl={sdl} selectedServiceId={selectedServiceId} onSelectService={setSelectedServiceId} />
<d.ConfigureDeploymentPanes
sdl={sdl}
selectedServiceId={selectedServiceId}
selectedPlacementName={selectedPlacementName}
onSelectService={setSelectedServiceId}
/>
</div>
</FormProvider>
</d.Layout>
Expand All @@ -93,29 +101,39 @@ export const ConfigureDeploymentForm: FC<Props> = ({ initialSdl, dependencies: d
interface InitialState {
values: SdlBuilderFormValuesType;
sdl: string;
selectedServiceId: string | null;
selectedServiceId: string;
importError?: string;
}

/**
* Derives the form values, preview SDL, and initial selection from one source
* so they can never diverge. A parseable carried-in template keeps its exact
* SDL text for the preview; if it can't be imported the screen falls back to a
* default deployment and reports `importError` so the failure is surfaced
* rather than silently swallowed.
* Derives the form values, preview SDL, and initial selection from one source so they can never diverge.
* A carried-in SDL is used only when it imports to a usable deployment (at least one visible service);
* otherwise — invalid YAML, or a service-less SDL the configure screen can't work with — the screen falls
* back to a default deployment. This guarantees there is always a service (and placement) to select.
*/
function getInitialState(carriedInSdl: string | undefined): InitialState {
if (carriedInSdl) {
try {
const values = importSimpleSdl(carriedInSdl);
return { values, sdl: carriedInSdl, selectedServiceId: seedSelectedServiceId(values) };
if (hasVisibleService(values)) {
return { values, sdl: carriedInSdl, selectedServiceId: seedSelectedServiceId(values) };
}
} catch (error) {
const values = defaultServiceWithPlacement();
return { values, sdl: regenerateSdl(values, ""), selectedServiceId: seedSelectedServiceId(values), importError: getImportErrorMessage(error) };
return defaultInitialState(getImportErrorMessage(error));
}
}
return defaultInitialState();
}

/** A fresh default deployment, optionally annotated with the error that made an import unusable. */
function defaultInitialState(importError?: string): InitialState {
const values = defaultServiceWithPlacement();
return { values, sdl: regenerateSdl(values, ""), selectedServiceId: seedSelectedServiceId(values) };
return { values, sdl: regenerateSdl(values, ""), selectedServiceId: seedSelectedServiceId(values), importError };
}

/** A usable deployment has at least one service the user can configure (log collectors don't count). */
function hasVisibleService(values: SdlBuilderFormValuesType): boolean {
return values.services.some(service => !isLogCollectorService(service));
}

/**
Expand All @@ -130,9 +148,10 @@ function getImportErrorMessage(error: unknown): string {
return "The deployment couldn't be loaded.";
}

/** Picks the first user-visible service to focus when the screen first mounts. */
function seedSelectedServiceId(values: SdlBuilderFormValuesType): string | null {
return values.services.find(service => !isLogCollectorService(service))?.id ?? null;
/** Picks the first user-visible service to focus when the screen first mounts. `getInitialState` guarantees one exists. */
function seedSelectedServiceId(values: SdlBuilderFormValuesType): string {
const visible = values.services.find(candidate => !isLogCollectorService(candidate)) ?? values.services[0];
return visible.id;
}

/** Regenerates the preview SDL, keeping the last good output while the form is mid-edit. */
Expand All @@ -144,11 +163,29 @@ function regenerateSdl(values: SdlBuilderFormValuesType, previous: string): stri
}
}

/**
* Resolves the placement the marketplace is scoped to. There is always a placement and a service, so this
* returns a name rather than null: it uses the selected service when present, otherwise the first visible
* service (which also covers the brief window after a removal, before the reselect effect runs), and falls
* back to the first placement.
*/
function resolveSelectedPlacementName(
services: SdlBuilderFormValuesType["services"],
placements: SdlBuilderFormValuesType["placements"],
selectedServiceId: string
): string {
const selected = services.find(candidate => candidate.id === selectedServiceId);
const service = selected ?? services.find(candidate => !isLogCollectorService(candidate));
const placement = (service && placements.find(candidate => candidate.id === service.placementId)) || placements[0];
return placement.name;
}

/** Keeps the selection on an existing service, falling back to the first visible one after a removal. */
function nextSelectedServiceId(values: SdlBuilderFormValuesType, previous: string | null): string | null {
function nextSelectedServiceId(values: SdlBuilderFormValuesType, previous: string): string {
const services = values.services ?? [];
if (previous && services.some(service => service?.id === previous)) {
if (services.some(candidate => candidate?.id === previous)) {
return previous;
}
return services.find(service => service && !isLogCollectorService(service as ServiceType))?.id ?? null;
const visible = services.find(candidate => candidate && !isLogCollectorService(candidate as ServiceType)) ?? services[0];
return visible.id;
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,19 @@ describe("ConfigureDeploymentPanes", () => {
expect(ConfigurationPane).toHaveBeenCalledWith(expect.objectContaining({ selectedServiceId: "svc-1" }), expect.anything());
});

it("threads the sdl and selected placement into the marketplace pane", () => {
const { MarketplacePane } = setup({ sdl: 'version: "2.0"', selectedPlacementName: "dcloud" });

expect(MarketplacePane).toHaveBeenCalledWith(expect.objectContaining({ sdl: 'version: "2.0"', placementName: "dcloud" }), expect.anything());
});

function setup(
input: {
isSdlPreviewEnabled?: boolean;
sdl?: string;
preserveStorage?: boolean;
selectedServiceId?: string | null;
selectedServiceId?: string;
selectedPlacementName?: string;
onSelectService?: (serviceId: string) => void;
} = {}
) {
Expand All @@ -114,10 +121,11 @@ describe("ConfigureDeploymentPanes", () => {
));
const DeploymentPane = vi.fn(() => <div data-testid="deployment-pane-mock" />);
const ConfigurationPane = vi.fn(() => <div data-testid="configuration-pane-mock" />);
const MarketplacePane = vi.fn(() => <div data-testid="marketplace-pane-mock" />);
const dependencies: typeof DEPENDENCIES = {
DeploymentPane: DeploymentPane as never,
ConfigurationPane: ConfigurationPane as never,
MarketplacePane: vi.fn(() => <div data-testid="marketplace-pane-mock" />),
MarketplacePane: MarketplacePane as never,
SdlPreviewPane: SdlPreviewPane as never,
useFlag: (() => input.isSdlPreviewEnabled ?? false) as never
};
Expand All @@ -130,13 +138,14 @@ describe("ConfigureDeploymentPanes", () => {
<JotaiStoreProvider store={createStore()}>
<ConfigureDeploymentPanes
sdl={input.sdl ?? ""}
selectedServiceId={input.selectedServiceId ?? null}
selectedServiceId={input.selectedServiceId ?? ""}
selectedPlacementName={input.selectedPlacementName ?? "dcloud"}
onSelectService={input.onSelectService ?? vi.fn()}
dependencies={dependencies}
/>
</JotaiStoreProvider>
);

return { SdlPreviewPane, DeploymentPane, ConfigurationPane, unmount };
return { SdlPreviewPane, DeploymentPane, ConfigurationPane, MarketplacePane, unmount };
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ export const DEPENDENCIES = { DeploymentPane, ConfigurationPane, MarketplacePane

type Props = {
sdl: string;
selectedServiceId: string | null;
selectedServiceId: string;
selectedPlacementName: string;
onSelectService: (serviceId: string) => void;
dependencies?: typeof DEPENDENCIES;
};

export const ConfigureDeploymentPanes: FC<Props> = ({ sdl, selectedServiceId, onSelectService, dependencies: d = DEPENDENCIES }) => {
export const ConfigureDeploymentPanes: FC<Props> = ({ sdl, selectedServiceId, selectedPlacementName, onSelectService, dependencies: d = DEPENDENCIES }) => {
const [activePane, setActivePane] = useState<ActivePane>("deployment");
const [isSdlPreviewOpen, setIsSdlPreviewOpen] = useAtom(sdlStore.sdlPreviewOpen);
const isSdlPreviewEnabled = d.useFlag("ui_sdl_preview_panel");
Expand All @@ -36,7 +37,7 @@ export const ConfigureDeploymentPanes: FC<Props> = ({ sdl, selectedServiceId, on
<d.ConfigurationPane selectedServiceId={selectedServiceId} />
</div>
<div className={cn("min-h-0 md:block", { hidden: activePane !== "marketplace" })}>
<d.MarketplacePane />
<d.MarketplacePane sdl={sdl} placementName={selectedPlacementName} />
</div>
{isSdlPreviewEnabled && (
<d.SdlPreviewPane sdl={sdl} isOpen={isSdlPreviewOpen} onOpen={() => setIsSdlPreviewOpen(true)} onClose={() => setIsSdlPreviewOpen(false)} />
Expand Down
Loading